@usenaive-sdk/cli 0.12.2 → 0.13.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 (193) hide show
  1. package/README.md +76 -5
  2. package/dist/client.d.ts +74 -1
  3. package/dist/client.js +351 -17
  4. package/dist/client.js.map +1 -1
  5. package/dist/commands/aeo.js +69 -27
  6. package/dist/commands/aeo.js.map +1 -1
  7. package/dist/commands/app-data.js +29 -11
  8. package/dist/commands/app-data.js.map +1 -1
  9. package/dist/commands/apps.d.ts +7 -0
  10. package/dist/commands/apps.js +255 -18
  11. package/dist/commands/apps.js.map +1 -1
  12. package/dist/commands/audio.d.ts +9 -0
  13. package/dist/commands/audio.js +391 -0
  14. package/dist/commands/audio.js.map +1 -0
  15. package/dist/commands/auth.d.ts +31 -0
  16. package/dist/commands/auth.js +403 -0
  17. package/dist/commands/auth.js.map +1 -0
  18. package/dist/commands/billing.js +12 -12
  19. package/dist/commands/billing.js.map +1 -1
  20. package/dist/commands/brain.d.ts +2 -0
  21. package/dist/commands/brain.js +1255 -0
  22. package/dist/commands/brain.js.map +1 -0
  23. package/dist/commands/browser.js +35 -8
  24. package/dist/commands/browser.js.map +1 -1
  25. package/dist/commands/business.js +117 -16
  26. package/dist/commands/business.js.map +1 -1
  27. package/dist/commands/clone.d.ts +2 -0
  28. package/dist/commands/clone.js +203 -0
  29. package/dist/commands/clone.js.map +1 -0
  30. package/dist/commands/companies.d.ts +8 -0
  31. package/dist/commands/companies.js +137 -47
  32. package/dist/commands/companies.js.map +1 -1
  33. package/dist/commands/company-data.d.ts +2 -0
  34. package/dist/commands/company-data.js +168 -0
  35. package/dist/commands/company-data.js.map +1 -0
  36. package/dist/commands/compute.js +10 -3
  37. package/dist/commands/compute.js.map +1 -1
  38. package/dist/commands/config.d.ts +2 -0
  39. package/dist/commands/config.js +36 -0
  40. package/dist/commands/config.js.map +1 -0
  41. package/dist/commands/connections.d.ts +6 -0
  42. package/dist/commands/connections.js +29 -1
  43. package/dist/commands/connections.js.map +1 -1
  44. package/dist/commands/domains.js +1 -1
  45. package/dist/commands/ecommerce.js +19 -9
  46. package/dist/commands/ecommerce.js.map +1 -1
  47. package/dist/commands/email.js +56 -9
  48. package/dist/commands/email.js.map +1 -1
  49. package/dist/commands/env.d.ts +3 -3
  50. package/dist/commands/env.js +68 -4
  51. package/dist/commands/env.js.map +1 -1
  52. package/dist/commands/formation.js +3 -3
  53. package/dist/commands/identity.js +4 -3
  54. package/dist/commands/identity.js.map +1 -1
  55. package/dist/commands/images.js +87 -13
  56. package/dist/commands/images.js.map +1 -1
  57. package/dist/commands/init.d.ts +3 -0
  58. package/dist/commands/init.js +39 -36
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/commands/jobs.js +1 -0
  61. package/dist/commands/jobs.js.map +1 -1
  62. package/dist/commands/keys.js +26 -3
  63. package/dist/commands/keys.js.map +1 -1
  64. package/dist/commands/legacy-auth.d.ts +27 -0
  65. package/dist/commands/legacy-auth.js +64 -0
  66. package/dist/commands/legacy-auth.js.map +1 -0
  67. package/dist/commands/llm.js +13 -0
  68. package/dist/commands/llm.js.map +1 -1
  69. package/dist/commands/login.js +2 -0
  70. package/dist/commands/login.js.map +1 -1
  71. package/dist/commands/loops.d.ts +2 -0
  72. package/dist/commands/loops.js +260 -0
  73. package/dist/commands/loops.js.map +1 -0
  74. package/dist/commands/media.js +3 -3
  75. package/dist/commands/media.js.map +1 -1
  76. package/dist/commands/mobile.d.ts +4 -0
  77. package/dist/commands/mobile.js +382 -0
  78. package/dist/commands/mobile.js.map +1 -0
  79. package/dist/commands/payments.d.ts +2 -0
  80. package/dist/commands/payments.js +131 -0
  81. package/dist/commands/payments.js.map +1 -0
  82. package/dist/commands/people.d.ts +2 -0
  83. package/dist/commands/people.js +193 -0
  84. package/dist/commands/people.js.map +1 -0
  85. package/dist/commands/phone.js +3 -3
  86. package/dist/commands/phone.js.map +1 -1
  87. package/dist/commands/projects.d.ts +13 -0
  88. package/dist/commands/projects.js +220 -0
  89. package/dist/commands/projects.js.map +1 -0
  90. package/dist/commands/register.js +16 -8
  91. package/dist/commands/register.js.map +1 -1
  92. package/dist/commands/sandbox.d.ts +2 -0
  93. package/dist/commands/sandbox.js +245 -0
  94. package/dist/commands/sandbox.js.map +1 -0
  95. package/dist/commands/search.js +26 -12
  96. package/dist/commands/search.js.map +1 -1
  97. package/dist/commands/seo.js +16 -8
  98. package/dist/commands/seo.js.map +1 -1
  99. package/dist/commands/sessions.d.ts +6 -0
  100. package/dist/commands/sessions.js +109 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/social-data.d.ts +2 -0
  103. package/dist/commands/social-data.js +244 -0
  104. package/dist/commands/social-data.js.map +1 -0
  105. package/dist/commands/social.js +8 -6
  106. package/dist/commands/social.js.map +1 -1
  107. package/dist/commands/teams.d.ts +69 -0
  108. package/dist/commands/teams.js +503 -0
  109. package/dist/commands/teams.js.map +1 -0
  110. package/dist/commands/up.d.ts +150 -0
  111. package/dist/commands/up.js +415 -15
  112. package/dist/commands/up.js.map +1 -1
  113. package/dist/commands/usage.js +15 -4
  114. package/dist/commands/usage.js.map +1 -1
  115. package/dist/commands/use.d.ts +9 -0
  116. package/dist/commands/use.js +76 -11
  117. package/dist/commands/use.js.map +1 -1
  118. package/dist/commands/users.d.ts +8 -0
  119. package/dist/commands/users.js +28 -9
  120. package/dist/commands/users.js.map +1 -1
  121. package/dist/commands/vault.js +28 -1
  122. package/dist/commands/vault.js.map +1 -1
  123. package/dist/commands/video.js +190 -270
  124. package/dist/commands/video.js.map +1 -1
  125. package/dist/commands/voice.d.ts +2 -0
  126. package/dist/commands/voice.js +516 -0
  127. package/dist/commands/voice.js.map +1 -0
  128. package/dist/commands/wallet.d.ts +2 -0
  129. package/dist/commands/wallet.js +244 -0
  130. package/dist/commands/wallet.js.map +1 -0
  131. package/dist/commands/whoami.js +17 -1
  132. package/dist/commands/whoami.js.map +1 -1
  133. package/dist/config.d.ts +24 -1
  134. package/dist/config.js +36 -7
  135. package/dist/config.js.map +1 -1
  136. package/dist/index.d.ts +2 -1
  137. package/dist/index.js +290 -20
  138. package/dist/index.js.map +1 -1
  139. package/dist/lib/brain-select.d.ts +193 -0
  140. package/dist/lib/brain-select.js +403 -0
  141. package/dist/lib/brain-select.js.map +1 -0
  142. package/dist/lib/cli-validation.d.ts +12 -0
  143. package/dist/lib/cli-validation.js +46 -0
  144. package/dist/lib/cli-validation.js.map +1 -0
  145. package/dist/lib/deprecation.d.ts +158 -0
  146. package/dist/lib/deprecation.js +140 -0
  147. package/dist/lib/deprecation.js.map +1 -0
  148. package/dist/lib/duration.d.ts +8 -0
  149. package/dist/lib/duration.js +11 -0
  150. package/dist/lib/duration.js.map +1 -0
  151. package/dist/lib/http-response.d.ts +11 -0
  152. package/dist/lib/http-response.js +96 -0
  153. package/dist/lib/http-response.js.map +1 -0
  154. package/dist/lib/pricing.d.ts +132 -0
  155. package/dist/lib/pricing.js +148 -0
  156. package/dist/lib/pricing.js.map +1 -0
  157. package/dist/lib/stream-frames.d.ts +114 -0
  158. package/dist/lib/stream-frames.js +169 -0
  159. package/dist/lib/stream-frames.js.map +1 -0
  160. package/dist/lib/teams-catalog.d.ts +141 -0
  161. package/dist/lib/teams-catalog.js +765 -0
  162. package/dist/lib/teams-catalog.js.map +1 -0
  163. package/dist/lib/unknown-command.d.ts +57 -0
  164. package/dist/lib/unknown-command.js +93 -0
  165. package/dist/lib/unknown-command.js.map +1 -0
  166. package/dist/lib/version-check.d.ts +26 -0
  167. package/dist/lib/version-check.js +21 -0
  168. package/dist/lib/version-check.js.map +1 -0
  169. package/dist/output.d.ts +119 -10
  170. package/dist/output.js +309 -15
  171. package/dist/output.js.map +1 -1
  172. package/dist/prompt.d.ts +17 -0
  173. package/dist/prompt.js +70 -0
  174. package/dist/prompt.js.map +1 -0
  175. package/package.json +11 -6
  176. package/dist/commands/billing-client.d.ts +0 -2
  177. package/dist/commands/billing-client.js +0 -271
  178. package/dist/commands/billing-client.js.map +0 -1
  179. package/dist/commands/crm-app.d.ts +0 -2
  180. package/dist/commands/crm-app.js +0 -306
  181. package/dist/commands/crm-app.js.map +0 -1
  182. package/dist/commands/crm.d.ts +0 -2
  183. package/dist/commands/crm.js +0 -319
  184. package/dist/commands/crm.js.map +0 -1
  185. package/dist/commands/integrations.d.ts +0 -2
  186. package/dist/commands/integrations.js +0 -199
  187. package/dist/commands/integrations.js.map +0 -1
  188. package/dist/commands/link.d.ts +0 -3
  189. package/dist/commands/link.js +0 -91
  190. package/dist/commands/link.js.map +0 -1
  191. package/dist/commands/operators.d.ts +0 -11
  192. package/dist/commands/operators.js +0 -120
  193. package/dist/commands/operators.js.map +0 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ Command-line interface for the Naive API v2. Register, manage identity, and use
