flipstream 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +357 -27
  2. package/dist/commands/auth/login.js +7 -1
  3. package/dist/commands/auth/status.js +29 -3
  4. package/dist/commands/catalog.d.ts +15 -0
  5. package/dist/commands/catalog.js +110 -0
  6. package/dist/commands/connections/list.d.ts +1 -0
  7. package/dist/commands/connections/list.js +31 -4
  8. package/dist/commands/contract.d.ts +11 -0
  9. package/dist/commands/contract.js +35 -0
  10. package/dist/commands/health.d.ts +10 -0
  11. package/dist/commands/health.js +31 -0
  12. package/dist/commands/log/add.d.ts +16 -0
  13. package/dist/commands/log/add.js +48 -0
  14. package/dist/commands/log/list.d.ts +19 -0
  15. package/dist/commands/log/list.js +43 -0
  16. package/dist/commands/query.d.ts +15 -2
  17. package/dist/commands/query.js +255 -42
  18. package/dist/commands/skills/install.d.ts +16 -0
  19. package/dist/commands/skills/install.js +55 -0
  20. package/dist/commands/workspaces/connections.js +6 -3
  21. package/dist/commands/workspaces/get.js +4 -2
  22. package/dist/commands/workspaces/list.js +3 -0
  23. package/dist/lib/api/admin-client.d.ts +5 -0
  24. package/dist/lib/api/admin-client.js +19 -0
  25. package/dist/lib/api/connections.d.ts +0 -1
  26. package/dist/lib/api/connections.js +0 -25
  27. package/dist/lib/api/errors.d.ts +1 -0
  28. package/dist/lib/api/errors.js +13 -2
  29. package/dist/lib/api/http.d.ts +2 -0
  30. package/dist/lib/api/http.js +40 -4
  31. package/dist/lib/api/hydrate.d.ts +10 -0
  32. package/dist/lib/api/hydrate.js +46 -0
  33. package/dist/lib/api/ids.d.ts +1 -0
  34. package/dist/lib/api/ids.js +5 -0
  35. package/dist/lib/api/log.d.ts +22 -0
  36. package/dist/lib/api/log.js +56 -0
  37. package/dist/lib/api/projections.d.ts +1 -0
  38. package/dist/lib/api/projections.js +23 -0
  39. package/dist/lib/api/short-uuid.d.ts +1 -0
  40. package/dist/lib/api/short-uuid.js +30 -0
  41. package/dist/lib/auth/claims.js +3 -3
  42. package/dist/lib/auth/flow.js +8 -1
  43. package/dist/lib/auth/headless.js +14 -10
  44. package/dist/lib/auth/refresh.js +21 -1
  45. package/dist/lib/command/admin.d.ts +1 -0
  46. package/dist/lib/command/admin.js +21 -0
  47. package/dist/lib/command/base.d.ts +4 -0
  48. package/dist/lib/command/base.js +97 -3
  49. package/dist/lib/command/flags.d.ts +4 -0
  50. package/dist/lib/command/flags.js +11 -0
  51. package/dist/lib/command/planner.d.ts +9 -0
  52. package/dist/lib/command/planner.js +14 -0
  53. package/dist/lib/config/constants.d.ts +3 -1
  54. package/dist/lib/config/constants.js +14 -1
  55. package/dist/lib/config/xdg.d.ts +4 -0
  56. package/dist/lib/config/xdg.js +56 -1
  57. package/dist/lib/errors.d.ts +20 -1
  58. package/dist/lib/errors.js +132 -13
  59. package/dist/lib/output/dialogs.d.ts +27 -0
  60. package/dist/lib/output/dialogs.js +94 -0
  61. package/dist/lib/output/interactivity.d.ts +11 -0
  62. package/dist/lib/output/interactivity.js +48 -0
  63. package/dist/lib/output/redact.d.ts +1 -0
  64. package/dist/lib/output/redact.js +12 -0
  65. package/dist/lib/output/runlog.d.ts +3 -0
  66. package/dist/lib/output/runlog.js +72 -0
  67. package/dist/lib/output/sanitize.d.ts +2 -0
  68. package/dist/lib/output/sanitize.js +57 -0
  69. package/dist/lib/output/sidecar.d.ts +30 -0
  70. package/dist/lib/output/sidecar.js +58 -0
  71. package/dist/lib/output/table.js +5 -1
  72. package/dist/lib/output/trace.d.ts +11 -0
  73. package/dist/lib/output/trace.js +89 -0
  74. package/dist/lib/planner/catalog.d.ts +26 -0
  75. package/dist/lib/planner/catalog.js +60 -0
  76. package/dist/lib/planner/client.d.ts +14 -0
  77. package/dist/lib/planner/client.js +47 -0
  78. package/dist/lib/planner/connection.d.ts +14 -0
  79. package/dist/lib/planner/connection.js +139 -0
  80. package/dist/lib/planner/diagnose.d.ts +8 -0
  81. package/dist/lib/planner/diagnose.js +50 -0
  82. package/dist/lib/planner/errors.d.ts +14 -0
  83. package/dist/lib/planner/errors.js +129 -0
  84. package/dist/lib/planner/filters.d.ts +8 -0
  85. package/dist/lib/planner/filters.js +74 -0
  86. package/dist/lib/planner/request.d.ts +24 -0
  87. package/dist/lib/planner/request.js +51 -0
  88. package/dist/lib/planner/suggest.d.ts +2 -0
  89. package/dist/lib/planner/suggest.js +45 -0
  90. package/dist/lib/planner/vocabulary.d.ts +9 -0
  91. package/dist/lib/planner/vocabulary.js +95 -0
  92. package/dist/lib/skills/install.d.ts +24 -0
  93. package/dist/lib/skills/install.js +69 -0
  94. package/dist/lib/store/keyring.d.ts +3 -0
  95. package/dist/lib/store/keyring.js +45 -2
  96. package/dist/lib/store/memory-store.d.ts +1 -0
  97. package/dist/lib/store/memory-store.js +5 -0
  98. package/docs/AGENT-CONTRACT.md +238 -0
  99. package/oclif.manifest.json +606 -8
  100. package/package.json +22 -3
  101. package/skill/SKILL.md +55 -0
  102. package/dist/lib/auth/register.d.ts +0 -4
  103. package/dist/lib/auth/register.js +0 -43
