Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fec8669
fix(cfl): resolve --config split-brain resolution (#455)
rianjs Jul 17, 2026
efa4cbb
feat(cfl)!: standardize page body formats with --body-format (#455)
rianjs Jul 17, 2026
a1c7677
feat(cfl)!: explicit search scope, strict limits, implement page view…
rianjs Jul 17, 2026
b8bd6c5
fix(cfl): address review feedback on search/limits/--full batch (#455)
rianjs Jul 17, 2026
9ffca95
Merge branch 'main' into 455-cfl-config
rianjs Jul 17, 2026
6d343f1
fix(cfl): address review feedback on body-format batch (#455)
rianjs Jul 17, 2026
836add3
Merge branch '455-cfl-body-format' into 455-cfl-search
rianjs Jul 17, 2026
af89c01
fix(cfl): thread --config path through init and config show (#455)
rianjs Jul 17, 2026
33c005e
Merge branch '455-cfl-config' into 455-cfl-body-format
rianjs Jul 17, 2026
177033c
Merge remote-tracking branch 'origin/455-cfl-config' into 455-cfl-config
rianjs Jul 17, 2026
d6d77b5
Merge branch '455-cfl-config' into 455-cfl-body-format
rianjs Jul 17, 2026
ddea625
fix(cfl): exact empty bodies, ADF envelope validation, plain-mode doc…
rianjs Jul 17, 2026
541e0ce
Merge branch '455-cfl-body-format' into 455-cfl-search
rianjs Jul 17, 2026
5c249cc
fix(cfl): explicit --config skips shared-store layering (#455)
rianjs Jul 17, 2026
e171814
Merge branch '455-cfl-config' into 455-cfl-body-format
rianjs Jul 17, 2026
60ab320
fix(cfl): exact body formats bypass truncation (#455)
rianjs Jul 17, 2026
78e3ff2
Merge branch '455-cfl-body-format' into 455-cfl-search
rianjs Jul 17, 2026
d87e0a9
docs(cfl): correct Go prerequisite and document bin/cfl invocation (#…
rianjs Jul 17, 2026
8fbd5ad
fix(cfl): omit absent --full fields, correct scripting and --full doc…
rianjs Jul 17, 2026
56c859e
Merge origin/main after #457/#459 squash (branch content already pres…
rianjs Jul 17, 2026
655b85c
fix(cfl): restore #459 final-round changes clobbered by -X ours recon…
rianjs Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ cfl page create --space DEV --title "New Page" --file content.md
cfl page edit 123456

# Search content
cfl search "my search query"
cfl search "my search query" # Global search
cfl search "my search query" --space DEV # Explicit space scope
cfl search --cql "space = DEV AND type = page"

# Copy a page
Expand Down
5 changes: 4 additions & 1 deletion docs/ARTIFACT_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The full artifact provides richer inspection-oriented output for debugging and d

**Examples:**
- `jtk issues get --full` → agent fields + created, updated, reporter, components, labels
- `cfl page view --full` → agent fields + version, created, modified, author
- `cfl page view --full` → agent fields + parent ID, creation time, author ID

## Page Body Format (cfl)

Expand All @@ -57,6 +57,9 @@ The full artifact provides richer inspection-oriented output for debugging and d
--body-format xhtml → selected artifact with exact storage XHTML body
```

`cfl page view --full` cannot be combined with `--content-only` or `--web`.
Commands without a defined full artifact reject `--full` instead of ignoring it.

## Output Format

JTK and CFL both use text-first output. The `-o json` resource surface has been removed from both tools (JTK earlier, then CFL via #392); CFL retains `-o table` and `-o plain` only. JSON is reserved for control-plane envelopes (`cfl set-credential --json`, `jtk set-credential --json`) and round-trip payloads (`jtk automation export`).
Expand Down
18 changes: 11 additions & 7 deletions skills/Confluence/CliReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cfl config test
| Flag | Description |
|------|-------------|
| `-o, --output FORMAT` | Output format (see SKILL.md "Output Representation and Format"): `table` (default), `plain` |
| `--full` | Inspection-oriented representation (see SKILL.md). Not a content-truncation flag — for `page view` content truncation, use `--no-truncate`. |
| `--full` | Inspection additions on supported page/space views and list/search commands. Unsupported commands reject it. Not a content-truncation flag — use `--no-truncate` for that. |
| `--no-color` | Disable colored output |
| `-c, --config PATH` | Override config file location (default: `~/.config/cfl/config.yml`) |

Expand All @@ -41,6 +41,7 @@ cfl [resource] [action] [ID] [flags]
|---------|-------------|
| `cfl page list --space KEY` | List pages in space |
| `cfl page view PAGE_ID` | View page content as markdown (truncated at 5000 chars by default) |
| `cfl page view PAGE_ID --full` | Add parent ID, creation time, and author ID metadata |
| `cfl page view PAGE_ID --no-truncate` | View full content without truncation |
| `cfl page view PAGE_ID --content-only` | Output content only (no metadata headers); implies `--no-truncate` |
| `cfl page view PAGE_ID --body-format xhtml` | View exact Confluence storage XHTML |
Expand Down Expand Up @@ -83,6 +84,8 @@ cfl [resource] [action] [ID] [flags]
| `--show-macros` | Show macro placeholders (e.g. `[TOC]`) instead of stripping them. Markdown-only — rejected with `--body-format adf|xhtml` |
| `-w, --web` | Open in browser |

`--full` composes with `--body-format` and is incompatible with `--content-only` and `--web`.

### Page List Flags

| Flag | Description |
Expand Down Expand Up @@ -115,25 +118,26 @@ echo "# Hello World" | cfl page create -s DEV -t "My Page"

| Command | Description |
|---------|-------------|
| `cfl search "query"` | Full-text search |
| `cfl search "query"` | Global full-text search |
| `cfl search "query" --space KEY` | Search within space |
| `cfl search "query" --type page` | Search pages only |
| `cfl search --label TAG` | Filter by label |
| `cfl search --title "TEXT"` | Filter by title |
| `cfl search --cql "CQL_QUERY"` | Raw CQL query |

**Note:** When `--cql` is provided, it takes precedence over the positional `[query]` argument. Don't combine them.
**Scope:** Search is global unless `--space` is explicit; configured `default_space` is not used.
Raw `--cql` cannot be combined with the positional query or any builder flag.

### Search Flags

| Flag | Description |
|------|-------------|
| `--space KEY` / `-s` | Filter by space key |
| `--space KEY` / `-s` | Explicitly filter by space key |
| `--type TYPE` / `-t` | Content type: `page`, `blogpost`, `attachment`, `comment` |
| `--label TAG` | Filter by label |
| `--title "TEXT"` | Filter by title (contains) |
| `--cql "QUERY"` | Raw CQL query (advanced). Takes precedence over positional query. |
| `--limit N` / `-l` | Max results (default 25) |
| `--cql "QUERY"` | Raw CQL query; mutually exclusive with query, space, type, title, and label inputs |
| `--limit N` / `-l` | Max results, greater than zero (default 25) |

### Common CQL Patterns

Expand Down Expand Up @@ -186,7 +190,7 @@ echo "# Hello World" | cfl page create -s DEV -t "My Page"
| Command | Description |
|---------|-------------|
| `cfl attachment list --page PAGE_ID` | List attachments on page |
| `cfl attachment list --page PAGE_ID --limit 50` | List with custom limit (default 25) |
| `cfl attachment list --page PAGE_ID --limit 50` | List with positive custom limit (default 25) |
| `cfl attachment list --page PAGE_ID --unused` | List orphaned attachments (not referenced in page content) |
| `cfl attachment upload --page PAGE_ID --file PATH` | Upload attachment |
| `cfl attachment upload --page PAGE_ID --file PATH -m "comment"` | Upload with comment |
Expand Down
7 changes: 5 additions & 2 deletions skills/Confluence/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ If still missing: ask the user. Then suggest they persist it:
- **Config file:** edit the `default_space` field in the file shown by `cfl config show`

Env var wins over config. Once set, space-scoped commands work without `--space`.
Search is the exception: `cfl search` is global unless `--space` is provided explicitly.

### Page ID, attachment ID, etc.

Expand All @@ -46,11 +47,13 @@ No defaults exist. Ask the user. Do not guess. If the user provides a Confluence

- **Representation** — what content is shown:
- `agent` (default) — curated, action-oriented, LLM-optimized
- `full` (`--full`) — inspection-oriented, additional fields (dates, authors, versions)
- `full` (`--full`) — inspection-oriented additions on page/space views and supported list/search commands
- **Page body format** — `--body-format markdown|adf|xhtml` on page view/create/edit. Omission means Markdown; ADF and XHTML are exact.
- **Output format** — how it's rendered: `table` (default), `plain` (`-o plain`). For scripted extraction, parse `ID: <id>` lines from command output.
- **Output format** — how it's rendered: `table` (default) or `plain` (`-o plain`). Detail and mutation commands expose IDs as `ID: <id>` lines; for list/search commands, use `-o plain` TSV and read the first (`ID`) column.

They combine freely — e.g., `cfl page list --full -o plain` returns TSV; detail output in plain mode remains semantically equivalent text.
`page view --full` composes with every body format, but not with `--content-only` or `--web`.
Commands without a defined full artifact reject `--full`.

### Extracting Page IDs from URLs

Expand Down
2 changes: 1 addition & 1 deletion skills/Confluence/Workflows/ManageAttachments.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ List, upload, download, and delete attachments on Confluence pages.
```bash
cfl attachment list --page PAGE_ID

# Increase result count (default 25)
# Increase result count with a positive limit (default 25)
cfl attachment list --page PAGE_ID --limit 100

# Show only unused/orphaned attachments
Expand Down
31 changes: 12 additions & 19 deletions skills/Confluence/Workflows/SearchPages.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ Search, find, and filter Confluence pages using full-text search, CQL, or space-
| "search in SPACE" | `cfl search "query" --space KEY` | Space-scoped search |
| "find pages with label" | `cfl search --label TAG` | Label-based search |
| "search by title" | `cfl search --title "text"` | Title-based search |
| "CQL", advanced query | `cfl search --cql "CQL"` | Raw CQL query (takes precedence over positional query) |
| "CQL", advanced query | `cfl search --cql "CQL"` | Raw CQL query |
| "list pages in SPACE" | `cfl page list --space KEY` | Simple space listing |

**Note:** `--cql` takes precedence over the positional `[query]` argument. Don't combine them — use one or the other.
**Scope:** Searches are global unless `--space` is provided explicitly. `default_space` is not applied.
Raw `--cql` cannot be combined with a positional query or `--space`, `--type`, `--title`, or `--label`.

### Common Filters (CQL Building Blocks)

Expand Down Expand Up @@ -58,24 +59,19 @@ cfl search --cql "type=page AND space=KEY AND lastModified > now('-7d')"
cfl page list --space KEY
```

Use `--limit N` to control result count (default 25).
Use a positive `--limit N` to control result count (default 25).

### Scripting / Parsing Output

When the next step depends on extracting a page ID from the results, request JSON output. With `-o json`, the output has this structure:
When the next step depends on extracting a page ID from the results, use `-o plain`. List and search output is TSV with a header row; the first column is `ID`.

```json
{
"results": [
{ "id": "...", "title": "...", "type": "page", "spaceName": "...", "excerpt": "..." }
],
"_meta": { "count": 0, "hasMore": false }
}
```bash
cfl search --title "Release plan" --space DEV -o plain | awk -F '\t' 'NR > 1 { print $1 }'
```

The `--title` filter does substring matching, so multiple pages may be returned — narrow with `--space` when you need a single result.

Avoid pattern-matching against default `table` output — it's human-oriented and layout may change. Always use `-o json` when a downstream step parses the response.
Avoid pattern-matching against default `table` output — it's human-oriented and layout may change. Use `-o plain` TSV when a downstream step parses the response.

## Output Format

Expand All @@ -92,11 +88,8 @@ After returning results:
3. If no results, suggest broadening or adjusting filters
4. Offer to view any specific page from the results

## Missing Space Key

If a space key is needed but not specified in the request, consult `cfl`'s defaulting order (`--space` flag → `CFL_DEFAULT_SPACE` env var → `default_space` in config file). If still missing, ask the user and suggest persisting it:

- **Env var (per-shell):** `export CFL_DEFAULT_SPACE=KEY` — add to `.bashrc` / `.zshrc` / equivalent to persist
- **Config file:** edit the `default_space` field in the file shown by `cfl config show`
## Search Scope

Env var wins over config. See the **Defaults & Missing Inputs** section in `SKILL.md` for the full rationale.
Omit `--space` only when global search is intended. If the user asks for a
space-scoped search without naming the space, ask for the key; do not substitute
the configured default.
3 changes: 3 additions & 0 deletions skills/Confluence/Workflows/ViewPage.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ The default Markdown body is truncated at 5000 chars; `--show-macros` follows th
| User Says | Command | When to Use |
|-----------|---------|-------------|
| "view page", "show page", "read page" | `cfl page view PAGE_ID` | Default markdown view (subject to truncation) |
| "show page metadata", "inspect page" | `cfl page view PAGE_ID --full` | Add parent ID, creation time, and author ID |
| "show full page", "all content", "no truncation" | `cfl page view PAGE_ID --no-truncate` | Full content without truncation |
| "just the content", "content only" | `cfl page view PAGE_ID --content-only` | Content without metadata headers (implies `--no-truncate`) |
| "XHTML", "storage format" | `cfl page view PAGE_ID --body-format xhtml` | Complete exact storage XHTML |
| "ADF", "Atlassian document format" | `cfl page view PAGE_ID --body-format adf` | Complete exact ADF JSON |
| "show macros" | `cfl page view PAGE_ID --show-macros` | Preserve macro placeholders like `[TOC]` (subject to truncation) |
| "open in browser", "open page" | `cfl page view PAGE_ID --web` | Opens in default browser |

`--full` composes with every `--body-format` and is incompatible with `--content-only` and `--web`.

### Finding Page IDs

If the user provides a page title instead of ID, search first:
Expand Down
1 change: 1 addition & 0 deletions tools/cfl/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

### Changed

- Search is global unless `--space` is explicit, raw CQL conflicts are rejected, attachment/search limits must be positive, and `page view --full` now emits its documented metadata ([#455](https://github.com/open-cli-collective/atlassian-cli/issues/455))
- `page view`, `page create`, and `page edit` now use `--body-format markdown|adf|xhtml`; removed `--raw`, `--storage`, and `--no-markdown`, made editor buffers representation-safe, and made Markdown conversion fail closed ([#455](https://github.com/open-cli-collective/atlassian-cli/issues/455))
- Improved init and config test UX with user details display ([#56](https://github.com/open-cli-collective/atlassian-cli/pull/56))

Expand Down
26 changes: 19 additions & 7 deletions tools/cfl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,18 @@ sudo rpm -i cfl-VERSION.x86_64.rpm

### From Source

**Go install**
**Build from a clone**

```bash
go install github.com/open-cli-collective/confluence-cli/cmd/cfl@latest
git clone https://github.com/open-cli-collective/atlassian-cli.git
cd atlassian-cli
make build-cfl
./bin/cfl --version
```

Requires Go 1.24 or later.
The binary is written to `bin/cfl`; invoke it as `./bin/cfl` or copy it somewhere on your `PATH` (e.g. `install bin/cfl /usr/local/bin/`).

Requires Go 1.26 or later. (`go install` of the old `confluence-cli` module path installs a stale pre-monorepo binary; use Homebrew or a clone instead.)

## Quick Start

Expand Down Expand Up @@ -186,6 +191,7 @@ These flags are available on all commands:
| `--config` | `-c` | `~/.config/cfl/config.yml` | Path to config file |
| `--output` | `-o` | `table` | Output format: `table`, `plain` |
| `--no-color` | | `false` | Disable colored output |
| `--full` | | `false` | Inspection-oriented output; supported only by `page list`, `page view`, `space list`, `space view`, `attachment list`, and `search` (rejected elsewhere) |
| `--help` | `-h` | | Show help for command |
| `--version` | `-v` | | Show version (root command only) |

Expand Down Expand Up @@ -286,6 +292,7 @@ View a Confluence page. **Content is displayed as markdown by default.**

```bash
cfl page view 12345
cfl page view 12345 --full # Add parent/creation/author metadata
cfl page view 12345 --body-format xhtml
cfl page view 12345 --body-format adf
cfl page view 12345 --version 7
Expand All @@ -303,6 +310,8 @@ cfl page view 12345 --show-macros --content-only | cfl page edit 12345 --legacy
| `--show-macros` | | `false` | Show Confluence macro placeholders (e.g., `[TOC]`) instead of stripping them |
| `--content-only` | | `false` | Output only page content (no Title/ID/Version headers); implies `--no-truncate` |

`--full` composes with every body format and is incompatible with `--content-only` and `--web`.

**Arguments:**
- `<page-id>` - The page ID (**required**)

Expand Down Expand Up @@ -486,6 +495,7 @@ cfl page delete 12345 --force
### `cfl search [query]`

Search for pages, blog posts, attachments, and comments across Confluence.
Searches are global unless `--space` is provided explicitly; `default_space` is not used.

Uses Confluence Query Language (CQL) under the hood. Convenient flags handle common
filters, or use `--cql` for advanced queries.
Expand Down Expand Up @@ -515,16 +525,18 @@ cfl search --cql "type=page AND space=DEV AND lastModified > now('-7d')"

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--cql` | | | Raw CQL query (advanced) |
| `--space` | `-s` | (from config) | Filter by space key |
| `--cql` | | | Raw CQL query (cannot be combined with query or builder flags) |
| `--space` | `-s` | | Filter by space key |
| `--type` | `-t` | | Content type: `page`, `blogpost`, `attachment`, `comment` |
| `--title` | | | Filter by title (contains) |
| `--label` | | | Filter by label |
| `--limit` | `-l` | `25` | Maximum number of results |
| `--limit` | `-l` | `25` | Maximum number of results (must be greater than zero) |

**Arguments:**
- `[query]` - Full-text search terms (optional if using filters)

Raw `--cql` is mutually exclusive with `[query]`, `--space`, `--type`, `--title`, and `--label`.

**CQL Reference:**
Common CQL operators for `--cql`:
- `=` exact match: `type=page`
Expand Down Expand Up @@ -600,7 +612,7 @@ cfl attachment list --page 12345 --unused
| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--page` | `-p` | | Page ID (**required**) |
| `--limit` | `-l` | `25` | Maximum number of attachments to return |
| `--limit` | `-l` | `25` | Maximum number of attachments to return (must be greater than zero) |
| `--unused` | | `false` | Show only attachments not referenced in page content |

---
Expand Down
9 changes: 9 additions & 0 deletions tools/cfl/api/pages.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@ func normalizeVersionedPage(page *Page, pageID string, version Version, currentP
if page.Links.WebUI == "" {
page.Links.WebUI = currentPage.Links.WebUI
}
if page.ParentID == "" {
page.ParentID = currentPage.ParentID
}
if page.AuthorID == "" {
page.AuthorID = currentPage.AuthorID
}
if page.CreatedAt == nil {
page.CreatedAt = currentPage.CreatedAt
}
}
version.Page = nil
page.Version = &version
Expand Down
6 changes: 6 additions & 0 deletions tools/cfl/api/pages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ func TestClient_GetPageVersion_LocatesAndFetchesSingleBody(t *testing.T) {
"id": "12345",
"title": "History Page",
"spaceId": "987",
"parentId": "456",
"authorId": "author-1",
"createdAt": "2024-02-03T04:05:06Z",
"version": {"number": 3},
"_links": {"webui": "/spaces/DEV/pages/12345"}
}`))
Expand Down Expand Up @@ -212,6 +215,9 @@ func TestClient_GetPageVersion_LocatesAndFetchesSingleBody(t *testing.T) {
testutil.Equal(t, "12345", page.ID)
testutil.Equal(t, "History Page", page.Title)
testutil.Equal(t, "987", page.SpaceID)
testutil.Equal(t, "456", page.ParentID)
testutil.Equal(t, "author-1", page.AuthorID)
testutil.NotNil(t, page.CreatedAt)
testutil.Equal(t, 2, page.Version.Number)
testutil.Equal(t, "author-2", page.Version.AuthorID)
testutil.Equal(t, "<p>Version 2</p>", page.Body.Storage.Value)
Expand Down
3 changes: 3 additions & 0 deletions tools/cfl/api/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ func (c *Client) Search(ctx context.Context, opts *SearchOptions) (*SearchRespon
// Build CQL query
cql := ""
if opts != nil {
if opts.CQL != "" && (opts.Text != "" || opts.Space != "" || opts.Type != "" || opts.Title != "" || opts.Label != "") {
return nil, fmt.Errorf("raw CQL cannot be combined with query-builder fields")
}
cql = opts.CQL
if cql == "" {
cql = buildCQL(opts)
Expand Down
27 changes: 25 additions & 2 deletions tools/cfl/api/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,35 @@ func TestClient_Search_RawCQL(t *testing.T) {

client := NewClient(server.URL, "user@example.com", "token")
_, err := client.Search(context.Background(), &SearchOptions{
CQL: `type=page AND lastModified > now("-7d")`,
Text: "ignored", // Should be ignored when CQL is set
CQL: `type=page AND lastModified > now("-7d")`,
})
testutil.RequireNoError(t, err)
}

func TestClient_Search_RawCQLConflicts(t *testing.T) {
t.Parallel()
tests := []struct {
name string
opts SearchOptions
}{
{name: "text", opts: SearchOptions{Text: "query"}},
{name: "space", opts: SearchOptions{Space: "DEV"}},
{name: "type", opts: SearchOptions{Type: "page"}},
{name: "title", opts: SearchOptions{Title: "Title"}},
{name: "label", opts: SearchOptions{Label: "docs"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tt.opts.CQL = "type=page"
_, err := (&Client{}).Search(context.Background(), &tt.opts)
testutil.RequireError(t, err)
testutil.Contains(t, err.Error(), "raw CQL cannot be combined")
})
}
}

func TestClient_Search_NoQuery(t *testing.T) {
t.Parallel()
client := NewClient("http://unused", "user@example.com", "token")
Expand Down
Loading
Loading