14
14
  - **Video** — Generate via fal.ai (dynamic pricing)
15
15
  - **Social** — Connect accounts, create posts, schedule, analytics (via bundle.social)
16
16
  - **Verification** — Identity verification (KYC) for company founders/members via Footprint
17
- - **Formation** — Company formation (LLC incorporation) via Doola ($249 fee via Stripe Checkout)
17
+ - **Formation** — Company formation (LLC incorporation) via Doola ($349 fee via Stripe Checkout)
18
18
  - **Cards** — Virtual cards — create, fund, assign agents, track transactions (Stripe Issuing + Reloadly)
19
19
  - **Jobs** — List, inspect, and cancel async jobs
20
20
  - **Status** — Credit balance and usage history
@@ -74,7 +74,7 @@ The CLI stores config at `~/.naive/config.json`:
74
74
  Set via registration or environment:
75
75
 
76
76
  ```bash
77
- # Auto-saved on register/login/link
77
+ # Auto-saved on register/login/browser auth
78
78
  naive register --name "My Agent" --email owner@example.com --password yourpassword
79
79
 
80
80
  # Or set manually
@@ -82,13 +82,61 @@ export NAIVE_API_KEY=nv_sk_live_...
82
82
  export NAIVE_BASE_URL=http://localhost:3100 # for local dev
83
83
  ```