@@ -7,7 +7,37 @@ import { freshAccessToken, keyOf, parseCredentials } from './credentials.js';
7
7
  export const KEYRING_SERVICE = 'io.flipstream.cli';
8
8
  // TokenStore backed by the OS keychain. The whole Credentials JSON is the single
9
9
  // secret per host (keyed by host).
10
+ //
11
+ // Reads are CACHED for the life of the process. Without this, one command touches
12
+ // the keychain several times — the auth gate reads it, then every authed request
13
+ // re-reads it through withFreshToken, and a paginated call re-reads it per page.
14
+ // On macOS each read is a separate access, so a user who granted "Allow" rather
15
+ // than "Always Allow" gets prompted once per read: two dialogs for `catalog`,
16
+ // three for `query`, more for anything that paginates or resolves a name.
17
+ //
18
+ // This costs nothing in EXPOSURE: the credentials are already in memory the moment
19
+ // they are read — they have to be, to go in an Authorization header — so holding
20
+ // them for the rest of a short-lived CLI process reveals nothing new. Nothing is
21
+ // written to disk, and the cache dies with the process.
22
+ //
23
+ // It does cost COHERENCE, and that is worth stating plainly rather than glossing.
24
+ // Another CLI process can log out or rotate tokens underneath us, and this cache
25
+ // will not notice. Two consequences, handled differently:
26
+ //
27
+ // Deleting someone else's session — handled. A failed refresh compare-and-deletes
28
+ // (see auth/refresh.ts): it re-reads past this cache and only clears when the
29
+ // stored token is still the one it tried. That race predates the cache; caching
30
+ // only widened the window.
31
+ //
32
+ // Using a token after a concurrent `auth logout` — accepted. Logout REVOKES
33
+ // upstream, so a stale cached token gets a 401 from the server rather than data,
34
+ // surfacing as data_auth_failed. Re-reading before every request would fix it and
35
+ // would also undo the entire point of caching, for a case that already fails
36
+ // safely.
10
37
  export class KeyringStore {
38
+ // `null` is cached too: "this host has no credentials" is an answer worth
39
+ // remembering, or a logged-out run re-reads on every check.
40
+ #cache = new Map();
11
41
  accessTokenIfFresh(host) {
12
42
  return freshAccessToken(this.load(host));
13
43
  }
@@ -23,6 +53,7 @@ export class KeyringStore {
23
53
  }
24
54
  }
