This file orients coding agents (Claude Code and others) working in this repository. It is intentionally short — the real guidance lives elsewhere.
-
Read
CONTRIBUTING.mdfirst. It covers the project structure, the build/test/lint commands, coding and testing conventions, and the commit/PR expectations every change must follow. -
Then read, only as the task needs them, the documents under
docs/:docs/technical-design.md— architecture, the package layout, the API-client/flavor abstraction, the config and error models, and the rendering pipeline. Read before changing core behavior.docs/installation.md— install methods, shell completion, and the companion Skill. Read for distribution/UX changes.docs/releasing.md— versioning, the changelog step, tagging, and the release/CI workflows. Read before cutting a release or touching.github/workflows/.
Pull in a document when the task touches its area; do not read all of docs/
up front.
- Run
make testandmake e2ebefore claiming a change is complete. - Keep commits scoped to one logical change; follow the commit and PR
conventions in
CONTRIBUTING.md. - Never commit
.env, credentials, tokens, or build artifacts. - Output rendering, argv correction, the update check, the credential store,
credential resolution, the configuration engine, and the code behind
pkg/errorsandpkg/transportcome from agent-cli-kit, shared by every CLI in the family. Fix bugs in those there, not here.internal/authdeclares confluence-cli's auth schemes and recovery wording and maps config onto the shared resolver.internal/configregisters confluence-cli's settings, file locations, command wording and flavor rules with the shared config engine (schema.go) and keeps the YAML shape ofconfig.yaml, including the legacy flat layout.pkg/errorsre-exports the sharedCLIErrormodel and exit-code map (0–11) and installs confluence-cli's hints (hints.go);pkg/transportre-exports the shared retrying HTTP client with confluence-cli's User-Agent.
This CLI targets two backends (Confluence Cloud and Data Center / Server). Writes go through the v1 REST API on both, but the flavors still diverge on endpoints, pagination, and field shapes — and the recurring bug class is a branch (often Cloud) honouring an input the other branch silently drops. A real example: Cloud user search ignored the page cursor and capped at the first page while the DC branch paged correctly.
When you touch any path that branches on c.flavor:
- Handle both branches or consciously diverge — every input the command accepts (filters, cursors, version, expand) must be consumed by both flavor branches, or the gap must be deliberate and documented. Compiling proves nothing; a branch that never reads a field is silently wrong.
- Verify pagination + optimistic-lock symmetry. If one branch returns a
nextcursor or sends aversion, the other almost certainly must too. - Add a test that exercises both flavors for the changed path so a one-sided change is caught.
Every non-trivial identifier a command accepts as input must be discoverable through another command in this CLI. Examples of inputs covered by this rule: space keys, page IDs, comment IDs, attachment IDs, label names, version numbers, user identifiers (accountId on Cloud, username on Data Center), and file paths within an attachment.
The rule is symmetric: an input is only discoverable if (a) the CLI has a
command that lists / searches values of that kind, and (b) that listing
command itself is reachable without already knowing some other value the CLI
can't tell you about. A comment list that demands a page ID is only useful
if search (or another path-to-page command) also exists.
When you add a command or flag that takes a new kind of input:
- Walk every parameter the new surface accepts. For each, answer: "Where does the caller (especially an AI agent) get this value?"
- If the answer is another command in this CLI, you are done.
- If the answer is an existing identifier the user already had in hand
(e.g. they pasted a Confluence page URL), that counts too —
pkg/urlrefalready parses page / comment IDs out of URLs. - If the answer is out-of-band (a web UI, another tool, the API
directly), that is a gap. Add the missing discovery command in the same
PR, or surface it as a follow-up issue and document the dead end in
CHANGELOG.mdunder Known gaps.
The same rule applies to error messages. When a command rejects an
invocation for missing a required identifier, the resulting CLIError's
next_steps must include the discovery command (e.g. space list,
search, comment list, attachment list). Errors that say
"Pass --space <key>" without showing the user how to find a valid <key>
are defects.
Recovery steps must name commands that exist in this CLI. The context listing
is config get-contexts; UNKNOWN_CONTEXT names it through the wording
registered in internal/config/schema.go. Resolve an advertised command
against the command tree in a test.
The e2e harness should exercise this contract: when adding a "missing
input" error path, also add a scripts/e2e.sh assertion that the error
output contains the discovery command's name (use a stderr-capturing helper
when needed, since structured errors are written to stderr).
Two orthogonal protections guard every operation that mutates remote state:
--dry-runis a per-command flag on every mutating command. It must resolve the request viaClient.DescribeWrite(ctx, op)and emit the resultingWriteRequestPlan{Method, URL, Payload}instead of sending the HTTP request. Use the sharedemitDryRun(s, client, ctx, op)helper — never re-implement the dispatch inline. Every write request type that reaches a command must also have aDescribeWritecase; the read path (build helper) must be the same code path the live write uses so the preview cannot drift from the actual request.- Read-only posture is a session-level switch:
defaults.read_onlyin the config file, orCONFLUENCE_CLI_READ_ONLYin the environment. When active,appState.newClient()wraps the client inapiclient.NewReadOnly(...), which returns a structuredREADONLY_BLOCKED(category=permission) error from every mutating method before any HTTP request is sent.--allow-writes(root persistent flag) overrides the posture for one invocation.
Client is the union of Reader and Writer (pkg/apiclient/client.go).
Declare every new method in exactly one of them: anything that can change
remote state goes in Writer, never in Reader, because the read-only
wrapper delegates Reader unchanged. When you add a method to Writer:
- Add its blocking override on
readOnlyClientinpkg/apiclient/readonly.go; the build fails until you do. - Add a
DescribeWritecase + a--dry-runbranch on the calling command. - Add an e2e assertion for both the
--dry-runhappy path and theREADONLY_BLOCKEDrejection inside the read-only block ofscripts/e2e.sh, where the mock server's write log must stay empty. - Add a row to the wrapper's write table in
pkg/apiclient/readonly_test.go; the test fails until you do.
A method added to Reader needs a canned answer on recordingReader and a
row in the delegation test in the same file, which also fails until it has
one.
--dry-run must not be blocked by read-only mode. DescribeWrite may perform
reads needed to build the request, but never sends the mutation. It is a
Reader method, so the wrapper passes it through. Keep preview/live payload parity tests for
both flavors, including AddCommentReq and threaded storage/wiki bodies.
- Page body edits must use the current raw storage body and its version; preserve
unrelated human content and rich markup. Conflict recovery must merge the
intended edit into newly fetched content before retrying with
--version. Metadata-only reads followed by a version bump are not safe recovery. - A transient or decoding error does not prove a write failed. Skill and runtime guidance must verify remote state before replaying an uncertain mutation.
- Keep the Skill's human-comment approval gate and reason-once explanation. Technical previews do not introduce another approval gate for an authorized change; attribution describes the actual AI contribution.
-
Keep list pagination in agent-cli-kit's
output.EmitList. NDJSON emits rows on stdout and a structured_notice.paginationon stderr only after successful row output when more results remain. Preserve cursors through field projection and empty filtered pages; completed--allresults emit no pagination notice.Options.NoticeWriterdefaults to stderr andNextFlagdefaults to--cursor, shared with the table footer. Test cursor replay through the real CLI for both Cloud and Data Center when changing this path. -
Actively maintain the docs. When a change affects architecture, installation, commands, flags, or the release process, update the relevant file under
docs/in the same commit. Stale docs are a defect. -
This includes the GitHub Pages site.
docs/index.htmlis the published landing page (served at https://angelmsger.github.io/confluence-cli/) and.github/workflows/pages.ymlredeploys it on every push tomainthat touchesdocs/. When commands, the feature list, or install instructions change, updatedocs/index.htmlto match — do not let the landing page drift from the README and the CLI. -
Keep the companion Skill in sync — it is the agent-facing source of truth. The embedded Skill under
skills/confluence/(SKILL.mdreferences/) is what coding agents read instead of--help, so a capability the Skill omits effectively does not exist for them. Any new command, subcommand, flag, or alias — and any change to flavor-specific behavior (Cloud vs Data Center / Server) — must be reflected in the Skill's## Commandslist and the relevantreferences/file in the same commit. In particular: a flag whose help text points at another command (e.g.--authorsaying "find IDs withuser search") must have that command listed in the Skill; and never leave a Skill claim that contradicts the code.
-
Actively maintain
CHANGELOG.md. Whenever a change is user-facing (a flag, command, output, behavior, or bug fix), add an entry to the[Unreleased]section in the same commit — do not leave it for later. -
If you bump the version, you must tag the commit. "Bumping the version" means renaming
[Unreleased]inCHANGELOG.mdto the new version with today's date and updatingbuild/npm/package.json. The CLI's own version is derived from the git tag via-ldflags, so a version bump is not real until the commit carrying it is tagged:git tag vX.Y.Z <commit> git push origin vX.Y.Z
See
docs/releasing.mdfor the full release procedure.
Keep service presets separate from personal credentials. config set-context
must resolve the named destination (including a new one), ignore personal
variables before scheme inference, remain offline and credential-store-free,
and share one merge for dry-run and execution. Preserve other contexts, personal
usernames and shared defaults. Login must persist its identity as well as its
secret and reject a different complete service URL before credential writes.
Use the common acquisition guide in all prompt styles and missing-credential
recovery; never request a guide URL with credentials. Cover a fresh config reload,
conflict/idempotent setup, and partial persistence failures when changing this
flow. The canonical behavior is in the installation guide's team setup section.
--since excludes --from and --to, and --to requires --from; the
window is the half-open [from, to). Resolve it with resolveHistoryWindow
before any credential or network access and report a rejected one through
badHistoryWindow: BAD_TIME_RANGE, usage category, the violated rule as the
message. Reuse both for any new event or history filter rather than adding a
second parser. search --after/--before are server-side lastmodified
filters, not an event window, and stay outside this contract.
Keep auth reuse separate from public service setup. The decision belongs to
agent-cli-kit's reuse package; this repository only describes its contexts,
resolves, verifies and writes. Match complete URLs and the effective deployment
flavor before credential access, keep ValidateService as the scheme check (Cloud
never adopts pat, not even when selected explicitly), never replace a working
login automatically, and retain operational failures. A username is an identity
only under basic. Verification reports the canonical account from the same
user/current call (accountId on Cloud, userKey or the username on Data
Center); credentials of one account are one login. Never copy secrets, infer
identity from environment variables or activate a context. Cover dry-run,
ambiguity, scope mismatch, concurrent edits, fresh-load credential resolution and
both flavors. Native self-configuration follows the existing read-only exception.
The keychain account is <host>:<scheme>, so contexts on one host with one
scheme share a secret: a team preset and the personal context whose login it
reuses, or two spellings of one URL. Forget a stored secret only through
forgetUnusedCredential, after the config write has succeeded, and only when no
remaining context resolves the same account. Resolve a missing scheme as a
request does (basic on Cloud, pat elsewhere) instead of assuming one. auth logout is the explicit exception. Cover a URL edit that keeps the host, a
shared and an unshared deletion, a scheme change, a context without a scheme
and a failed write; see internal/app/config_credentials_test.go. internal/app
tests run against the in-memory keyring that TestMain installs; never add a
test that can reach the OS keychain.