84
84
 
85
+ ### The durable runtime — `naive teams`
86
+
87
+ The addressing tuple is `(company, tenant, team)` and it is in the path, not
88
+ inferred, so `--tenant` is required on every subcommand except `list`. There is
89
+ no default: `--tenant @self` is the only shorthand.
90
+
91
+ ```bash
92
+ naive teams list
93
+ naive teams show support --tenant tu_8f21
94
+ naive teams submit support "refund the duplicate charge" --tenant tu_8f21
95
+ naive teams board support --tenant tu_8f21
96
+ naive teams watch support --tenant tu_8f21 --run run_01J8 --follow # NDJSON
97
+ naive teams approvals support --tenant tu_8f21
98
+ naive teams decide support apr_01J --allow --because "verified with the customer"
99
+ ```
100
+
101
+ All 27 subcommands, their flags, their REST paths and their ActionIds are
102
+ declared once in `src/lib/teams-catalog.ts`; `src/commands/teams.ts` builds the
103
+ commander tree by walking it and declares no subcommand of its own.
104
+
105
+ `--because` is a required *flag* (never a positional) on every op that widens
106
+ authority or destroys state: `unblock decide settle model stop migrate rollback`.
107
+
108
+ Exit codes: `0` success · `1` error · `2` success with a diff
109
+ (`plan --detailed-exitcode`) · `3` success where the terminal state is not
110
+ success (`tasks|task --fail-on-unverified`, `watch --fail-on-terminal`).
111
+ `unverified` is a third terminal state, and a CI script that reads `0` as "the
112
+ work is done" would otherwise count an unattested task as a pass.
113
+
114
+ **Not yet served.** There is no `/v1/teams` mount on `packages/api` in this
115
+ repository. Every `naive teams` subcommand therefore reaches the API's catch-all
116
+ today, and the CLI says exactly that — it reports the missing *route*, not a
117
+ missing team.
118
+
119
+ ### Deprecation
120
+
121
+ `ceo · tasks · objectives · employees · cron · loops · memory` drive the legacy
122
+ orchestration runtime, which is frozen. **They all keep working**, unchanged.
123
+ Each prints a one-line banner on stderr carrying a greppable `[dep.<group>]` id
124
+ and its replacement, and attaches a `deprecation` object to the JSON envelope on
125
+ stdout, so stdout stays `JSON.parse`-able. Nothing is disabled and no route is
126
+ removed.
127
+
128
+ Where a capability genuinely has no successor (`tasks complete`, `cron pause`,
129
+ the provisioning verbs) the notice says so and gives the reason, rather than
130
+ inventing a replacement — the record's `replacement` is a closed union, so a
131
+ missing one cannot render as an empty string.
132
+
85
133
  ### Commands
86
134
 
87
135
  ```bash
88
136
  # Auth
89
137
  naive register --name "Agent" --email user@example.com --password mypassword --company "Acme"
90
138
  naive login --email user@example.com --password mypassword
91
- naive link --email existing@example.com
139
+ naive auth email existing@example.com
92
140
  naive whoami
93
141
  naive companies
94
142
  naive keys list
@@ -165,6 +213,7 @@ naive email create --local-part hello # Create new inbox on active domain
165
213
  naive email delete <inbox-id> # Delete an inbox
166
214
  naive email send --from-inbox <uuid> --to user@example.com --subject "Hi" --body "Hello"
167
215
  naive email inbox --inbox <uuid> --limit 20
216
+ naive email sent --limit 20
168
217
  naive email read <email-id>
169
218
 
170
219
  # Search
@@ -196,13 +245,13 @@ naive social sync # Sync from bundle.
196
245
  naive social upload --url https://example.com/video.mp4 # Upload media
197
246
  naive social posts # List posts
198
247
  naive social posts --status draft # Filter by status
199
- naive social post "Hello world!" --platforms twitter,linkedin --publish # Create + publish (1 credit)
248
+ naive social post "Hello world!" --platforms twitter,linkedin --publish # Create + publish (2.5 credits; 3 for X)
200
249
  naive social post "Draft" --platforms twitter # Create draft (free)
201
250
  naive social post "Demo" --platforms youtube --media-url https://example.com/video.mp4 --youtube-type SHORT --publish
202
251
  naive social get <post-id> # Get post details
203
252
  naive social edit <post-id> --content "Updated" # Edit draft
204
253
  naive social delete <post-id> # Delete post
205
- naive social publish <post-id> # Publish draft (1 credit)
254
+ naive social publish <post-id> # Publish draft (2.5 credits; 3 for X)
206
255
  naive social analytics <post-id> # Post analytics
207
256
  naive social comments <post-id> # Post comments
208
257
  naive social account-analytics <account-id> # Account analytics
@@ -301,3 +350,25 @@ src/
301
350
  ├── cards.ts # naive cards (list/cardholder/create-cardholder/create/details/check-payment/retry-issue/top-up/refund/cancel/assignments/assign/unassign/log-transaction/transactions)
302
351
  └── jobs.ts # naive jobs (list/get/cancel)
303
352
  ```