25
55
  clear(host) {
56
+ this.#cache.set(host, null);
26
57
  try {
27
58
  new Entry(KEYRING_SERVICE, keyOf(host)).deletePassword();
28
59
  }
@@ -31,15 +62,27 @@ export class KeyringStore {
31
62
  }
32
63
  }
33
64
  load(host) {
65
+ const cached = this.#cache.get(host);
66
+ if (cached !== undefined)
67
+ return cached;
68
+ return this.reload(host);
69
+ }
70
+ reload(host) {
71
+ let creds = null;
34
72
  try {
35
73
  const secret = new Entry(KEYRING_SERVICE, keyOf(host)).getPassword();
36
- return secret ? parseCredentials(secret) : null;
74
+ creds = secret ? parseCredentials(secret) : null;
37
75
  }
38
76
  catch {
39
- return null;
77
+ creds = null;
40
78
  }
79
+ this.#cache.set(host, creds);
80
+ return creds;
41
81
  }
42
82
  save(host, creds) {
43
83
  new Entry(KEYRING_SERVICE, keyOf(host)).setPassword(JSON.stringify(creds));
84
+ // Keep the cache authoritative: a refresh saves rotated tokens mid-command,
85
+ // and a later read must see them rather than the ones it started with.
86
+ this.#cache.set(host, creds);
44
87
  }
45
88
  }
@@ -6,5 +6,6 @@ export declare class MemoryStore implements TokenStore {
6
6
  available(): boolean;
7
7
  clear(host: string): void;
8
8
  load(host: string): Credentials | null;
9
+ reload(host: string): Credentials | null;
9
10
  save(host: string, creds: Credentials): void;
10
11
  }
