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.
- package/README.md +357 -27
- package/dist/commands/auth/login.js +7 -1
- package/dist/commands/auth/status.js +29 -3
- package/dist/commands/catalog.d.ts +15 -0
- package/dist/commands/catalog.js +110 -0
- package/dist/commands/connections/list.d.ts +1 -0
- package/dist/commands/connections/list.js +31 -4
- package/dist/commands/contract.d.ts +11 -0
- package/dist/commands/contract.js +35 -0
- package/dist/commands/health.d.ts +10 -0
- package/dist/commands/health.js +31 -0
- package/dist/commands/log/add.d.ts +16 -0
- package/dist/commands/log/add.js +48 -0
- package/dist/commands/log/list.d.ts +19 -0
- package/dist/commands/log/list.js +43 -0
- package/dist/commands/query.d.ts +15 -2
- package/dist/commands/query.js +255 -42
- package/dist/commands/skills/install.d.ts +16 -0
- package/dist/commands/skills/install.js +55 -0
- package/dist/commands/workspaces/connections.js +6 -3
- package/dist/commands/workspaces/get.js +4 -2
- package/dist/commands/workspaces/list.js +3 -0
- package/dist/lib/api/admin-client.d.ts +5 -0
- package/dist/lib/api/admin-client.js +19 -0
- package/dist/lib/api/connections.d.ts +0 -1
- package/dist/lib/api/connections.js +0 -25
- package/dist/lib/api/errors.d.ts +1 -0
- package/dist/lib/api/errors.js +13 -2
- package/dist/lib/api/http.d.ts +2 -0
- package/dist/lib/api/http.js +40 -4
- package/dist/lib/api/hydrate.d.ts +10 -0
- package/dist/lib/api/hydrate.js +46 -0
- package/dist/lib/api/ids.d.ts +1 -0
- package/dist/lib/api/ids.js +5 -0
- package/dist/lib/api/log.d.ts +22 -0
- package/dist/lib/api/log.js +56 -0
- package/dist/lib/api/projections.d.ts +1 -0
- package/dist/lib/api/projections.js +23 -0
- package/dist/lib/api/short-uuid.d.ts +1 -0
- package/dist/lib/api/short-uuid.js +30 -0
- package/dist/lib/auth/claims.js +3 -3
- package/dist/lib/auth/flow.js +8 -1
- package/dist/lib/auth/headless.js +14 -10
- package/dist/lib/auth/refresh.js +21 -1
- package/dist/lib/command/admin.d.ts +1 -0
- package/dist/lib/command/admin.js +21 -0
- package/dist/lib/command/base.d.ts +4 -0
- package/dist/lib/command/base.js +97 -3
- package/dist/lib/command/flags.d.ts +4 -0
- package/dist/lib/command/flags.js +11 -0
- package/dist/lib/command/planner.d.ts +9 -0
- package/dist/lib/command/planner.js +14 -0
- package/dist/lib/config/constants.d.ts +3 -1
- package/dist/lib/config/constants.js +14 -1
- package/dist/lib/config/xdg.d.ts +4 -0
- package/dist/lib/config/xdg.js +56 -1
- package/dist/lib/errors.d.ts +20 -1
- package/dist/lib/errors.js +132 -13
- package/dist/lib/output/dialogs.d.ts +27 -0
- package/dist/lib/output/dialogs.js +94 -0
- package/dist/lib/output/interactivity.d.ts +11 -0
- package/dist/lib/output/interactivity.js +48 -0
- package/dist/lib/output/redact.d.ts +1 -0
- package/dist/lib/output/redact.js +12 -0
- package/dist/lib/output/runlog.d.ts +3 -0
- package/dist/lib/output/runlog.js +72 -0
- package/dist/lib/output/sanitize.d.ts +2 -0
- package/dist/lib/output/sanitize.js +57 -0
- package/dist/lib/output/sidecar.d.ts +30 -0
- package/dist/lib/output/sidecar.js +58 -0
- package/dist/lib/output/table.js +5 -1
- package/dist/lib/output/trace.d.ts +11 -0
- package/dist/lib/output/trace.js +89 -0
- package/dist/lib/planner/catalog.d.ts +26 -0
- package/dist/lib/planner/catalog.js +60 -0
- package/dist/lib/planner/client.d.ts +14 -0
- package/dist/lib/planner/client.js +47 -0
- package/dist/lib/planner/connection.d.ts +14 -0
- package/dist/lib/planner/connection.js +139 -0
- package/dist/lib/planner/diagnose.d.ts +8 -0
- package/dist/lib/planner/diagnose.js +50 -0
- package/dist/lib/planner/errors.d.ts +14 -0
- package/dist/lib/planner/errors.js +129 -0
- package/dist/lib/planner/filters.d.ts +8 -0
- package/dist/lib/planner/filters.js +74 -0
- package/dist/lib/planner/request.d.ts +24 -0
- package/dist/lib/planner/request.js +51 -0
- package/dist/lib/planner/suggest.d.ts +2 -0
- package/dist/lib/planner/suggest.js +45 -0
- package/dist/lib/planner/vocabulary.d.ts +9 -0
- package/dist/lib/planner/vocabulary.js +95 -0
- package/dist/lib/skills/install.d.ts +24 -0
- package/dist/lib/skills/install.js +69 -0
- package/dist/lib/store/keyring.d.ts +3 -0
- package/dist/lib/store/keyring.js +45 -2
- package/dist/lib/store/memory-store.d.ts +1 -0
- package/dist/lib/store/memory-store.js +5 -0
- package/docs/AGENT-CONTRACT.md +238 -0
- package/oclif.manifest.json +606 -8
- package/package.json +22 -3
- package/skill/SKILL.md +55 -0
- package/dist/lib/auth/register.d.ts +0 -4
- 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
|
-
|
|
74
|
+
creds = secret ? parseCredentials(secret) : null;
|
|
37
75
|
}
|
|
38
76
|
catch {
|
|
39
|
-
|
|
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
|
}
|
|
@@ -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
|
+
```
|