353
+
354
+
355
+ ## Purpose
356
+
357
+ Naïve CLI — provision and govern a real-world agent profile per agent (identity, card, comms, runtime). Formerly @usenaive/cli.
358
+
359
+ ## Consumers
360
+
361
+ Part of the Naive monorepo. See the [root README](../../README.md) for how components fit together and which packages depend on this one.
362
+
363
+ ## Run standalone
364
+
365
+ ```bash
366
+ pnpm --filter @usenaive-sdk/cli dev
367
+ pnpm --filter @usenaive-sdk/cli build
368
+ pnpm --filter @usenaive-sdk/cli test
369
+ pnpm --filter @usenaive-sdk/cli typecheck
370
+ ```
371
+
372
+ ## Place in the topology
373
+
374
+ One node in the Naive monorepo (open plumbing). The [root README](../../README.md) has the full architecture diagram.
package/dist/client.d.ts CHANGED
@@ -1,6 +1,45 @@
1
+ /** True when `data` is our synthetic non-JSON marker (so handleApiError fires even
2
+ * on the rare non-JSON body carrying a 2xx status). */
3
+ export declare function isUpstreamError(data: unknown): boolean;
4
+ /**
5
+ * Read a fetch Response body as JSON without ever throwing. Real API bodies are
6
+ * always JSON (the fast path). A non-JSON body (HTML gateway page, proxy text,
7
+ * empty body) is turned into a synthetic `{ error: { code: "upstream_error", … } }`
8
+ * envelope — the same shape formatApiError already understands — carrying the HTTP
9
+ * status and a bounded snippet, so the caller emits a clean error instead of crashing.
10
+ */
11
+ export declare function parseJsonOrUpstreamError(resp: Response): Promise<unknown>;
1
12
  export declare function setActiveUserOverride(userId?: string): void;
2
- /** Resolve the active subject user id (override > config). */
13
+ /**
14
+ * Resolve the active subject user id (override > env > config).
15
+ *
16
+ * NAIVE_ACTIVE_USER_ID is how a containerized agent gets a subject: its Hermes
17
+ * profile's .env carries its own sealed key plus its tenant_user id, so the CLI
18
+ * it runs scopes calls to /v1/users/<that id>/... and the agent's AccountKit
19
+ * (its declared `can:` / `limits:`) is actually enforced. There is no
20
+ * ~/.naive/config.json in a profile's home to hold `active_user_id`.
21
+ */
3
22
  export declare function activeUserId(): string | undefined;