@@ -15,6 +15,11 @@ export class MemoryStore {
15
15
  load(host) {
16
16
  return this.entries.get(keyOf(host)) ?? null;
17
17
  }
18
+ // No cache here, so a forced re-read is the same read. Present so the interface
19
+ // is honest and tests exercise the same call sites as the real store.
20
+ reload(host) {
21
+ return this.load(host);
22
+ }
18
23
  save(host, creds) {
19
24
  this.entries.set(keyOf(host), creds);
20
25
  }
@@ -0,0 +1,238 @@
1
+ # Flipstream CLI — Agent Contract (v3)
2
+
3
+ The machine-facing promises the CLI makes so an AI coding agent (Claude Code,
4
+ Codex, an MCP server) or a script can drive it deterministically. These are
5
+ **frozen**: a conformance suite (`test/contract/agent-contract.test.ts`)
6
+ auto-discovers every command and fails the build if any of these rules break.
7
+
8
+ `CONTRACT_VERSION` is surfaced by `flipstream version` (`contractVersion`) and
9
+ defined in `src/lib/config/constants.ts`. **Current: `3`.** A breaking change to
10
+ anything below bumps it.
11
+
12
+ > **v2 → v3 (E11-1).** Two changes are breaking by this document's own rules,
13
+ > hence the bump. (1) **HTTP 429 moved** from `data_request_failed`/exit 1 to
14
+ > `rate_limited`/exit 7 — the transient classification is correct, and v2
15
+ > consumers branching on exit 1 must follow. (2) **`usage` narrowed**: missing
16
+ > query inputs and malformed bodies now carry specific codes
17
+ > (`missing_source`, `missing_connection_id`, `missing_source_and_connection_id`,
18
+ > `missing_request`, `invalid_body_json`); `usage` still covers bad flags,
19
+ > `--json`+`--ndjson`, and planner 422s. The envelope's new optional fields
20
+ > (below) are additive and would NOT alone have bumped the version.
21
+
22
+ > **v1 → v2 (E8).** `query` now targets the **query planner** and builds a
23
+ > `LogicalRequest` from flags. Previously it forwarded an opaque body to
24
+ > pulse-data. `--body`/`--body-file`/stdin still forward verbatim, so the escape
25
+ > hatch is unchanged; the default target and default input mode are not. Adds
26
+ > `catalog` and `health`.
27
+
28
+ ## Discovery — how an agent learns what it can do
29
+
30
+ This is the capability map. **Enumerate it first**, then call commands.
31
+
32
+ - **`flipstream commands --json`** → the full machine-readable catalog: every
33
+ command's `id`, `summary`, `description`, `examples`, `flags`, `args`, and
34
+ `aliases`. One call returns everything the CLI can do.
35
+ - **`flipstream <command> --help`** → human help for a single command.
36
+ - **`flipstream --help`** → topic overview (`auth`, `workspaces`/`ws`,
37
+ `connections`/`conn`, plus top-level `query`, `catalog`, `health`, `version`,
38
+ `commands`).
39
+ - **`flipstream contract`** → this document, verbatim, from the installed
40
+ package — context self-service, offline, no auth.
41
+ - **`flipstream skills install`** → installs the flipstream skill for detected
42
+ AI coding agents (local detection only; never in CI; never phones home).
43
+ - **`flipstream catalog [source]`** → the *data* vocabulary (which is not the
44
+ same thing as the command surface): the exact names `-d`, `-m` and filter keys
45
+ accept. Read it before constructing a query; a name's `name` is what you send,
46
+ its `label` is only for humans.
47
+
48
+ Every command obeys the rules below, and the conformance suite
49
+ (`test/contract/agent-contract.test.ts`) auto-discovers commands and fails the
50
+ build if any one drifts — so this contract stays true as commands are added.
51
+
52
+ ## Core rules
53
+
54
+ 1. **stdout = data, stderr = diagnostics.** Under `--json`/`--ndjson`, stdout
55
+ carries **only** machine output (a single JSON document, or NDJSON lines).
56
+ All human text — prompts, progress (`Opening your browser…`), `--verbose`
57
+ diagnostics — goes to **stderr**. So `flipstream <cmd> --json 2>/dev/null`
58
+ yields clean JSON.
59
+ 2. **`--json`** → exactly one JSON document on stdout. **`--ndjson`** → one
60
+ compact JSON object per line (streaming), no enclosing array, no blank lines.
61
+ `--json` and `--ndjson` together is a usage error (exit 2).
62
+ 3. **Failures are JSON too.** Under `--json`/`--ndjson`, a failure prints the
63
+ **error envelope to stdout** (not stderr):
64
+
65
+ ```json
66
+ { "error": { "code": "not_logged_in", "message": "Run `flipstream auth login`.", "exit": 4 } }
67
+ ```
68
+
69
+ This is what makes `flipstream <cmd> --json 2>/dev/null | jq .` work on
70
+ **both** success and failure. Branch on `error.code` (stable) and/or the
71
+ process exit code — never on the message wording (which may be reworded).
72
+
73
+ **Envelope remediation fields (E11-1, additive — absent unless set, never
74
+ `null`). "Envelope v3" refers to this error-envelope shape; it ships in
75
+ CONTRACT_VERSION 3:**
76
+
77
+ ```json
78
+ { "error": {
79
+ "code": "missing_connection_id", "message": "Missing --connection-id.", "exit": 2,
80
+ "hint": "List your connections and pass one of their ids.",
81
+ "next": ["flipstream connections list --json"],
82
+ "docs": "docs/AGENT-CONTRACT.md",
83
+ "retryable": false,
84
+ "retry_after_ms": 30000,
85
+ "upstream_code": "ENGINE_FAILED"
86
+ } }
87
+ ```
88
+
89
+ - `hint` — one imperative sentence: what to do next.
90
+ - `next` — ready-to-run command lines, most useful first. **Prefer executing
91
+ these over guessing** — they name the discovery command that produces
92
+ whatever was missing. Execute them **argv-split, never via `sh -c`**: they
93
+ are CLI-authored, redacted, and any interpolated value is validated against
94
+ a conservative charset, but they are not shell-quoted.
95
+ - `docs` — a URL or repo path for the failure class.
96
+ - `retryable` — `true`: re-running the SAME command may succeed (429/5xx/
97
+ transport). `false`: it deterministically fails again — do **not** retry
98
+ unchanged. Absent: unknown; use the exit code (7/8 lean retryable).
99
+ - `retry_after_ms` — server-stated wait (from `Retry-After`), only ever
100
+ alongside `retryable: true`, and clamped to at most 3 600 000 (one hour) —
101
+ a hostile header can never park an agent indefinitely.
102
+ - `upstream_code` — the backing service's own error code verbatim (e.g. the
103
+ planner's `ENGINE_FAILED` vs `UNKNOWN_DIMENSION`), for branching on the
104
+ upstream taxonomy when our `code` alone is too coarse.
105
+
106
+ Decision procedure: `retryable && retry_after_ms` → wait, retry. `retryable
107
+ === false` → change something (follow `next`) before any retry. Otherwise →
108
+ branch on `code`/exit as before. Human mode prints the same facts as
109
+ indented `hint:`/`next:`/`docs:`/`retry:` lines on stderr.
110
+ 4. **No secrets, ever.** No token / `Authorization` header / `code` / verifier
111
+ appears on stdout, stderr, argv, or in any error — including a malicious DATA
112
+ response that echoes a token-shaped key (it is redacted). No command exposes a
113
+ `--token`/`--secret`/`--verifier`/`--bearer` flag.
114
+ 5. **No telemetry / no phone-home.** The only network calls are to the OAuth
115
+ provider and the data host you query.
116
+
117
+ ## Exit codes (`src/lib/exit-codes.ts`)
118
+
119
+ | code | name | meaning | agent guidance |
120
+ |---|---|---|---|
121
+ | 0 | success | data emitted | use the output |
122
+ | 1 | generic | unclassified failure (incl. non-auth data 4xx) | inspect `error.message` |
123
+ | 2 | usage | bad flags / unknown command / malformed input | fix the invocation |
124
+ | 4 | auth-required | not logged in, or session expired | run `flipstream auth login` |
125
+ | 5 | auth-failed | auth attempt rejected (state/iss/token/role/target) | re-auth / check permissions |
126
+ | 7 | network | transport/discovery/upstream failure | retry later |
127
+ | 8 | timeout | operation timed out | retry |
128
+
129
+ ## Observability sidecar & logs (E11-6, opt-in, local-only)
130
+
131
+ None of this is ever sent anywhere — it is local files a supervising process
132
+ opts into. It composes with `--json` rather than competing with it: stdout is
133
+ untouched.
134
+
135
+ - **`FLIPSTREAM_OUTPUT_FILE=<path>`** (or **`FLIPSTREAM_OUTPUT_FILE_DIRECTORY=<dir>`**
136
+ for a timestamped file) → one JSON object per line, each with its own
137
+ `version` so entry schemas evolve independently:
138
+ - `{type:"session", version:1, cli_version, contract_version, argv, log_file_path, timestamp}` (before the command runs; `argv` is redacted and capped)
139
+ - `{type:"result", version:1, command, duration_ms, exit:0, timestamp}` (clean exit)
140
+ - `{type:"command-failed", version:1, command, error_code, exit, message, log_file_path, retry_after_ms?, timestamp}` (failure)
141
+ Files are created `0600`. A write failure disables the sidecar for the run
142
+ and prints one stderr notice.
143
+ - **`FLIPSTREAM_LOG=debug`** → request/response wire trace on stderr (headers
144
+ never traced; bodies omitted unless the escape hatch below is set).
145
+ - **`FLIPSTREAM_LOG_SANITIZE=false`** → include redacted bodies in the trace.
146
+ This ALSO writes them to the run log even without `--verbose`, so it prints a
147
+ one-time stderr notice; the redaction floor (token masking) still applies.
148
+ - Per-run logs live under `$XDG_STATE_HOME/flipstream-cli/logs/` (`0600`, newest
149
+ 20 kept); a failing command prints `🪵 Logs were written to <path>` on stderr
150
+ in human mode.
151
+
152
+ ## Per-command `--json` shapes
153
+
154
+ | command | success shape (stdout) |
155
+ |---|---|
156
+ | `version` | `{version, node, platform, contractVersion}` |
157
+ | `auth login` | redacted decoded token claims (object) or `null` |
158
+ | `auth status` | `{host, loggedIn}` — when logged in adds `{account, expired, expiresAt, scopes}` |
159
+ | `auth logout` | `{host, loggedOut, revoked}` |
160
+ | `auth clear-all` | `{clearedHosts, configRemoved}` |
161
+ | `query` | the planner/engine response, verbatim (redacted); the CLI adds no fields. Under `--dry-run`, the `LogicalRequest` body instead — and nothing is sent |
162
+ | `catalog` | the catalog document as served: `{sources:[…], defaultSource}`, or one source `{name, label, description, dimensions:[…], metrics:[…]}` |
163
+ | `health` | the planner's health document (e.g. `{status:"ok"}`); needs no auth |
164
+ | `contract` | `{contractVersion, markdown}` — this document; needs no auth |
165
+ | `skills install` | `{installed:[{agent, path, status, backup?}]}` — `status` is `installed`/`updated`/`unchanged`; `backup` set when a differing local copy was preserved |
166
+ | `ws list` · `ws connections` · `conn list` · `log list` | a list envelope `{count, records}`; each record is a projected object with a redacted `raw` passthrough (`--ndjson` streams the records) |
167
+ | `log add` | the created log entry (projected object with a redacted `raw`) |
168
+
169
+ ### The `query` request shape (`LogicalRequest`)
170
+
171
+ You name a **source**, then name dimensions, metrics and filters using the names
172
+ `catalog <source>` prints. Which physical table answers that is the planner's
173
+ problem, not yours — and it never tells you, by design.
174
+
175
+ | field | flag | notes |
176
+ |---|---|---|
177
+ | `source` | `--source` | dataset discriminator, e.g. `gsc`. A source name, never a table |
178
+ | `connection_id` | `--connection-id` | a UUID, or a **name** the CLI resolves (ambiguity is refused, exit 2) |
179
+ | `dimensions` | `-d` (repeatable) | the group-by |
180
+ | `metrics` | `-m` (repeatable) | |
181
+ | `filters` | `-f` (repeatable) | `col=a,b` · `col=from..to` · `col=<json>`. **`col=` is refused** |
182
+ | `sort` | `-s` (repeatable) | `FIELD[:asc\|desc]`, default `asc` |
183
+ | `offset` / `rows` | `--offset` / `--rows` | default `0` / `100` |
184
+ | `table` | `--table` | honoured only for an unmodelled source; ignored otherwise |
185
+
186
+ **The filter gotcha, because it fails silently.** The routing predicate reads a
187
+ selection by **length**: `[]` is length 0 (inactive → the summary table) and
188
+ `[""]` is length 1 (active → the dimension's table). They mean different things,
189
+ they are one comma apart, and neither warns — you just get different numbers. So
190
+ `-f 'device='` is **refused** rather than guessed; write `device=[]` or
191
+ `device=[""]` explicitly.
192
+
193
+ The response is forwarded verbatim and is governed by the planner/engine, not
194
+ this CLI. `--body`/`--body-file`/stdin still send a body verbatim for any shape
195
+ the flags cannot express.
196
+
197
+ ## Error taxonomy
198
+
199
+ `error.code` is stable; `exit` is the table above. Messages are illustrative.
200
+
201
+ | `error.code` | exit | when | agent guidance |
202
+ |---|---|---|---|
203
+ | `usage` | 2 | bad flag / `--json`+`--ndjson` / planner 422 | fix the invocation |
204
+ | `missing_source` | 2 | `query` flags path without `--source` | run `flipstream catalog`, pass a source |
205
+ | `missing_connection_id` | 2 | `query` flags path without `--connection-id` | run `flipstream connections list`, pass an id |
206
+ | `missing_source_and_connection_id` | 2 | `query` flags path missing both | follow `next` |
207
+ | `missing_request` | 2 | bare `query` with empty piped stdin and no flags | build a request (see `next`) |
208
+ | `invalid_body_json` | 2 | `--body`/`--body-file`/stdin body is not valid JSON | fix the body; message names its origin |
209
+ | `rate_limited` | 7 | HTTP 429 from planner/admin | wait `retry_after_ms`, retry |
210
+ | `non_interactive` | 2 | a prompt had no answerable context and no fallback | pass the named flag, or run interactively |
211
+ | `prompt_aborted` | 2 | an interactive prompt was aborted (EOF / Ctrl-C) | re-run, or pass the named flag |
212
+ | `contract_unavailable` | 1 | the packaged `AGENT-CONTRACT.md` is missing from this installation | reinstall the CLI |
213
+ | `not_logged_in` | 4 | no stored session for the host | `auth login` |
214
+ | `session_expired` | 4 | session existed but refresh was rejected | `auth login` (distinct from above) |
215
+ | `auth_failed` | 5 | generic auth attempt failure | re-auth |
216
+ | `state_mismatch` | 5 | loopback `state`/`iss` validation failed (possible CSRF) | retry login |
217
+ | `token_exchange_failed` | 5 | `/oauth/token` rejected the exchange (e.g. `invalid_grant`) | retry login |
218
+ | `data_auth_failed` | 5 | DATA/admin host returned 401 after refresh | re-auth |
219
+ | `role_forbidden` | 5 | 403 — your role lacks permission for this action | check your role/permissions |
220
+ | `invalid_target` | 5 | token audience rejected by the DATA host (defensive; not expected) | re-bind / contact support |
221
+ | `discovery_failed` | 7 | OAuth discovery unreachable / invalid | retry later |
222
+ | `network_error` | 7 | transport failure | retry later |
223
+ | `data_upstream_error` | 7 | DATA host 5xx | retry later |
224
+ | `loopback_timeout` / `timeout` | 8 | browser/loopback or request timed out | retry |
225
+ | `not_found` | 1 | 404 — resource not found or not in your organization | verify the id / your org |
226
+ | `data_request_failed` | 1 | other DATA/admin 4xx except 429 (surfaces the host's `detail`) | inspect `error.message` |
227
+
228
+ Provider OAuth errors (`invalid_grant`, `access_denied` from clicking **Deny**,
229
+ `invalid_client`, `unsupported_grant_type`) surface as `token_exchange_failed` /
230
+ `auth_failed` (exit 5) with the provider's `<error>: <error_description>` message.
231
+
232
+ ## Consuming the CLI (the canonical pattern)
233
+
234
+ ```bash
235
+ out=$(flipstream auth status --json 2>/dev/null); code=$?
236
+ echo "$out" | jq . # always valid JSON — data on 0, {error:…} otherwise
237
+ [ "$code" -eq 4 ] && flipstream auth login
238
+ ```