feat: altimate integrations command for GitHub, Jira and Linear

HK
17h ago
+5408-0

Issue for this PR

No linked issue. Stacked on #1609. The Claude Code plugin side is AltimateAI/altimate-claude-plugin#12.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds altimate integrations so GitHub, Jira and Linear can be connected from the terminal, using the same credential fields as the Altimate workspace web form, instead of setting them up in the web app. Stacked on #1609.

  • Detection: finds credentials already on the machine: gh, jira-cli, GITHUB_TOKEN/GH_TOKEN, JIRA_API_TOKEN, LINEAR_API_KEY, and MCP servers in Claude Code's config. A project .mcp.json only shows that a provider is present; its values are never used, and a Jira site and token must come from the same source.
  • fields <provider>: the form fields from the workspace's integrations catalog. They are cached, with a built-in fallback, and pre-filled from the machine. Secrets are reported only as a masked tail.
  • set / set-secret --from-clipboard | --from-env | --stdin: fills fields. A secret is read by the CLI itself and never printed.
  • connect <provider> [--local-only] [--share]: validates directly with GitHub, Jira or Linear, then either keeps the credential in the OS keychain or saves it as a connection in the user's own account and attaches it to the linked workspace. A rejected credential shows the provider's message and stores nothing.
  • Sharing needs --share: attaching to a workspace that is not private (or doesn't say) lets its members use the connection, so connect, update and sync only do it with --share. Without it nothing is written to the workspace, the connection stays in the user's account, and the result names altimate integrations sync <provider> --share. The check uses the workspace as read right before writing. A connection the workspace already uses stays attached. Output says (private) only for a private workspace, otherwise who can use it, and --json carries shared: true.
  • update <provider>: replaces a rejected token on the same connection.
  • status: reports rejected when a token stopped working.
  • Other commands: open-token-page, sync, block/unblock, disconnect (removes the local copy and the workspace connection this tool made).
  • Local vs synced:
    • A synced credential lives only in the workspace connection this tool made, and a local one lives only on this machine.
    • connect --local-only (Keep local) moves a workspace copy here and stays local until unblock.
    • block refuses a synced connection.
  • A rejected credential stores nothing:
    • Drafts are discarded.
    • A connection the workspace marks invalid is removed.
    • Local secrets are deleted only after the workspace accepts the upload.
  • Concurrency:
    • Writes go through the repo's Flock lock, as temp file plus rename. The state file and the secrets file (outside macOS) are mode 0600. The catalog cache and the mock-remote file hold no secrets and use the default mode.
    • Every decision and upload runs under one lock.
    • Secrets never go on argv; macOS security -i reads them from stdin.
  • Signed out: sync is skipped and no demo workspace is shown. status reports workspace.signedIn.
  • Output: every command has --json, which the Claude Code plugin reads.
  • No browser flow: there is no OAuth flow. Credentials come from the machine or are pasted into the same fields the web form uses.

Known limitations and follow-ups

  • Linux and Windows: a kept-local secret goes to a 0600 file, not an OS keychain. Spec AC5 asks for the keychain only; this needs libsecret and DPAPI support.

  • Connection identity: a managed connection is matched by its name, Provider (from <hostname>), not a source.machine field. Renaming the host orphans it, and fixing that needs a backend field. Keychain keys are not yet namespaced per Altimate tenant. Two machines with the same hostname also share one connection.

  • Org policy "local only": not built. The backend has no policy source yet.

  • Workspace integrations list: the backend only replaces the whole list (PATCH /datamates/{id}). The CLI re-reads it just before writing, but two concurrent writers can still race until there is an incremental or conditional update.

  • Values starting with -: integrations set can't take one, because the argument parser reads it as a flag.

  • Token expiry: shown for GitHub only. Jira and Linear don't report it.

  • After a failed workspace removal: when Keep local can't remove the workspace copy, the credential is kept here and a warning says so, but status doesn't yet show both copies coherently.

  • Rollback: restoring the previous secrets after a failed keychain write is best effort. A failed restore is not reported.

  • Secret-name fallback: treating catalog fields with known secret names as secret applies only to freshly fetched catalogs, not to a cached one.

  • Update before attach: update rewrites the connection before the workspace attach is checked. If that attach then fails (a stale link or a failed read), members of a shared workspace already use the new credential, but the result only says it could not be attached.

  • Plugin: the Claude Code plugin has no --share control yet. On a shared workspace, its Link workspace and Sync all show the refusal reason.

  • Lock and slow backends: a command holds the local lock across its workspace calls (20 s timeout each), while another command waiting for the lock gives up after 30 s. Only a slow backend hits this.

  • Partial-failure edge cases: if sync can't remove a legacy local key after an upload, re-running sync doesn't retry that one key. In text mode, the cleanup warning is hidden when the workspace attach fails at the same time.

  • Env overrides: ALTIMATE_INTEGRATIONS_* backend and API variables (and JIRA_BASE_URL pairing) are honoured in every build. Tests and the local-backend setup rely on them; they should only apply under mock mode or in test builds.

  • Stale connection ids: attach replaces an entry whose connection id is missing from every connection the user can see. Telling a deleted connection from one the user can't see needs a backend rule.

  • Draft secret names: drafts are not recorded by field name, so a draft for a field the workspace catalog later drops is not found by discard or disconnect.

  • Unreadable login on disconnect: for a record that isn't synced, the workspace step is skipped, so a copy that exists only in the workspace is left in place.

  • Signed-out disconnect after a failed Keep local removal: it reports success while the workspace copy remains.

  • Draft writes: a field typed with set while a connect is being checked isn't part of the credential comparison; only the stored credential is.

  • Plugin: the pane doesn't yet show workspaceUnchecked (a failed workspace lookup in status), so counts can look synced when the workspace wasn't read.

Pre-existing issues seen during review, in code from #1609 (not changed here):

  • api/client.ts signOut: a failed withdrawClaudeSkills is swallowed, with no warning or retry.
  • cli/cmd/link.ts: picking the workspace that's already linked returns before the --claude-md write, and a failed CLAUDE.md write is swallowed by an empty catch.
  • mcp-server/adapter.ts: tables beyond MAX_FUSED_TABLES are dropped silently.
  • tool-cli/workspace-tools.ts: projectIsLinked ignores ALTIMATE_DISABLE_WORKSPACE.
  • tool-cli/claude-md.ts: there's a window between the lstat check and the CLAUDE.md write.
  • cli/cmd/tool.ts: the connections stamp uses only mtime and size.
  • tools/sql-result-format.ts: csvCell doesn't neutralise =, +, - and @ prefixes.
  • workspace/client.ts: setup reads credentials before the disabled check.
  • session/analyst-effort.ts: an unknown explicit variant skips the analyst default.

How did you verify your code works?

  • bun test test/altimate/integrations.test.ts: 153 pass, 0 fail. Every test case is listed below.
  • bun test test/altimate: 10,462 pass, 0 fail. bun run typecheck and bun turbo typecheck are clean.
  • CI is green on the latest commit.
  • Ran against a local Altimate backend with real GitHub and Jira credentials:
    • GitHub kept local, validated against api.github.com with nothing sent to Altimate
    • Jira validated and synced
    • a bad Linear key rejected with Linear's 401
    • a revoked Jira connection updated on the same connection
  • Ran the CLI in a scratch sandbox: signed-out status, a masked set-secret, and a bad GitHub token rejected with the provider's 401, with no secret left on disk.

All 153 test cases in test/altimate/integrations.test.ts (grouped by describe block):

detection parsing (2)
  • claude.json and .mcp.json servers
  • jira-cli config
status (3)
  • shape and states in demo mode
  • a signed-out user is not shown a linked workspace; mock mode keeps the demo one
  • not_found without detections
block policy and sync (1)
  • blocked providers stay local, unblocked sync
expiry (2)
  • isExpired uses a 60 second skew
  • status reports expired and expiresAt
keychain (6)
  • a secret goes to security on stdin, quoted, and never in argv
  • read and delete use the keychain by account
  • a failed keychain delete is an error unless the item was already absent
  • a disconnect the keychain refuses keeps the record, so it can be retried
  • a failed keychain write is an error
  • an account id outside the allowed characters is refused before security runs
state file (1)
  • is written for the owner only
secret storage (1)
  • a value with a line break is refused as not a token
workspace connections (48)
  • sync creates the managed connection, then updates it in place
  • a new connection the workspace marks invalid is removed again and nothing is stored
  • backend errors keep the message (code) format
  • status prefers the managed connection, else the latest validated own one, and skips shared ones
  • a newer hand-made connection stays what is shown while the deleted-elsewhere check follows the managed one
  • a synced credential deleted in the workspace is offered again when it is still found here
  • a saved record shows the newest validated connection, a newer hand-made one included
  • sync <provider> --keep-synced takes the attach-only path that sync --all takes
  • attach-only runs under the lock and names the kept workspace inert, with its id
  • sync over every provider leaves a saved connection alone and only attaches it
  • block still sets the policy when the workspace cannot be read, and says it was not checked
  • block refuses a provider whose managed connection is in the workspace
  • disconnect waits for a running upload instead of interleaving with it
  • sync only uploads a detected or registered credential, never a typed-in draft
  • sync moves a local credential to the workspace and leaves no secret here
  • a sync that lost the race to Keep local uploads nothing
  • disconnect says when the connection could not be detached from the linked workspace
  • disconnect warns when it cannot tell whether the directory is linked, and not when it is unlinked
  • a synced record marked rejected locally is connected once the workspace shows the connection valid
  • sync over every provider leaves a workspace connection alone when this machine has no record of it
  • sync over every provider does not attach a workspace connection the workspace marks invalid
  • sync over every provider attaches a workspace connection when nothing for the provider is detected here
  • the status of a synced record takes the workspace connection's expiry on the real path
  • a synced record fixed in the workspace is attached by a run over every provider
  • a synced record takes its expiry from the workspace over an older local one
  • a sync prepared before a same-variant local update uploads nothing
  • a sync prepared before the record changed uploads nothing
  • sync records the provider check: validatedAt and account on success, rejected for a refused stored credential
  • sync uploads nothing when the provider rejects the detected credential, or cannot be reached
  • a synced connection carries the known expiry, so the workspace view shows it
  • sync over every provider says a synced connection deleted in the workspace is gone, not already there
  • sync over every provider attaches a workspace connection even when the only thing found here is a remote MCP server
  • a credential found here is still offered next to a hand-made workspace connection
  • purging a provider's secrets covers every variant, not only the connected one
  • purging a provider's secrets keeps the fields just written
  • a refused new connection that cannot be removed still reports the refusal
  • a fresh local connection wins over the remote one
  • disconnect deletes only the managed connections
  • a failed workspace delete keeps the local credential so disconnect can be retried
  • upsertManaged reports when the project is not linked to a workspace
  • workspace info reports an unlinked project
  • an already saved connection with no local secret is verified and reported as not linked
  • a saved connection that is gone from the workspace is still skipped
  • only the existing connections endpoints are called
  • the CLI exits non-zero on a failed sync, reports step outcomes, and refuses to block a synced connection
  • a hand-made connection is shown as not managed
  • a local expiry is not erased by a workspace copy of unknown expiry
  • without a signed-in backend nothing is removed
failure descriptions (1)
  • validation lists, string details and codes
jira domain and cli flags (2)
  • normaliseJiraDomain sends the subdomain only
  • the browser sign-in flags are gone and the token command is removed
credentials flow › fields (6)
  • built-in table when nothing else is known
  • jira variant follows the site and the form is pre-filled
  • secrets report only a masked tail
  • jiraVariantFor
  • workspace catalog is read, cached and used when offline
  • parseCatalog reads a wrapped list and a variants map
credentials flow › set and set-secret (5)
  • a typed Jira site is reduced to the subdomain before it is stored
  • set stores a non-secret draft and rejects secrets and unknown fields
  • set-secret reads the injected clipboard and returns only the masked tail
  • clipboard content that is not a token is refused
  • secrets can come from an environment variable or stdin
credentials flow › where Jira values may come from (7)
  • a planted project file cannot point the user's token at another host
  • a token planted in a project file is not used
  • a project file still counts as present, without being syncable
  • a site from one source is not paired with a token from another
  • same-named servers in the user and project scopes are separate sources
  • a site and a token from the same source are used together
  • a site the user typed pairs with a detected token
credentials flow › validators (6)
  • github token: accepted and rejected
  • jira cloud: accepted and rejected
  • jira server: accepted and rejected
  • linear key: accepted and rejected
  • github reports the token expiry and an unreachable provider is not a rejection
  • a plain-http Jira server URL is refused
credentials flow › connect and update (46)
  • missing fields are listed
  • local-only validates, stores locally and calls no backend
  • a 2xx without an account is not accepted as a valid credential
  • a catalog id naming the server variant gets the server type and token page
  • state writes are atomic and private
  • sync reads a detected Jira credential through the connect path
  • detected credentials are used without typing
  • a rejected token stores nothing
  • a rejected token does not linger to override what is detected
  • keep local on a connection that exists only in the workspace removes that copy and holds sync
  • update with local-only keeps a synced connection local instead of uploading
  • sync is held before the workspace copy is removed, and released again if the removal fails
  • Keep local removes nothing from the workspace when the local copy cannot be stored
  • a local connect that fails part way restores the secrets it wrote
  • a catalog that omits the secret flags still treats the built-in secret fields as secret
  • a Jira variant naming the server is validated as the server
  • a refused replacement of a non-secret field leaves the working connection alone
  • keep local stores a credential unverified when the provider is unreachable; connect stores nothing
  • a rejected input is not stored unverified
  • a failed state write leaves no temp file behind
  • two processes updating the same state never lose an update
  • discard clears typed fields and pasted secrets
  • sync creates the managed connection with the catalog field names and keeps no secret locally
  • jira server sends the variant type with its catalog fields
  • a new connection the workspace marks invalid is removed and nothing is stored
  • a replacement the workspace marks invalid leaves the connection marked rejected until update fixes it
  • a backend refusal is reported as message (code)
  • signed out, or blocked, behaves as local-only and says so
  • update needs a connection and keeps a local connection local
  • update of a synced connection updates the same connection in place
  • a credential kept local is never uploaded until sync is allowed
  • signed out, sync does not pretend to reach the workspace
  • keeping a synced connection local removes the workspace copy and holds sync
  • keeping a synced connection local while signed out changes nothing
  • a synced connection cannot be blocked: there is no local copy to keep
  • a remote-only managed connection can be updated
  • Keep local on a connection only the workspace holds refuses when the workspace cannot be read
  • Keep local on a connection only the workspace holds removes it and keeps the secret here
  • Keep local on a local record finds a workspace copy uploaded after it was recorded
  • a workspace refusal keeps typed non-secret values and drops secret drafts
  • a clipboard URL is not taken as a token
  • a failed upload keeps the local secret
  • a hand-made connection cannot be updated from here
  • a workspace connection removed elsewhere stops reading as synced
  • disconnect while signed out refuses when the credential is saved in the workspace
  • disconnect removes the credential secrets
credentials flow › rejected status (3)
  • a refused replacement leaves the working connection alone
  • a stored credential the provider now refuses is rejected until it is updated
  • a workspace connection marked invalid is rejected
credentials flow (1)
  • the CLI shows the credential commands and no browser flow
catalog fields a person fills in (1)
  • skips backend bookkeeping, repeated fields and browser sign-in variants
attachToIntegrations (2)
  • adds a missing entry with read-only tools only
  • sets the connection on an existing entry and extends a connection list
attach keeps another connection (5)
  • an entry on another connection keeps it, an empty one or this one is set
  • an entry on a connection that no longer exists takes this one
  • an entry on a connection shared by another member is kept; a deleted one is taken over, with its id dropped from the list
  • attach reports when the workspace kept its own connection
  • detach leaves an entry on another connection alone
detachFromIntegrations (1)
  • clears only this connection and leaves other entries alone
printed text and catalog shape (2)
  • clean removes escape sequences, control and bidi characters
  • catalogEntries unwraps a list, items, integrations and data alike
saved login (1)
  • a missing credentials file is signed out, an unreadable one is an error

Cost side effects

None. There are no model calls and no warehouse queries. Each command makes a fixed, small number of REST calls: to GitHub, Jira or Linear to validate a credential, and to the Altimate workspace API for connections and the integrations catalog. Nothing grows with user data.

Screenshots / recordings

Not applicable. This is a CLI change; the UI is in AltimateAI/altimate-claude-plugin#12.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an integrations command for managing GitHub, Jira, and Linear connections.
    • Check connection status and detect locally available credentials, then connect, update, sync, or disconnect providers.
    • Save credentials locally or sync them to a linked workspace. Use --share to attach connections to a non-private workspace.
    • Block or unblock providers, inspect credential fields, and open provider token pages.
    • Use JSON output for supported commands and provide secret drafts via clipboard, environment variable, or stdin.