23
+ export declare function setActiveProjectOverride(projectId?: string): void;
24
+ /**
25
+ * The `--project` flag as parsed off the ROOT program, if it was given.
26
+ *
27
+ * `naive use <child> --project <id>` writes the flag after the subcommand, but
28
+ * commander binds it to the root program's identically-named option, leaving the
29
+ * subcommand's own `opts.project` undefined. `use` reads it back through here.
30
+ */
31
+ export declare function activeProjectOverride(): string | undefined;
32
+ /**
33
+ * Resolve the active project id (override > env > config).
34
+ *
35
+ * Undefined is a MEANINGFUL answer, not a missing one: the server resolves the
36
+ * organization's default project, which is where `naive` acted before projects
37
+ * existed and where migration 059 backfilled every row. So the header is sent
38
+ * only when a project was actually chosen, and a CLI that never mentions one
39
+ * keeps producing byte-identical requests.
40
+ */
41
+ export declare function activeProjectId(): string | undefined;
42
+ export declare function scopePathToUser(path: string, subject: string | undefined): string;
4
43
  export declare function apiRequest(method: string, rawPath: string, body?: Record<string, unknown>, opts?: {
5
44
  noAuth?: boolean;
6
45
  userId?: string;
@@ -9,6 +48,23 @@ export declare function apiRequest(method: string, rawPath: string, body?: Recor
9
48
  status: number;
10
49
  data: unknown;
11
50
  }>;
51
+ /**
52
+ * Like {@link apiRequest} but for endpoints that return a BINARY body (e.g.
53
+ * synthesized speech). Sends the same JSON request and auth/subject handling,
54
+ * and on success returns the raw bytes plus the response headers, so callers
55
+ * can read ids like `x-audio-request-id` off the response.
56
+ *
57
+ * An error response is still JSON, so it is parsed into the usual `{ status,
58
+ * data }` shape — `handleApiError` works unchanged.
59
+ */
60
+ export declare function apiRequestBinary(method: string, rawPath: string, body?: Record<string, unknown>, opts?: {
61
+ userId?: string;
62
+ headers?: Record<string, string>;
63
+ }): Promise<{
64
+ status: number;
65
+ data: unknown;
66
+ headers?: Record<string, string>;
67
+ }>;
12
68
  /**
13
69
  * Upload a raw gzipped payload (e.g. a project tarball for direct app deploys).
14
70
  * Same auth + subject handling as apiRequest, but with a binary body.
@@ -23,6 +79,23 @@ export declare function handleApiError(action: string, resp: {
23
79
  status: number;
24
80
  data: unknown;
25
81
  }): void;
82
+ /** Exchange email+password for a session token, captured from Set-Cookie (the token is
83
+ * not in the response body). Returns the token + the login payload. Exits on API error. */
84
+ export declare function sessionLogin(email: string, password: string): Promise<{
85
+ token: string;
86
+ data: Record<string, unknown>;
87
+ }>;
88
+ /** HTTP for human-only routes. Sends the session cookie, NEVER the agent api_key.
89
+ * Supports JSON and multipart (native FormData) bodies, with an upload timeout. */
90
+ export declare function sessionRequest(method: string, path: string, opts?: {
91
+ body?: Record<string, unknown>;
92
+ multipart?: FormData;
93
+ headers?: Record<string, string>;
94
+ timeoutMs?: number;
95
+ }): Promise<{
96
+ status: number;
97
+ data: unknown;
98
+ }>;
26
99
  /**
27
100
  * Governance: a sensitive action may be deferred for human approval (HTTP 202
28
101
  * with `status: "pending_approval"`). When that happens, emit a structured
package/dist/client.js CHANGED
@@ -1,14 +1,144 @@
1
- import { loadConfig, getApiKey } from "./config.js";
2
- import { formatApiError, agentOutput } from "./output.js";
1
+ import { loadConfig, getApiKey, getSessionToken } from "./config.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { formatApiError, agentOutput, agentError, setPendingUpdate } from "./output.js";
4
+ import { readVersion } from "./version.js";
5
+ import { evaluateCliVersion } from "./lib/version-check.js";
6
+ import { networkErrorResponse, parseHttpResponse } from "./lib/http-response.js";
7
+ const UPDATE_COMMAND = "npm i -g @usenaive-sdk/cli@latest";
8
+ /**
9
+ * Server-driven version handshake. The API stamps `x-naive-cli-latest` and
10
+ * `x-naive-cli-min` on every response; we compare them against this CLI's own
11
+ * version. Below-minimum is a hard block (exits via agentError); below-latest
12
+ * queues a non-fatal update notice for the next agentOutput. Missing/unparseable
13
+ * headers are a no-op (evaluateCliVersion returns "ok").
14
+ */
15
+ function checkCliVersion(resp) {
16
+ const latest = resp.headers.get("x-naive-cli-latest") ?? undefined;
17
+ const min = resp.headers.get("x-naive-cli-min") ?? undefined;
18
+ if (!latest && !min)
19
+ return;
20
+ // Best-effort: the version check must never break the user's command. The only
21
+ // intended non-return exit is the `cli_outdated` hard block; any unexpected
22
+ // throw is swallowed to a no-op.
23
+ try {
24
+ const current = readVersion();
25
+ const decision = evaluateCliVersion(current, min, latest);
26
+ if (decision.kind === "block") {
27
+ agentError({
28
+ action: "version_check",
29
+ error: {
30
+ code: "cli_outdated",
31
+ message: `Your naive CLI (v${decision.current}) is older than the minimum supported version (v${decision.min}). Update to continue.`,
32
+ },
33
+ recovery_steps: [
34
+ { command: UPDATE_COMMAND, description: "Update to the latest naive CLI, then re-run your command" },
35
+ ],
36
+ });
37
+ return; // unreachable — agentError calls process.exit(1)
38
+ }
39
+ if (decision.kind === "notice") {
40
+ setPendingUpdate({
41
+ current: decision.current,
42
+ latest: decision.latest,
43
+ update_command: UPDATE_COMMAND,
44
+ note: `A newer naive CLI is available (v${decision.current} → v${decision.latest}). Update with: ${UPDATE_COMMAND}`,
45
+ });
46
+ }
47
+ }
48
+ catch {
49
+ // Version check is advisory; on any unexpected failure, do nothing.
50
+ }
51
+ }
52
+ // ── Non-JSON body handling (standalone helper) ──────────────────────────────
53
+ //
54
+ // The main request paths (apiRequest / apiUploadGzip / sessionLogin /
55
+ // sessionRequest) go through requestResponse() + lib/http-response.ts, which is
56
+ // content-type-aware, redacts secrets, and does the idempotent GET/HEAD retry.
57
+ // Callers that run their OWN fetch for a media/binary path (commands/media.ts,
58
+ // commands/clone.ts) use parseJsonOrUpstreamError() below to read a JSON body
59
+ // without ever throwing on a non-JSON gateway page.
60
+ const BODY_SNIPPET_MAX = 200;
61
+ /** Collapse whitespace and hard-truncate a response body so we never dump a full
62
+ * HTML page into the error envelope. */
63
+ function bodySnippet(text) {
64
+ return text.replace(/\s+/g, " ").trim().slice(0, BODY_SNIPPET_MAX);
65
+ }
66
+ /** True when `data` is our synthetic non-JSON marker (so handleApiError fires even
67
+ * on the rare non-JSON body carrying a 2xx status). */
68
+ export function isUpstreamError(data) {
69
+ return (typeof data === "object" &&
70
+ data !== null &&
71
+ data.error?.code === "upstream_error");
72
+ }
73
+ /**
74
+ * Read a fetch Response body as JSON without ever throwing. Real API bodies are
75
+ * always JSON (the fast path). A non-JSON body (HTML gateway page, proxy text,
76
+ * empty body) is turned into a synthetic `{ error: { code: "upstream_error", … } }`
77
+ * envelope — the same shape formatApiError already understands — carrying the HTTP
78
+ * status and a bounded snippet, so the caller emits a clean error instead of crashing.
79
+ */
80
+ export async function parseJsonOrUpstreamError(resp) {
81
+ const raw = await resp.text();
82
+ try {
83
+ return JSON.parse(raw);
84
+ }
85
+ catch {
86
+ return {
87
+ error: {
88
+ code: "upstream_error",
89
+ message: `The API returned a non-JSON response (HTTP ${resp.status}). This is usually a transient gateway timeout — retry in a moment.`,
90
+ http_status: resp.status,
91
+ body_snippet: bodySnippet(raw),
92
+ },
93
+ };
94
+ }
95
+ }
3
96
  // Per-invocation override for the active subject user (set by a global --user
4
97
  // flag). Falls back to config.active_user_id, then the api key's default.
5
98
  let _userOverride;
6
99
  export function setActiveUserOverride(userId) {
7
100
  _userOverride = userId;
8
101
  }
9
- /** Resolve the active subject user id (override > config). */
102
+ /**
103
+ * Resolve the active subject user id (override > env > config).
104
+ *
105
+ * NAIVE_ACTIVE_USER_ID is how a containerized agent gets a subject: its Hermes
106
+ * profile's .env carries its own sealed key plus its tenant_user id, so the CLI
107
+ * it runs scopes calls to /v1/users/<that id>/... and the agent's AccountKit
108
+ * (its declared `can:` / `limits:`) is actually enforced. There is no
109
+ * ~/.naive/config.json in a profile's home to hold `active_user_id`.
110
+ */
10
111
  export function activeUserId() {
11
- return _userOverride ?? loadConfig().active_user_id;
112
+ const fromEnv = process.env.NAIVE_ACTIVE_USER_ID?.trim();
113
+ return _userOverride ?? (fromEnv || undefined) ?? loadConfig().active_user_id;
114
+ }
115
+ // Per-invocation override for the active project (set by a global --project flag).
116
+ let _projectOverride;
117
+ export function setActiveProjectOverride(projectId) {
118
+ _projectOverride = projectId;
119
+ }
120
+ /**
121
+ * The `--project` flag as parsed off the ROOT program, if it was given.
122
+ *
123
+ * `naive use <child> --project <id>` writes the flag after the subcommand, but
124
+ * commander binds it to the root program's identically-named option, leaving the
125
+ * subcommand's own `opts.project` undefined. `use` reads it back through here.
126
+ */
127
+ export function activeProjectOverride() {
128
+ return _projectOverride;
129
+ }
130
+ /**
131
+ * Resolve the active project id (override > env > config).
132
+ *
133
+ * Undefined is a MEANINGFUL answer, not a missing one: the server resolves the
134
+ * organization's default project, which is where `naive` acted before projects
135
+ * existed and where migration 059 backfilled every row. So the header is sent
136
+ * only when a project was actually chosen, and a CLI that never mentions one
137
+ * keeps producing byte-identical requests.
138
+ */
139
+ export function activeProjectId() {
140
+ const fromEnv = process.env.NAIVE_ACTIVE_PROJECT_ID?.trim();
141
+ return _projectOverride ?? (fromEnv || undefined) ?? loadConfig().active_project_id;
12
142
  }
13
143
  /**
14
144
  * Native primitives that have per-user path routes (/v1/users/:id/<prim>).
@@ -18,10 +148,23 @@ export function activeUserId() {
18
148
  * AccountKit — matching the SDK and dashboard. The legacy routes don't run
19
149
  * the subject middleware, so without this the header was silently ignored.
20
150
  */
21
- const PER_USER_PRIMITIVES = new Set(["cards", "phone", "trading", "email", "domains", "verification", "formation", "social", "compute", "queue"]);
22
- function scopePathToUser(path, subject) {
23
- if (!subject)
24
- return path; // solo flow (no active user) keeps legacy behavior
151
+ // NOTE: unlike the others, `wallet` and `payments` have NO legacy company-level
152
+ // mount. The default subject sentinel below is therefore required even when the
153
+ // caller has not explicitly selected a tenant user.
154
+ const PER_USER_PRIMITIVES = new Set(["cards", "phone", "trading", "email", "domains", "verification", "formation", "social", "compute", "queue", "mobile", "wallet", "payments"]);
155
+ const USER_PATH_ONLY_PRIMITIVES = new Set(["wallet", "payments"]);
156
+ /**
157
+ * The wallet's OPERATOR verbs: fund / sweep / setPolicy, plus POST /wallet
158
+ * (create) and /transfer, which are operator verbs too since they
159
+ * provision the float and move it out respectively. All of them move or
160
+ * re-govern the float. Reads (GET /wallet, /wallet/balances) and the agent's own
161
+ * /payments surface are NOT admin.
162
+ */
163
+ const WALLET_ADMIN_PATH = /^\/v1\/(?:users\/[^/]+\/)?wallet(?:\/(?:fund|transfer|policy|sweep))?(?:\?|$)/;
164
+ function isWalletAdminCall(method, rawPath) {
165
+ return method.toUpperCase() === "POST" && WALLET_ADMIN_PATH.test(rawPath);
166
+ }
167
+ export function scopePathToUser(path, subject) {
25
168
  if (path.startsWith("/v1/users/"))
26
169
  return path; // already user-scoped
27
170
  const m = path.match(/^\/v1\/([^/?]+)(.*)$/);
@@ -29,12 +172,58 @@ function scopePathToUser(path, subject) {
29
172
  return path;
30
173
  if (!PER_USER_PRIMITIVES.has(m[1]))
31
174
  return path;
32
- return `/v1/users/${subject}/${m[1]}${m[2]}`;
175
+ const resolvedSubject = subject ?? (USER_PATH_ONLY_PRIMITIVES.has(m[1]) ? "default" : undefined);
176
+ if (!resolvedSubject)
177
+ return path;
178
+ return `/v1/users/${resolvedSubject}/${m[1]}${m[2]}`;
179
+ }
180
+ const TRANSIENT_READ_STATUSES = new Set([500, 502, 503, 504]);
181
+ async function requestResponse(url, init) {
182
+ const method = String(init.method ?? "GET").toUpperCase();
183
+ const mayRetry = method === "GET" || method === "HEAD";
184
+ for (let attempt = 0; attempt < 2; attempt++) {
185
+ let response;
186
+ try {
187
+ response = await fetch(url, init);
188
+ }
189
+ catch (error) {
190
+ if (mayRetry && attempt === 0) {
191
+ await new Promise((resolve) => setTimeout(resolve, 100));
192
+ continue;
193
+ }
194
+ return networkErrorResponse(error);
195
+ }
196
+ if (mayRetry && attempt === 0 && TRANSIENT_READ_STATUSES.has(response.status)) {
197
+ await response.body?.cancel().catch(() => { });
198
+ await new Promise((resolve) => setTimeout(resolve, 100));
199
+ continue;
200
+ }
201
+ try {
202
+ return { ...(await parseHttpResponse(response)), response };
203
+ }
204
+ catch (error) {
205
+ return { ...networkErrorResponse(error), response };
206
+ }
207
+ }
208
+ return networkErrorResponse(new Error("Request failed after retry"));
33
209
  }
34
210
  export async function apiRequest(method, rawPath, body, opts) {
211
+ // Invariant guard: the human-only admin surface must NEVER be reached with an agent
212
+ // key. apiRequest sends the agent bearer, so it must refuse these paths outright.
213
+ if (rawPath.startsWith("/v1/voice-admin")) {
214
+ throw new Error("Internal error: /v1/voice-admin is human-only and must be called via sessionRequest, not apiRequest.");
215
+ }
216
+ // Same invariant for the wallet's operator verbs: an agent must
217
+ // never be able to fund, transfer, sweep or re-policy the float it spends
218
+ // from. The server enforces this too (enforceWalletAdmin); this is the
219
+ // client-side half, so a mistake surfaces at the call site, not as a 403.
220
+ if (isWalletAdminCall(method, rawPath)) {
221
+ throw new Error(`Internal error: ${rawPath} is an operator-only wallet verb and must be called via sessionRequest, not apiRequest.`);
222
+ }
35
223
  const config = loadConfig();
36
224
  const headers = {
37
225
  "Content-Type": "application/json",
226
+ "X-Request-Id": randomUUID(),
38
227
  ...(opts?.headers ?? {}),
39
228
  };
40
229
  let path = rawPath;
@@ -45,18 +234,78 @@ export async function apiRequest(method, rawPath, body, opts) {
45
234
  const subject = opts?.userId ?? activeUserId();
46
235
  if (subject)
47
236
  headers["X-Naive-User-Id"] = subject;
237
+ // Select the project the same way: a header, so no route has to change.
238
+ const project = activeProjectId();
239
+ if (project)
240
+ headers["X-Naive-Project-Id"] = project;
48
241
  // Native primitives only isolate via their per-user path routes, so rewrite
49
- // legacy paths to the path-scoped form when a subject is active.
242
+ // legacy paths to the path-scoped form. "default" asks the API to resolve
243
+ // the key/company default when the CLI has no explicit `naive use` value.
50
244
  path = scopePathToUser(path, subject);
51
245
  }
52
246
  const url = `${config.base_url}${path}`;
53
- const resp = await fetch(url, {
247
+ const parsed = await requestResponse(url, {
54
248
  method,
55
249
  headers,
56
250
  ...(body && { body: JSON.stringify(body) }),
57
251
  });
58
- const data = await resp.json();
59
- return { status: resp.status, data };
252
+ if (parsed.response)
253
+ checkCliVersion(parsed.response);
254
+ return { status: parsed.status, data: parsed.data };
255
+ }
256
+ /**
257
+ * Like {@link apiRequest} but for endpoints that return a BINARY body (e.g.
258
+ * synthesized speech). Sends the same JSON request and auth/subject handling,
259
+ * and on success returns the raw bytes plus the response headers, so callers
260
+ * can read ids like `x-audio-request-id` off the response.
261
+ *
262
+ * An error response is still JSON, so it is parsed into the usual `{ status,
263
+ * data }` shape — `handleApiError` works unchanged.
264
+ */
265
+ export async function apiRequestBinary(method, rawPath, body, opts) {
266
+ const config = loadConfig();
267
+ const headers = {
268
+ "Content-Type": "application/json",
269
+ "X-Request-Id": randomUUID(),
270
+ ...(opts?.headers ?? {}),
271
+ Authorization: `Bearer ${getApiKey()}`,
272
+ };
273
+ const subject = opts?.userId ?? activeUserId();
274
+ if (subject)
275
+ headers["X-Naive-User-Id"] = subject;
276
+ const binaryProject = activeProjectId();
277
+ if (binaryProject)
278
+ headers["X-Naive-Project-Id"] = binaryProject;
279
+ const path = scopePathToUser(rawPath, subject);
280
+ let response;
281
+ try {
282
+ response = await fetch(`${config.base_url}${path}`, {
283
+ method,
284
+ headers,
285
+ ...(body && { body: JSON.stringify(body) }),
286
+ });
287
+ }
288
+ catch (error) {
289
+ return networkErrorResponse(error);
290
+ }
291
+ checkCliVersion(response);
292
+ if (!response.ok) {
293
+ // Errors come back as the standard JSON envelope even on a binary route.
294
+ const text = await response.text().catch(() => "");
295
+ let data = {};
296
+ try {
297
+ data = text ? JSON.parse(text) : {};
298
+ }
299
+ catch {
300
+ data = { error: { code: "upstream_error", message: text.slice(0, 500) || response.statusText } };
301
+ }
302
+ return { status: response.status, data };
303
+ }
304
+ const flatHeaders = {};
305
+ response.headers.forEach((value, key) => {
306
+ flatHeaders[key.toLowerCase()] = value;
307
+ });
308
+ return { status: response.status, data: await response.arrayBuffer(), headers: flatHeaders };
60
309
  }
61
310
  /**
62
311
  * Upload a raw gzipped payload (e.g. a project tarball for direct app deploys).
@@ -66,25 +315,110 @@ export async function apiUploadGzip(rawPath, payload, opts) {
66
315
  const config = loadConfig();
67
316
  const headers = {
68
317
  "Content-Type": "application/gzip",
318
+ "X-Request-Id": randomUUID(),
69
319
  Authorization: `Bearer ${getApiKey()}`,
70
320
  };
71
321
  const subject = opts?.userId ?? activeUserId();
72
322
  if (subject)
73
323
  headers["X-Naive-User-Id"] = subject;
324
+ const uploadProject = activeProjectId();
325
+ if (uploadProject)
326
+ headers["X-Naive-Project-Id"] = uploadProject;
74
327
  const path = scopePathToUser(rawPath, subject);
75
- const resp = await fetch(`${config.base_url}${path}`, {
328
+ const parsed = await requestResponse(`${config.base_url}${path}`, {
76
329
  method: "POST",
77
330
  headers,
78
331
  body: new Uint8Array(payload),
79
332
  });
80
- const data = await resp.json().catch(() => ({}));
81
- return { status: resp.status, data };
333
+ if (parsed.response)
334
+ checkCliVersion(parsed.response);
335
+ return { status: parsed.status, data: parsed.data };
82
336
  }
83
337
  export function handleApiError(action, resp) {
84
- if (resp.status >= 400) {
338
+ // Fires on any 4xx/5xx, and also on a synthetic upstream_error carrying a 2xx
339
+ // status (a non-JSON body from a proxy) so a malformed success never slips through.
340
+ if (resp.status >= 400 || isUpstreamError(resp.data)) {
85
341
  formatApiError(action, resp.data);
86
342
  }
87
343
  }
344
+ // ── Human session auth (for human-only operations: voice clone/revoke) ──────────────
345
+ //
346
+ // A session cookie is materially more powerful than the agent key on the voice surface
347
+ // (it can clone AND revoke), so it gets stricter handling: never sent over plaintext to a
348
+ // remote host, and the agent api_key is never read in this path.
349
+ /** Refuse to put a session cookie on the wire over plaintext http to a non-local host —
350
+ * the server only marks the cookie `secure` in prod, so we protect it client-side. */
351
+ function assertSecureBaseUrl(baseUrl) {
352
+ let u;
353
+ try {
354
+ u = new URL(baseUrl);
355
+ }
356
+ catch {
357
+ throw new Error(`Invalid base_url: ${baseUrl}`);
358
+ }
359
+ const isLocal = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
360
+ if (u.protocol === "http:" && !isLocal) {
361
+ throw new Error(`Refusing to send a session cookie over plaintext http to ${u.hostname}. Use https, or a localhost base_url for dev.`);
362
+ }
363
+ }
364
+ /** Exchange email+password for a session token, captured from Set-Cookie (the token is
365
+ * not in the response body). Returns the token + the login payload. Exits on API error. */
366
+ export async function sessionLogin(email, password) {
367
+ const config = loadConfig();
368
+ assertSecureBaseUrl(config.base_url);
369
+ const parsed = await requestResponse(`${config.base_url}/v1/auth/session/login`, {
370
+ method: "POST",
371
+ headers: { "Content-Type": "application/json", "X-Request-Id": randomUUID() },
372
+ body: JSON.stringify({ email, password }),
373
+ });
374
+ const data = parsed.data;
375
+ if (parsed.status >= 400 || isUpstreamError(data)) {
376
+ formatApiError("auth.session-login", data); // exits the process
377
+ }
378
+ // Parse by name, not by index — the server may set more than one cookie.
379
+ const cookie = parsed.response?.headers.getSetCookie().find((c) => c.startsWith("naive_session="));
380
+ if (!cookie) {
381
+ throw new Error("Login succeeded but no naive_session cookie was returned.");
382
+ }
383
+ const token = cookie.slice("naive_session=".length).split(";")[0];
384
+ return { token, data };
385
+ }
386
+ /** HTTP for human-only routes. Sends the session cookie, NEVER the agent api_key.
387
+ * Supports JSON and multipart (native FormData) bodies, with an upload timeout. */
388
+ export async function sessionRequest(method, path, opts) {
389
+ const config = loadConfig();
390
+ assertSecureBaseUrl(config.base_url);
391
+ const url = `${config.base_url}${path}`;
392
+ const headers = {
393
+ // getSessionToken() never falls back to the agent key.
394
+ Cookie: `naive_session=${getSessionToken()}`,
395
+ "X-Request-Id": randomUUID(),
396
+ ...(opts?.headers ?? {}),
397
+ };
398
+ let payload;
399
+ if (opts?.multipart) {
400
+ payload = opts.multipart; // fetch sets the multipart Content-Type + boundary
401
+ }
402
+ else if (opts?.body) {
403
+ headers["Content-Type"] = "application/json";
404
+ payload = JSON.stringify(opts.body);
405
+ }
406
+ // Bound the request so a stalled (large) upload can't hang the CLI forever.
407
+ const controller = new AbortController();
408
+ const timeout = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 120_000);
409
+ try {
410
+ const parsed = await requestResponse(url, {
411
+ method,
412
+ headers,
413
+ ...(payload !== undefined && { body: payload }),
414
+ signal: controller.signal,
415
+ });
416
+ return { status: parsed.status, data: parsed.data };
417
+ }
418
+ finally {
419
+ clearTimeout(timeout);
420
+ }
421
+ }
88
422
  /**
89
423
  * Governance: a sensitive action may be deferred for human approval (HTTP 202
90
424
  * with `status: "pending_approval"`). When that happens, emit a structured