@usenaive-sdk/cli 0.12.5 → 0.13.1

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 (164) hide show
  1. package/README.md +69 -6
  2. package/dist/client.d.ts +48 -0
  3. package/dist/client.js +199 -27
  4. package/dist/client.js.map +1 -1
  5. package/dist/commands/aeo.js +58 -23
  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/approvals.js +142 -9
  10. package/dist/commands/approvals.js.map +1 -1
  11. package/dist/commands/apps.d.ts +7 -0
  12. package/dist/commands/apps.js +255 -18
  13. package/dist/commands/apps.js.map +1 -1
  14. package/dist/commands/audio.d.ts +9 -0
  15. package/dist/commands/audio.js +391 -0
  16. package/dist/commands/audio.js.map +1 -0
  17. package/dist/commands/auth.d.ts +1 -0
  18. package/dist/commands/auth.js +47 -19
  19. package/dist/commands/auth.js.map +1 -1
  20. package/dist/commands/billing.js +9 -9
  21. package/dist/commands/billing.js.map +1 -1
  22. package/dist/commands/brain.js +813 -54
  23. package/dist/commands/brain.js.map +1 -1
  24. package/dist/commands/browser.js +8 -8
  25. package/dist/commands/browser.js.map +1 -1
  26. package/dist/commands/business.js +117 -16
  27. package/dist/commands/business.js.map +1 -1
  28. package/dist/commands/clone.js +8 -4
  29. package/dist/commands/clone.js.map +1 -1
  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 +295 -18
  37. package/dist/commands/compute.js.map +1 -1
  38. package/dist/commands/connections.d.ts +6 -0
  39. package/dist/commands/connections.js +42 -2
  40. package/dist/commands/connections.js.map +1 -1
  41. package/dist/commands/domains.js +63 -19
  42. package/dist/commands/domains.js.map +1 -1
  43. package/dist/commands/ecommerce.js +19 -9
  44. package/dist/commands/ecommerce.js.map +1 -1
  45. package/dist/commands/email.js +56 -9
  46. package/dist/commands/email.js.map +1 -1
  47. package/dist/commands/formation.js +5 -5
  48. package/dist/commands/formation.js.map +1 -1
  49. package/dist/commands/identity.js +4 -3
  50. package/dist/commands/identity.js.map +1 -1
  51. package/dist/commands/images.js +86 -12
  52. package/dist/commands/images.js.map +1 -1
  53. package/dist/commands/init.d.ts +3 -0
  54. package/dist/commands/init.js +44 -36
  55. package/dist/commands/init.js.map +1 -1
  56. package/dist/commands/jobs.js +1 -0
  57. package/dist/commands/jobs.js.map +1 -1
  58. package/dist/commands/legacy-auth.d.ts +27 -0
  59. package/dist/commands/legacy-auth.js +64 -0
  60. package/dist/commands/legacy-auth.js.map +1 -0
  61. package/dist/commands/llm.js +14 -1
  62. package/dist/commands/llm.js.map +1 -1
  63. package/dist/commands/login.js +2 -0
  64. package/dist/commands/login.js.map +1 -1
  65. package/dist/commands/logout.d.ts +7 -0
  66. package/dist/commands/logout.js +55 -0
  67. package/dist/commands/logout.js.map +1 -0
  68. package/dist/commands/media.js +3 -3
  69. package/dist/commands/media.js.map +1 -1
  70. package/dist/commands/mobile.d.ts +2 -0
  71. package/dist/commands/mobile.js +42 -1
  72. package/dist/commands/mobile.js.map +1 -1
  73. package/dist/commands/payments.js +1 -1
  74. package/dist/commands/people.d.ts +2 -0
  75. package/dist/commands/people.js +197 -0
  76. package/dist/commands/people.js.map +1 -0
  77. package/dist/commands/phone.js +40 -83
  78. package/dist/commands/phone.js.map +1 -1
  79. package/dist/commands/projects.d.ts +13 -0
  80. package/dist/commands/projects.js +220 -0
  81. package/dist/commands/projects.js.map +1 -0
  82. package/dist/commands/register.js +13 -5
  83. package/dist/commands/register.js.map +1 -1
  84. package/dist/commands/sandbox.d.ts +2 -0
  85. package/dist/commands/sandbox.js +558 -0
  86. package/dist/commands/sandbox.js.map +1 -0
  87. package/dist/commands/search.js +27 -13
  88. package/dist/commands/search.js.map +1 -1
  89. package/dist/commands/seo.js +16 -8
  90. package/dist/commands/seo.js.map +1 -1
  91. package/dist/commands/sessions.d.ts +6 -0
  92. package/dist/commands/sessions.js +109 -0
  93. package/dist/commands/sessions.js.map +1 -0
  94. package/dist/commands/social-data.d.ts +2 -0
  95. package/dist/commands/social-data.js +245 -0
  96. package/dist/commands/social-data.js.map +1 -0
  97. package/dist/commands/social.js +9 -7
  98. package/dist/commands/social.js.map +1 -1
  99. package/dist/commands/tasks.js +9 -0
  100. package/dist/commands/tasks.js.map +1 -1
  101. package/dist/commands/teams.d.ts +69 -0
  102. package/dist/commands/teams.js +503 -0
  103. package/dist/commands/teams.js.map +1 -0
  104. package/dist/commands/up.d.ts +150 -0
  105. package/dist/commands/up.js +465 -15
  106. package/dist/commands/up.js.map +1 -1
  107. package/dist/commands/usage.js +15 -4
  108. package/dist/commands/usage.js.map +1 -1
  109. package/dist/commands/use.d.ts +9 -0
  110. package/dist/commands/use.js +76 -11
  111. package/dist/commands/use.js.map +1 -1
  112. package/dist/commands/users.d.ts +8 -0
  113. package/dist/commands/users.js +28 -9
  114. package/dist/commands/users.js.map +1 -1
  115. package/dist/commands/vault.js +31 -1
  116. package/dist/commands/vault.js.map +1 -1
  117. package/dist/commands/verification.js +1 -1
  118. package/dist/commands/verification.js.map +1 -1
  119. package/dist/commands/video.js +118 -19
  120. package/dist/commands/video.js.map +1 -1
  121. package/dist/commands/wallet.js +3 -3
  122. package/dist/commands/wallet.js.map +1 -1
  123. package/dist/commands/whoami.js +17 -1
  124. package/dist/commands/whoami.js.map +1 -1
  125. package/dist/config.d.ts +16 -1
  126. package/dist/config.js +15 -1
  127. package/dist/config.js.map +1 -1
  128. package/dist/index.d.ts +2 -1
  129. package/dist/index.js +250 -24
  130. package/dist/index.js.map +1 -1
  131. package/dist/lib/brain-select.d.ts +193 -0
  132. package/dist/lib/brain-select.js +403 -0
  133. package/dist/lib/brain-select.js.map +1 -0
  134. package/dist/lib/cli-validation.d.ts +12 -0
  135. package/dist/lib/cli-validation.js +46 -0
  136. package/dist/lib/cli-validation.js.map +1 -0
  137. package/dist/lib/deprecation.d.ts +158 -0
  138. package/dist/lib/deprecation.js +140 -0
  139. package/dist/lib/deprecation.js.map +1 -0
  140. package/dist/lib/duration.d.ts +8 -0
  141. package/dist/lib/duration.js +11 -0
  142. package/dist/lib/duration.js.map +1 -0
  143. package/dist/lib/http-response.d.ts +11 -0
  144. package/dist/lib/http-response.js +96 -0
  145. package/dist/lib/http-response.js.map +1 -0
  146. package/dist/lib/pricing.d.ts +132 -0
  147. package/dist/lib/pricing.js +148 -0
  148. package/dist/lib/pricing.js.map +1 -0
  149. package/dist/lib/stream-frames.d.ts +114 -0
  150. package/dist/lib/stream-frames.js +169 -0
  151. package/dist/lib/stream-frames.js.map +1 -0
  152. package/dist/lib/teams-catalog.d.ts +141 -0
  153. package/dist/lib/teams-catalog.js +765 -0
  154. package/dist/lib/teams-catalog.js.map +1 -0
  155. package/dist/lib/unknown-command.d.ts +55 -1
  156. package/dist/lib/unknown-command.js +113 -0
  157. package/dist/lib/unknown-command.js.map +1 -1
  158. package/dist/output.d.ts +127 -5
  159. package/dist/output.js +189 -12
  160. package/dist/output.js.map +1 -1
  161. package/package.json +3 -3
  162. package/dist/commands/link.d.ts +0 -3
  163. package/dist/commands/link.js +0 -91
  164. package/dist/commands/link.js.map +0 -1
@@ -2,7 +2,34 @@ import { Command } from "commander";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { basename, extname } from "node:path";
4
4
  import { apiRequest, handleApiError, handlePendingApproval } from "../client.js";
5
- import { agentOutput } from "../output.js";
5
+ import { agentError, agentOutput } from "../output.js";
6
+ import { BRAIN_BINDING_KEY, BRAIN_CONNECT_PATH, bindingNextSteps, boundBrainId, listBrains, resolveAgent, resolveBrainId, selectBrainId, setAgentBrain, tryListAgents, } from "../lib/brain-select.js";
7
+ /**
8
+ * Normalise a command's brain-selection flags into a selector.
9
+ *
10
+ * `|| undefined` and not `?? undefined` ON PURPOSE: every one of these
11
+ * subcommands previously wrote `...(opts.kb && { knowledge_base_id: opts.kb })`,
12
+ * so an EMPTY `--kb ""` was silently dropped and the call fell through to the
13
+ * company default. Mapping "" to undefined keeps that exact behaviour instead of
14
+ * turning a pathological-but-working invocation into an error.
15
+ */
16
+ function brainSelector(opts) {
17
+ return { brain: opts.brain || undefined, kb: opts.kb || undefined, agent: opts.agent || undefined };
18
+ }
19
+ /**
20
+ * The three flags that name a brain, added to every subcommand that already
21
+ * scoped its call with `--kb`.
22
+ *
23
+ * `--kb <id>` is UNCHANGED and stays first in the help: a UUID passes straight
24
+ * through with no extra request, exactly as it always has. `--brain` is the same
25
+ * concept spelled the way the command group is (`naive brain …`) and additionally
26
+ * accepts a name. `--agent` means "whatever brain that agent is connected to".
27
+ */
28
+ function withBrainFlags(cmd) {
29
+ return cmd
30
+ .option("--brain <name|id>", "Use this brain (by name or id)")
31
+ .option("--agent <id|name>", "Use the brain this agent is connected to");
32
+ }
6
33
  /** Trivial extension -> MIME map; anything else omits content_type and lets the API decide. */
7
34
  const CONTENT_TYPES = {
8
35
  ".pdf": "application/pdf",
@@ -36,9 +63,41 @@ Subcommands:
36
63
  naive brain docs List ingested documents + processing status
37
64
  naive brain doc <id> Get one document (refreshed processing status)
38
65
  naive brain rm-doc <id> Delete an ingested document (permanent)
39
- naive brain list List the company's knowledge bases
40
- naive brain create Create a new knowledge base
66
+ naive brain list Every brain: which is default, and who is connected
67
+ naive brain create <name> Create a new knowledge base
68
+ naive brain default <brain> Make an existing brain the company default
41
69
  naive brain rm <id> Delete a knowledge base + its documents (permanent)
70
+ naive brain connect <brain> --agent <a> Connect an agent to a brain
71
+ naive brain disconnect --agent <a> Disconnect it again
72
+
73
+ A company can have SEVERAL brains (the schema has always allowed it: one row per
74
+ brain, one flagged default). Every read/write subcommand takes the brain three ways:
75
+ --kb <id> the shipped spelling — an id, passed straight through
76
+ --brain <name|id> the same thing, and it accepts a NAME
77
+ --agent <id|name> whatever brain that agent is connected to
78
+ Pass none and you get the company default, exactly as before.
79
+
80
+ CHOOSING THE DEFAULT:
81
+ naive brain default <name|id> Promote an existing brain. One request, no new
82
+ brain, idempotent, and only an ACTIVE brain can
83
+ be promoted.
84
+ naive brain create X --default Create one AND make it the default.
85
+ naive brain rm <the default> Succeeds, and promotes the oldest survivor in
86
+ the same transaction — the empty 204 cannot say
87
+ which, so run 'naive brain list' after.
88
+
89
+ The run loop (attach → work → consolidate → decide):
90
+ naive brain attach <goal> What the brain already knows, BEFORE the work
91
+ naive brain consolidate Run-end debrief: learned / decided / left open
92
+ naive brain proposals [id] The proposal queue; read one, or decide one
93
+ naive brain writebacks [id] The envelopes a run produced
94
+ naive brain status Operator view of the brain runtime (this company)
95
+ naive brain metrics Nervous-system counters (PROCESS-GLOBAL, not per-company)
96
+
97
+ A debrief PROPOSES. Nothing it says is a belief until a proposal is accepted:
98
+ $ naive brain consolidate --objective "refund audit" --learned "acme|refund window|30 days"
99
+ $ naive brain proposals --status pending
100
+ $ naive brain proposals prp_01J --accept --because "confirmed against the contract"
42
101
 
43
102
  note vs remember:
44
103
  'remember' writes a durable SEMANTIC MEMORY episode (facts/entities/claims are
@@ -64,16 +123,17 @@ Examples:
64
123
  $ naive brain query "And the P1s?" --session <session_id from the previous answer>
65
124
  $ naive brain docs
66
125
  `);
67
- brainCmd
126
+ withBrainFlags(brainCmd
68
127
  .command("recall <query>")
69
128
  .description("Recall structured semantic memory: facts, entities, and relevant episodes")
70
129
  .option("--kb <id>", "Recall from a specific knowledge base")
71
130
  .option("--entity <name>", "Filter to one entity/subject")
72
- .option("--limit <n>", "Maximum memories to return", (v) => Number.parseInt(v, 10))
131
+ .option("--limit <n>", "Maximum memories to return", (v) => Number.parseInt(v, 10)))
73
132
  .action(async (query, opts) => {
133
+ const kbId = await selectBrainId("brain.recall", brainSelector(opts));
74
134
  const resp = await apiRequest("POST", "/v1/brain/recall", {
75
135
  query,
76
- ...(opts.kb && { knowledge_base_id: opts.kb }),
136
+ ...(kbId && { knowledge_base_id: kbId }),
77
137
  ...(opts.entity && { entity: opts.entity }),
78
138
  ...(opts.limit && { limit: opts.limit }),
79
139
  });
@@ -89,21 +149,22 @@ brainCmd
89
149
  related_commands: ["naive brain think", "naive brain remember", "naive brain query"],
90
150
  });
91
151
  });
92
- brainCmd
152
+ withBrainFlags(brainCmd
93
153
  .command("remember <content>")
94
154
  .alias("mem")
95
155
  .description("Write a durable semantic memory episode into the shared company brain")
96
156
  .option("--title <title>", "Optional title")
97
157
  .option("--source <source>", "Provenance tag (default: cli)")
98
158
  .option("--confidence <n>", "0-1 confidence", (v) => Number.parseFloat(v))
99
- .option("--kb <id>", "Target a specific knowledge base")
159
+ .option("--kb <id>", "Target a specific knowledge base"))
100
160
  .action(async (content, opts) => {
161
+ const kbId = await selectBrainId("brain.remember", brainSelector(opts));
101
162
  const resp = await apiRequest("POST", "/v1/brain/remember", {
102
163
  content,
103
164
  ...(opts.title && { title: opts.title }),
104
165
  source: opts.source ?? "cli",
105
166
  ...(opts.confidence !== undefined && { confidence: opts.confidence }),
106
- ...(opts.kb && { knowledge_base_id: opts.kb }),
167
+ ...(kbId && { knowledge_base_id: kbId }),
107
168
  });
108
169
  handleApiError("brain.remember", resp);
109
170
  agentOutput({
@@ -114,16 +175,17 @@ brainCmd
114
175
  related_commands: ["naive brain recall", "naive brain think"],
115
176
  });
116
177
  });
117
- brainCmd
178
+ withBrainFlags(brainCmd
118
179
  .command("think <query>")
119
180
  .description("Synthesize from semantic memory and grounded Brain documents with gap notes")
120
181
  .option("--kb <id>", "Use a specific knowledge base")
121
182
  .option("--session <id>", "Continue a query session")
122
- .option("--limit <n>", "Maximum memories to include", (v) => Number.parseInt(v, 10))
183
+ .option("--limit <n>", "Maximum memories to include", (v) => Number.parseInt(v, 10)))
123
184
  .action(async (query, opts) => {
185
+ const kbId = await selectBrainId("brain.think", brainSelector(opts));
124
186
  const resp = await apiRequest("POST", "/v1/brain/think", {
125
187
  query,
126
- ...(opts.kb && { knowledge_base_id: opts.kb }),
188
+ ...(kbId && { knowledge_base_id: kbId }),
127
189
  ...(opts.session && { session_id: opts.session }),
128
190
  ...(opts.limit && { limit: opts.limit }),
129
191
  });
@@ -136,15 +198,16 @@ brainCmd
136
198
  related_commands: ["naive brain recall", "naive brain query", "naive brain remember"],
137
199
  });
138
200
  });
139
- brainCmd
201
+ withBrainFlags(brainCmd
140
202
  .command("graph")
141
203
  .description("Explore the company brain's entity/relation knowledge graph")
142
204
  .option("--kb <id>", "Use a specific knowledge base")
143
205
  .option("--entity <name>", "Center the graph on one entity/subject")
144
- .option("--limit <n>", "Maximum nodes/edges to return", (v) => Number.parseInt(v, 10))
206
+ .option("--limit <n>", "Maximum nodes/edges to return", (v) => Number.parseInt(v, 10)))
145
207
  .action(async (opts) => {
208
+ const kbId = await selectBrainId("brain.graph", brainSelector(opts));
146
209
  const resp = await apiRequest("POST", "/v1/brain/graph", {
147
- ...(opts.kb && { knowledge_base_id: opts.kb }),
210
+ ...(kbId && { knowledge_base_id: kbId }),
148
211
  ...(opts.entity && { entity: opts.entity }),
149
212
  ...(opts.limit && { limit: opts.limit }),
150
213
  });
@@ -160,15 +223,16 @@ brainCmd
160
223
  related_commands: ["naive brain timeline", "naive brain recall", "naive brain think"],
161
224
  });
162
225
  });
163
- brainCmd
226
+ withBrainFlags(brainCmd
164
227
  .command("timeline")
165
228
  .description("View the company brain's memory events and facts in chronological order")
166
229
  .option("--kb <id>", "Use a specific knowledge base")
167
230
  .option("--entity <name>", "Filter to one entity/subject")
168
- .option("--limit <n>", "Maximum entries to return", (v) => Number.parseInt(v, 10))
231
+ .option("--limit <n>", "Maximum entries to return", (v) => Number.parseInt(v, 10)))
169
232
  .action(async (opts) => {
233
+ const kbId = await selectBrainId("brain.timeline", brainSelector(opts));
170
234
  const resp = await apiRequest("POST", "/v1/brain/timeline", {
171
- ...(opts.kb && { knowledge_base_id: opts.kb }),
235
+ ...(kbId && { knowledge_base_id: kbId }),
172
236
  ...(opts.entity && { entity: opts.entity }),
173
237
  ...(opts.limit && { limit: opts.limit }),
174
238
  });
@@ -184,16 +248,17 @@ brainCmd
184
248
  related_commands: ["naive brain graph", "naive brain recall", "naive brain think"],
185
249
  });
186
250
  });
187
- brainCmd
251
+ withBrainFlags(brainCmd
188
252
  .command("forget <scope> <scope_ref>")
189
253
  .description("Tombstone/forget a semantic memory scope")
190
254
  .option("--kb <id>", "Knowledge base id")
191
- .option("--reason <reason>", "Reason for the tombstone")
255
+ .option("--reason <reason>", "Reason for the tombstone"))
192
256
  .action(async (scope, scope_ref, opts) => {
257
+ const kbId = await selectBrainId("brain.forget", brainSelector(opts));
193
258
  const resp = await apiRequest("POST", "/v1/brain/forget", {
194
259
  scope,
195
260
  scope_ref,
196
- ...(opts.kb && { knowledge_base_id: opts.kb }),
261
+ ...(kbId && { knowledge_base_id: kbId }),
197
262
  ...(opts.reason && { reason: opts.reason }),
198
263
  });
199
264
  handleApiError("brain.forget", resp);
@@ -207,15 +272,16 @@ brainCmd
207
272
  related_commands: ["naive brain recall", "naive brain docs"],
208
273
  });
209
274
  });
210
- brainCmd
275
+ withBrainFlags(brainCmd
211
276
  .command("query <question>")
212
277
  .description("Ask the knowledge base a question; returns a grounded answer with citations")
213
278
  .option("--kb <id>", "Query a specific knowledge base (defaults to the company brain)")
214
- .option("--session <id>", "Continue a previous conversation (session_id from an earlier answer)")
279
+ .option("--session <id>", "Continue a previous conversation (session_id from an earlier answer)"))
215
280
  .action(async (question, opts) => {
281
+ const kbId = await selectBrainId("brain.query", brainSelector(opts));
216
282
  const resp = await apiRequest("POST", "/v1/brain/query", {
217
283
  question,
218
- ...(opts.kb && { knowledge_base_id: opts.kb }),
284
+ ...(kbId && { knowledge_base_id: kbId }),
219
285
  ...(opts.session && { session_id: opts.session }),
220
286
  });
221
287
  handleApiError("brain.query", resp);
@@ -237,7 +303,7 @@ brainCmd
237
303
  related_commands: ["naive brain add", "naive brain docs"],
238
304
  });
239
305
  });
240
- brainCmd
306
+ withBrainFlags(brainCmd
241
307
  .command("add")
242
308
  .description("Ingest a document into the knowledge base (--file, --text, or --url)")
243
309
  .option("--file <path>", "Ingest a local file")
@@ -245,11 +311,11 @@ brainCmd
245
311
  .option("--url <url>", "Ingest the content at a URL")
246
312
  .option("--name <filename>", "Override the stored filename/label")
247
313
  .option("--source <source>", "Provenance tag (e.g. upload, web, agent:researcher)")
248
- .option("--kb <id>", "Target a specific knowledge base")
314
+ .option("--kb <id>", "Target a specific knowledge base"))
249
315
  .action(async (opts) => {
250
316
  const provided = [opts.file, opts.text, opts.url].filter((v) => v !== undefined).length;
251
317
  if (provided !== 1) {
252
- console.error(JSON.stringify({
318
+ console.log(JSON.stringify({
253
319
  success: false,
254
320
  action: "brain.add",
255
321
  error: { code: "missing_argument", message: "Provide exactly one of --file, --text, or --url" },
@@ -261,9 +327,10 @@ brainCmd
261
327
  }, null, 2));
262
328
  process.exit(1);
263
329
  }
330
+ const kbId = await selectBrainId("brain.add", brainSelector(opts));
264
331
  const body = {};
265
- if (opts.kb)
266
- body.knowledge_base_id = opts.kb;
332
+ if (kbId)
333
+ body.knowledge_base_id = kbId;
267
334
  if (opts.source)
268
335
  body.source = opts.source;
269
336
  if (opts.file) {
@@ -302,15 +369,16 @@ brainCmd
302
369
  related_commands: ["naive brain query", "naive brain docs", "naive brain doc"],
303
370
  });
304
371
  });
305
- brainCmd
372
+ withBrainFlags(brainCmd
306
373
  .command("note <fact>")
307
374
  .description("Ingest a short fact as a DOCUMENT (queryable). For semantic memory use 'remember' instead")
308
375
  .option("--source <source>", "Who/what recorded it (default: note)")
309
- .option("--kb <id>", "Target a specific knowledge base")
376
+ .option("--kb <id>", "Target a specific knowledge base"))
310
377
  .action(async (fact, opts) => {
378
+ const kbId = await selectBrainId("brain.note", brainSelector(opts));
311
379
  const body = { text: fact, source: opts.source ?? "note" };
312
- if (opts.kb)
313
- body.knowledge_base_id = opts.kb;
380
+ if (kbId)
381
+ body.knowledge_base_id = kbId;
314
382
  // Document ingest is NOT approval-gated (unlike deletes/forget), so there is
315
383
  // no pending-approval path to handle here.
316
384
  const resp = await apiRequest("POST", "/v1/brain/documents", body);
@@ -336,7 +404,7 @@ brainCmd
336
404
  .action(async (id, opts) => {
337
405
  const provided = [opts.file, opts.text, opts.url].filter((v) => v !== undefined).length;
338
406
  if (provided !== 1) {
339
- console.error(JSON.stringify({
407
+ console.log(JSON.stringify({
340
408
  success: false,
341
409
  action: "brain.replace_doc",
342
410
  error: { code: "missing_argument", message: "Provide exactly one of --file, --text, or --url" },
@@ -385,12 +453,13 @@ brainCmd
385
453
  related_commands: ["naive brain doc", "naive brain docs", "naive brain query"],
386
454
  });
387
455
  });
388
- brainCmd
456
+ withBrainFlags(brainCmd
389
457
  .command("docs")
390
458
  .description("List ingested documents and their processing status")
391
- .option("--kb <id>", "Filter by knowledge base")
459
+ .option("--kb <id>", "Filter by knowledge base"))
392
460
  .action(async (opts) => {
393
- const qs = opts.kb ? `?knowledge_base_id=${encodeURIComponent(opts.kb)}` : "";
461
+ const kbId = await selectBrainId("brain.docs", brainSelector(opts));
462
+ const qs = kbId ? `?knowledge_base_id=${encodeURIComponent(kbId)}` : "";
394
463
  const resp = await apiRequest("GET", `/v1/brain/documents${qs}`);
395
464
  handleApiError("brain.docs", resp);
396
465
  const data = resp.data;
@@ -445,33 +514,181 @@ brainCmd
445
514
  });
446
515
  brainCmd
447
516
  .command("list")
448
- .description("List the company's knowledge bases")
449
- .action(async () => {
517
+ .description("Every brain the company has: which is the default, and which agents are connected")
518
+ .option("--no-agents", "Skip the roster read and report brains only")
519
+ .action(async (opts) => {
450
520
  const resp = await apiRequest("GET", "/v1/brain");
451
521
  handleApiError("brain.list", resp);
452
522
  const data = resp.data;
523
+ const brains = data.knowledge_bases ?? [];
524
+ // THE BINDINGS COLUMN. It is an enrichment of a DIFFERENT surface
525
+ // (the agent roster, behind a different primitive), so a failure to
526
+ // read it must never cost the caller the brain list. The reason rides the
527
+ // envelope; a bare empty column would read as "nothing is connected".
528
+ let bindings = {};
529
+ let unavailable;
530
+ let orphaned = [];
531
+ if (opts.agents === false) {
532
+ unavailable = "not read: --no-agents was passed";
533
+ }
534
+ else {
535
+ const roster = await tryListAgents();
536
+ if (!roster.ok) {
537
+ unavailable = roster.because;
538
+ }
539
+ else {
540
+ const known = new Set(brains.map((b) => b.id));
541
+ for (const a of roster.agents) {
542
+ const bound = boundBrainId(a);
543
+ if (!bound)
544
+ continue;
545
+ if (!known.has(bound)) {
546
+ // A binding that points at a deleted brain is a real state, and
547
+ // silently dropping it would hide it forever: `brain rm` cannot
548
+ // reach into the roster, so the key outlives the row.
549
+ orphaned.push({ agent_id: a.id, agent_name: a.name, knowledge_base_id: bound });
550
+ continue;
551
+ }
552
+ (bindings[bound] ??= []).push({ id: a.id, name: a.name });
553
+ }
554
+ }
555
+ }
556
+ const knowledge_bases = brains.map((b) => ({
557
+ ...b,
558
+ ...(unavailable ? {} : { agents: bindings[b.id] ?? [] }),
559
+ }));
560
+ const defaultBrain = brains.find((b) => b.is_default);
561
+ // Two brains may share a name — the schema has no unique index on
562
+ // (company_id, name) — and when they do, `--brain <that name>` refuses as
563
+ // ambiguous. Reported here because this is the command that already has the
564
+ // whole list in hand; `brain create` deliberately does not pre-flight for it.
565
+ const byName = new Map();
566
+ for (const b of brains) {
567
+ const key = (b.name ?? "").trim().toLowerCase();
568
+ byName.set(key, [...(byName.get(key) ?? []), b.id]);
569
+ }
570
+ const ambiguous = [...byName.entries()]
571
+ .filter(([, ids]) => ids.length > 1)
572
+ .map(([key, ids]) => ({ name: brains.find((b) => b.id === ids[0]).name, ids, normalized: key }));
453
573
  agentOutput({
454
574
  action: "brain.list",
455
- result: resp.data,
575
+ result: {
576
+ knowledge_bases,
577
+ count: data.count ?? brains.length,
578
+ default_knowledge_base_id: defaultBrain?.id ?? null,
579
+ ...(unavailable ? { agents_unavailable_because: unavailable } : {}),
580
+ ...(orphaned.length > 0 ? { orphaned_bindings: orphaned } : {}),
581
+ ...(ambiguous.length > 0 ? { ambiguous_names: ambiguous } : {}),
582
+ },
456
583
  next_steps: [
457
- { command: "naive brain add --file <path>", description: "Ingest a document" },
458
- { command: 'naive brain query "..."', description: "Ask a question" },
459
- { command: "naive brain create --name <name>", description: "Create another knowledge base" },
584
+ { command: "naive brain create <name>", description: "Create another knowledge base" },
585
+ { command: "naive brain connect <brain> --agent <agent>", description: "Connect an agent to one of them" },
586
+ { command: 'naive brain query "..." --brain <name>', description: "Ask a specific brain a question" },
460
587
  ],
461
- hints: [`${data.count ?? 0} knowledge base(s).`],
462
- related_commands: ["naive brain add", "naive brain query", "naive brain docs", "naive brain create"],
588
+ hints: [
589
+ `${data.count ?? brains.length} knowledge base(s).`,
590
+ // "No default" has TWO causes and they need different actions. A company
591
+ // with no brains has simply not needed one — the API mints it on the first
592
+ // call. A company WITH brains and none flagged has rows it can promote in
593
+ // one request, and letting the lazy mint happen instead adds a brain nobody
594
+ // asked for. (Deleting the default no longer produces this state: measured
595
+ // on a booted app, the delete promotes the oldest survivor in the same
596
+ // transaction. An earlier run, before that landed, left zero defaults.)
597
+ defaultBrain
598
+ ? `Default: "${defaultBrain.name}" (${defaultBrain.id}) — used whenever no --brain/--kb/--agent is passed.`
599
+ : brains.length === 0
600
+ ? "No brain is flagged default yet; the API provisions one on the first call that needs it."
601
+ : `No brain is flagged default, but ${brains.length} exist. The next call that names no brain will CREATE one called "Default Brain" and make it the default — run \`naive brain default <name|id>\` first to promote one of the brains above instead.`,
602
+ ...(unavailable ? [`Connected agents not shown: ${unavailable}.`] : []),
603
+ ...(orphaned.length > 0
604
+ ? [`${orphaned.length} agent(s) are connected to a knowledge base that no longer exists — see orphaned_bindings.`]
605
+ : []),
606
+ ...(ambiguous.length > 0
607
+ ? [
608
+ `${ambiguous.length} name(s) are shared by more than one brain (${ambiguous
609
+ .map((a) => `"${a.name}" ×${a.ids.length}`)
610
+ .join(", ")}). --brain <name> refuses those and asks for an id.`,
611
+ ]
612
+ : []),
613
+ ],
614
+ related_commands: ["naive brain create", "naive brain connect", "naive brain add", "naive brain query"],
463
615
  });
464
616
  });
465
617
  brainCmd
466
- .command("create")
618
+ .command("create [name]")
467
619
  .description("Create (and provision) a new knowledge base")
620
+ // `--name` SHIPPED FIRST and stays. The positional is the ergonomic form
621
+ // (`naive brain create "Support"`); passing both is refused rather than
622
+ // guessed at, and passing `--name` alone behaves exactly as it always did.
468
623
  .option("--name <name>", "Name for the knowledge base (1-120 chars)")
469
- .option("--default", "Make this the company's default knowledge base")
470
- .action(async (opts) => {
624
+ .option("--default", "Make this the company's default knowledge base (to promote one that already exists, use `naive brain default`)")
625
+ .action(async (nameArg, opts) => {
626
+ if (nameArg && opts.name && nameArg !== opts.name) {
627
+ agentError({
628
+ action: "brain.create",
629
+ error: {
630
+ code: "invalid_input",
631
+ message: `A brain gets one name. Got "${nameArg}" as an argument and "${opts.name}" as --name.`,
632
+ },
633
+ recovery_steps: [
634
+ { command: `naive brain create "${nameArg}"`, description: "Use the argument" },
635
+ { command: `naive brain create --name "${opts.name}"`, description: "Use the flag" },
636
+ ],
637
+ });
638
+ return;
639
+ }
640
+ const name = nameArg ?? opts.name;
641
+ // NO PRE-FLIGHT LOOKUP HERE, ON PURPOSE. A duplicate-name warning would be
642
+ // worth having — `brain_knowledge_bases` has no unique index on
643
+ // (company_id, name), so two brains may share one and `--brain <name>` then
644
+ // refuses as ambiguous — but detecting it costs a `GET /v1/brain` that
645
+ // `naive brain create --name X` has never made. The invariant test
646
+ // (__tests__/brain-multi.test.ts) compares the WHOLE request stream and
647
+ // catches exactly that. The same fact is reported by `naive brain list` as
648
+ // `ambiguous_names`, which already reads the list.
471
649
  const resp = await apiRequest("POST", "/v1/brain", {
472
- ...(opts.name && { name: opts.name }),
650
+ ...(name && { name }),
473
651
  ...(opts.default && { is_default: true }),
474
652
  });
653
+ // 🔴 A 409 FROM THIS ROUTE DOES NOT MEAN NOTHING WAS CREATED, AND THE FIX IS
654
+ // `naive brain default`, NOT A RETRY.
655
+ //
656
+ // With `is_default: true` the route inserts and provisions the row FIRST and
657
+ // swaps the default AFTER. The swap now runs under a company advisory lock, so
658
+ // concurrent creates no longer lose the race — six concurrent creates with
659
+ // `is_default: true` all returned 201 when measured against a booted app over a
660
+ // real Postgres (scratchpad/default-probe.mts FACT 1, second run). What is left
661
+ // is the narrow case where the SWAP fails: the brain exists, and the server says
662
+ // so — `retryable: false`, `details.knowledge_base_id` naming it, and prose
663
+ // telling the caller to promote it with `PATCH /v1/brain/{id}`.
664
+ //
665
+ // Passing that through verbatim would leave a CLI user holding a raw HTTP call.
666
+ // So this branch keeps the server's code, message and details EXACTLY as sent
667
+ // and only turns the instruction into a command that can be run. Nothing else on
668
+ // this command is intercepted; `handleApiError` still handles every other status.
669
+ const created = resp.data;
670
+ const strandedId = created.error?.details?.knowledge_base_id;
671
+ if (resp.status === 409 && created.error?.code === "duplicate_record" && typeof strandedId === "string") {
672
+ agentError({
673
+ action: "brain.create",
674
+ error: {
675
+ code: "duplicate_record",
676
+ message: created.error.message ?? "The knowledge base was created but could not be made the default.",
677
+ ...(created.error.details ? { details: created.error.details } : {}),
678
+ },
679
+ recovery_steps: [
680
+ {
681
+ command: `naive brain default ${strandedId}`,
682
+ description: "THE BRAIN EXISTS. Only the default swap failed, so promote it — one request, no new brain.",
683
+ },
684
+ {
685
+ command: "naive brain list",
686
+ description: "Confirm it is there and see which brain currently holds the default.",
687
+ },
688
+ ],
689
+ });
690
+ return;
691
+ }
475
692
  handleApiError("brain.create", resp);
476
693
  const data = resp.data;
477
694
  agentOutput({
@@ -479,7 +696,13 @@ brainCmd
479
696
  result: resp.data,
480
697
  next_steps: [
481
698
  ...(data.id
482
- ? [{ command: `naive brain add --file <path> --kb ${data.id}`, description: "Ingest a document into this knowledge base" }]
699
+ ? [
700
+ // The ID, not the name: names are not unique, and a next step that
701
+ // may be ambiguous the moment a second brain shares the name is
702
+ // not a next step.
703
+ { command: `naive brain add --file <path> --brain ${data.id}`, description: "Ingest a document into this knowledge base" },
704
+ { command: `naive brain connect ${data.id} --agent <agent>`, description: "Connect an agent to it" },
705
+ ]
483
706
  : []),
484
707
  { command: "naive brain list", description: "List all knowledge bases" },
485
708
  ],
@@ -487,12 +710,226 @@ brainCmd
487
710
  `Knowledge base '${data.name ?? "?"}' created (status: ${data.status ?? "provisioning"}).`,
488
711
  ...(data.is_default ? ["This is now the company's default knowledge base."] : []),
489
712
  ],
490
- related_commands: ["naive brain list", "naive brain add", "naive brain rm"],
713
+ related_commands: ["naive brain list", "naive brain default", "naive brain connect", "naive brain add", "naive brain rm"],
714
+ });
715
+ });
716
+ /* ══════════════════════════════════════════════════════════════════════════
717
+ * CHOOSING THE DEFAULT — the operation that makes a wrong default recoverable
718
+ *
719
+ * Until `PATCH /v1/brain/:id` landed, `is_default` was settable at create and
720
+ * nowhere else, so an operator who ended up with the wrong default could only
721
+ * create ANOTHER brain to move it — and never re-point at one they already had.
722
+ * Measured then, against a booted app with a key that read `GET /v1/brain` 200:
723
+ * PATCH/PUT /v1/brain/{id}, POST /v1/brain/{id}/default, POST /v1/brain/default
724
+ * and PATCH /v1/brain all answered 404. Re-measured after the route landed: the
725
+ * PATCH answers 200 and the other four still 404.
726
+ *
727
+ * WHY A POSITIONAL AND NOT `--default` ON SOMETHING ELSE. This is one act on one
728
+ * brain, and the brain is the subject: `naive brain default "Support Brain"`.
729
+ * It takes the same `<name|id>` every other subcommand's `--brain` takes, through
730
+ * the same resolver, so a name that two brains share is refused with both ids
731
+ * rather than resolved to one of them.
732
+ *
733
+ * NO `--because`. The route takes exactly one body property (`is_default: true`)
734
+ * and the server writes its own audit event (`brain.kb.default_changed`, carrying
735
+ * the previous id); a reason this CLI invented would be dropped on the floor.
736
+ * ══════════════════════════════════════════════════════════════════════════ */
737
+ brainCmd
738
+ .command("default <brain>")
739
+ .description("Make an existing brain the company default (by name or id)")
740
+ .action(async (ref) => {
741
+ const id = await resolveBrainId("brain.default", ref);
742
+ const resp = await apiRequest("PATCH", `/v1/brain/${id}`, { is_default: true });
743
+ handleApiError("brain.default", resp);
744
+ const data = resp.data;
745
+ agentOutput({
746
+ action: "brain.default",
747
+ result: resp.data,
748
+ next_steps: [
749
+ { command: "naive brain list", description: "Confirm which brain is now the default" },
750
+ { command: `naive brain query "..." --brain ${data.id ?? id}`, description: "Ask this brain directly" },
751
+ ],
752
+ hints: [
753
+ `"${data.name ?? ref}" (${data.id ?? id}) is now the company default — used whenever no --brain/--kb/--agent is passed.`,
754
+ // Both of these are the server's rules, not this CLI's, and both are
755
+ // states a caller can reach: promoting the current default is a no-op,
756
+ // and a brain still provisioning is refused rather than promoted half-built.
757
+ "Promoting the brain that is already the default is a no-op and writes no audit event.",
758
+ "Only an active brain can be promoted; a provisioning or errored one is refused with invalid_input.",
759
+ ],
760
+ related_commands: ["naive brain list", "naive brain create", "naive brain connect"],
761
+ });
762
+ });
763
+ /* ══════════════════════════════════════════════════════════════════════════
764
+ * CONNECTING A BRAIN TO AN AGENT
765
+ *
766
+ * WHAT THIS IS, EXACTLY — because the difference matters and nothing else in
767
+ * the product states it. MEASURED in this tree:
768
+ *
769
+ * - NO TABLE binds an agent to a `brain_knowledge_bases` row. The
770
+ * `knowledge_base_id` FK appears on documents, episodes, entities, claims
771
+ * and ops rows — never on an agent, a profile or a tenant user. The binding
772
+ * is a KEY IN `agents.metadata`, not a column.
773
+ * - THREE ROUTES own it, all under the `brain` primitive gate:
774
+ * `POST /v1/brain/connect`, `DELETE /v1/brain/connections/:agentId` and
775
+ * `GET /v1/brain/connections` (packages/api/src/routes/brain.ts:1251-1277,
776
+ * of 42 `brainRouter` routes).
777
+ * - The one place the platform accepts "this work uses that brain" per RUN is
778
+ * `POST /v1/runs`, whose body carries `brain_knowledge_base_id`
779
+ * (contracts/brain-ops.ts:98) next to `agent_id`. The connect routes write
780
+ * the key that surface already consumes, rather than minting a second name.
781
+ *
782
+ * 🔴 WHAT THIS COMMAND USED TO DO, AND WHY IT DOES NOT ANY MORE. It wrote the
783
+ * binding with `PATCH {@link ROSTER_PATH}/:id {metadata:{…}}` — the roster route,
784
+ * mounted on the ROSTER's own primitive gate. That is a SECOND DOOR to
785
+ * brain-owned data past the brain gate, which is precisely what
786
+ * `packages/api/src/routes/brain.ts:17-22` says must not exist. Measured live on
787
+ * one tenant, one agent, one field: `POST …/brain/connect` → 403
788
+ * `subprocessor_consent_required`; the roster PATCH of the same key on the same
789
+ * agent → 200. Three things were also silently skipped by the roster door — the
790
+ * `brain.agent.connected` `activity_events` row, the `brain_connected_at` stamp
791
+ * (so `GET /v1/brain/connections` reported a timestamp from an EARLIER binding
792
+ * to a DIFFERENT brain), and atomicity (it read-modify-wrote the whole metadata
793
+ * bag). All three come back with the gated route.
794
+ *
795
+ * AND HERE IS WHAT IT IS NOT: it is not an enforcement point. Nothing in the
796
+ * API reads this key to CONSTRAIN what an agent may recall — an agent key that
797
+ * can reach `/v1/brain` can still name any of its company's knowledge bases.
798
+ * The binding is a DEFAULT, honoured by every `naive brain … --agent <a>` call
799
+ * in this CLI. Saying otherwise would describe governance this build does not
800
+ * have. Governance of the brain is the AccountKit's `brain` primitive and the
801
+ * statute gates on `brain.kb.delete` / `brain.forget` / `brain.proposal.accept`,
802
+ * and this command changes neither.
803
+ * ════════════════════════════════════════════════════════════════════════ */
804
+ brainCmd
805
+ .command("connect <brain>")
806
+ .description("Connect an agent to a brain — that agent's default knowledge base")
807
+ .requiredOption("--agent <id|name>", "The agent to connect (id, display name, or runtime profile name)")
808
+ .addHelpText("after", `
809
+ Examples:
810
+ $ naive brain connect "Support Brain" --agent "Support Sam"
811
+ $ naive brain connect 00a21080-e247-4935-8efb-a6c1051c189b --agent c62c69e3-69aa-4136-b571-fded6ce5a28b
812
+
813
+ Then, anywhere a brain is named:
814
+ $ naive brain attach "refund audit" --agent "Support Sam"
815
+ $ naive brain recall "refunds" --agent "Support Sam"
816
+
817
+ The binding is a DEFAULT, not a fence: it decides which brain this CLI reads for
818
+ that agent. It does not stop an agent key from naming another knowledge base —
819
+ that is the AccountKit's job, and this command does not touch it.
820
+ `)
821
+ .action(async (brainRef, opts) => {
822
+ const action = "brain.connect";
823
+ const kbId = await resolveBrainId(action, brainRef);
824
+ const brain = (await listBrains(action)).find((b) => b.id === kbId);
825
+ const agent = await resolveAgent(action, opts.agent);
826
+ const previous = boundBrainId(agent);
827
+ const { agent: updated, connection } = await setAgentBrain(action, agent, kbId);
828
+ agentOutput({
829
+ action,
830
+ result: {
831
+ agent: { id: agent.id, name: agent.name },
832
+ knowledge_base: { id: kbId, name: connection?.knowledge_base_name ?? brain?.name ?? null },
833
+ previous_knowledge_base_id: previous ?? null,
834
+ binding_key: BRAIN_BINDING_KEY,
835
+ // The route's own timestamp, not one this CLI invented. Reported because
836
+ // it is what `naive brain list` and `GET /v1/brain/connections` show, and
837
+ // because the roster PATCH this replaced left it stale or null.
838
+ connected_at: connection?.connected_at ?? null,
839
+ metadata: updated.metadata ?? null,
840
+ },
841
+ next_steps: bindingNextSteps(brain, agent.id),
842
+ hints: [
843
+ `${agent.name} → "${connection?.knowledge_base_name ?? brain?.name ?? kbId}".`,
844
+ ...(previous && previous !== kbId ? [`Replaced its previous binding (${previous}).`] : []),
845
+ ...(previous === kbId ? ["It was already connected to this brain; the binding was re-pointed and `connected_at` refreshed."] : []),
846
+ `Written by POST ${BRAIN_CONNECT_PATH}, which carries the brain primitive gate and records a \`brain.agent.connected\` ledger event.`,
847
+ `Stored on the agent as metadata.${BRAIN_BINDING_KEY} — the same field POST /v1/runs accepts to point a run's brain attach.`,
848
+ "This is the default this CLI uses for --agent. It is not a permission boundary.",
849
+ ],
850
+ related_commands: ["naive brain list", "naive brain disconnect", "naive brain attach"],
851
+ });
852
+ });
853
+ brainCmd
854
+ .command("disconnect [brain]")
855
+ .description("Disconnect an agent from its brain (the inverse of 'connect')")
856
+ .requiredOption("--agent <id|name>", "The agent to disconnect (id, display name, or runtime profile name)")
857
+ .addHelpText("after", `
858
+ Examples:
859
+ $ naive brain disconnect --agent "Support Sam"
860
+ $ naive brain disconnect "Support Brain" --agent "Support Sam" # refuses if it is bound elsewhere
861
+
862
+ Naming the brain is optional and is an ASSERTION: if the agent is connected to a
863
+ different one, the command refuses rather than clearing a binding you did not
864
+ mean to clear. Afterwards the agent falls back to the company default brain.
865
+ `)
866
+ .action(async (brainRef, opts) => {
867
+ const action = "brain.disconnect";
868
+ const agent = await resolveAgent(action, opts.agent);
869
+ const bound = boundBrainId(agent);
870
+ if (!bound) {
871
+ agentOutput({
872
+ action,
873
+ result: { agent: { id: agent.id, name: agent.name }, knowledge_base: null, changed: false },
874
+ next_steps: [{ command: `naive brain connect <brain> --agent ${agent.id}`, description: "Connect it to a brain" }],
875
+ hints: [`${agent.name} was not connected to a brain. Nothing to do.`],
876
+ related_commands: ["naive brain list", "naive brain connect"],
877
+ });
878
+ return;
879
+ }
880
+ if (brainRef) {
881
+ const asserted = await resolveBrainId(action, brainRef);
882
+ if (asserted !== bound) {
883
+ const brains = await listBrains(action);
884
+ const actual = brains.find((b) => b.id === bound);
885
+ agentError({
886
+ action,
887
+ error: {
888
+ code: "invalid_input",
889
+ message: `${agent.name} is connected to ${actual ? `"${actual.name}" (${bound})` : bound}, not to "${brainRef}". Nothing was changed.`,
890
+ agent: { id: agent.id, name: agent.name },
891
+ knowledge_base_id: bound,
892
+ },
893
+ recovery_steps: [
894
+ { command: `naive brain disconnect --agent ${agent.id}`, description: "Disconnect it from whatever it is bound to" },
895
+ { command: "naive brain list", description: "See every brain and what is bound to each" },
896
+ ],
897
+ });
898
+ return;
899
+ }
900
+ }
901
+ const brains = await listBrains(action);
902
+ const was = brains.find((b) => b.id === bound);
903
+ const { agent: updated } = await setAgentBrain(action, agent, null);
904
+ agentOutput({
905
+ action,
906
+ result: {
907
+ agent: { id: agent.id, name: agent.name },
908
+ disconnected_from: { id: bound, name: was?.name ?? null },
909
+ knowledge_base: null,
910
+ changed: true,
911
+ metadata: updated.metadata ?? null,
912
+ },
913
+ next_steps: [
914
+ { command: `naive brain connect <brain> --agent ${agent.id}`, description: "Connect it to a different brain" },
915
+ { command: "naive brain list", description: "See every brain and what is bound to each" },
916
+ ],
917
+ hints: [
918
+ `${agent.name} is no longer connected to "${was?.name ?? bound}".`,
919
+ "Its brain calls fall back to the company default, exactly as before it was connected.",
920
+ "The knowledge base itself is untouched — this removed a binding, not a brain. Use 'naive brain rm <id>' to delete one.",
921
+ ],
922
+ related_commands: ["naive brain connect", "naive brain list", "naive brain rm"],
491
923
  });
492
924
  });
493
925
  brainCmd
494
926
  .command("rm <id>")
495
927
  .description("Delete a knowledge base and all its documents (permanent)")
928
+ // STILL NO PRE-FLIGHT READ, for the reason `create` gives: a shipped
929
+ // invocation that made one request may not start making two. What changed is
930
+ // what the RESULT says. The route answers 204 with an empty body, and two of
931
+ // its consequences are invisible in that emptiness — both measured, both named
932
+ // here, and both reported by `naive brain list`, which is already the next step.
496
933
  .action(async (id) => {
497
934
  const resp = await apiRequest("DELETE", `/v1/brain/${id}`);
498
935
  handleApiError("brain.rm", resp);
@@ -501,9 +938,331 @@ brainCmd
501
938
  agentOutput({
502
939
  action: "brain.rm",
503
940
  result: resp.data ?? { deleted: true, id },
504
- next_steps: [{ command: "naive brain list", description: "List remaining knowledge bases" }],
505
- hints: ["Knowledge base and its documents deleted."],
506
- related_commands: ["naive brain list", "naive brain create"],
941
+ next_steps: [
942
+ {
943
+ command: "naive brain list",
944
+ description: "List remaining knowledge bases — and check `default_knowledge_base_id` and `orphaned_bindings`, " +
945
+ "which are the two things this delete can change without saying so.",
946
+ },
947
+ ],
948
+ hints: [
949
+ "Knowledge base and its documents deleted.",
950
+ // MEASURED on a booted app over a real Postgres (default-probe.mts FACT 3,
951
+ // second run): deleting the default answered 204 and the company still held
952
+ // exactly one default — the delete promotes the oldest surviving brain in
953
+ // the same transaction. An earlier run of the same probe, before that
954
+ // landed, left ZERO defaults and the next unscoped call minted a brain.
955
+ // The 204 body is empty and names no successor, so the only way to learn
956
+ // which brain it is, is to look.
957
+ "If this was the DEFAULT brain, another was promoted in the same transaction — the oldest surviving one. The 204 cannot say which: `naive brain list` shows it, and `naive brain default <brain>` picks a different one.",
958
+ // MEASURED this session: DELETE does not clear
959
+ // agents.metadata.brain_knowledge_base_id, so a connected agent keeps
960
+ // pointing at the dead id and its next scoped call 404s.
961
+ "Agents connected to it are NOT disconnected — they keep pointing at the deleted id and their scoped calls will 404. `naive brain list` reports them as `orphaned_bindings`; `naive brain disconnect --agent <id>` clears one.",
962
+ ],
963
+ related_commands: ["naive brain list", "naive brain create", "naive brain disconnect"],
964
+ });
965
+ });
966
+ /* ========================================================================== */
967
+ /* The durable-runtime era additions (06 §1.7). */
968
+ /* */
969
+ /* §1.7 specifies FOURTEEN new subcommands: */
970
+ /* attach beliefs belief reaffirm lessons learn proposals writebacks */
971
+ /* retention levels status metrics consolidate decisions */
972
+ /* */
973
+ /* SIX are shipped here. MEASURED against packages/api/src/routes/brain.ts in */
974
+ /* this tree (37 handlers, enumerated), these six and only these six have a */
975
+ /* route to reach: */
976
+ /* */
977
+ /* attach POST /v1/brain/attach (:454) */
978
+ /* consolidate POST /v1/brain/consolidate (:465) */
979
+ /* proposals GET /v1/brain/proposals (:637, :649) */
980
+ /* writebacks GET /v1/brain/writebacks (:617, :629) */
981
+ /* status GET /v1/brain/status (:350) */
982
+ /* metrics GET /v1/brain/metrics (:750) */
983
+ /* */
984
+ /* The other EIGHT — beliefs, belief, reaffirm, lessons, learn, retention, */
985
+ /* levels, decisions — have no handler on `brainRouter` and no table behind */
986
+ /* them. They are deliberately NOT shipped. `naive brain` is a working group */
987
+ /* with sixteen working subcommands; adding eight that can only ever 404 */
988
+ /* degrades a live surface, and a subcommand whose only possible outcome is an */
989
+ /* error is not a command, it is a promise. They land with their routes. */
990
+ /* */
991
+ /* `proposals [id]` is one name at two scopes rather than a `proposals` / */
992
+ /* `proposal` pair — the same shape the group already uses for a list and its */
993
+ /* member elsewhere, and one fewer name to keep in step. */
994
+ /* ========================================================================== */
995
+ /** Collect a repeatable option into an array (commander's documented idiom). */
996
+ function collect(value, previous = []) {
997
+ return [...previous, value];
998
+ }
999
+ withBrainFlags(brainCmd
1000
+ .command("attach [goal]")
1001
+ .description("Attach a read capsule for a goal — what the brain already knows, before the work starts")
1002
+ .option("--goal <goal>", "The goal (alternative to the positional argument)")
1003
+ .option("--entity <name>", "An entity the work touches (repeatable, max 20)", collect)
1004
+ .option("--artifact <ref>", "An artifact the work touches (repeatable, max 50)", collect)
1005
+ .option("--since <iso8601>", "Only what changed since this watermark")
1006
+ .option("--kb <id>", "Attach from a specific knowledge base"))
1007
+ .action(async (goalArg, opts) => {
1008
+ const goal = goalArg ?? opts.goal;
1009
+ if (!goal) {
1010
+ agentError({
1011
+ action: "brain.attach",
1012
+ error: { code: "invalid_input", message: "A goal is required — pass it positionally or with --goal" },
1013
+ recovery_steps: [
1014
+ { command: 'naive brain attach "refund audit"', description: "Positional form" },
1015
+ { command: 'naive brain attach --goal "refund audit"', description: "Flag form (same call)" },
1016
+ ],
1017
+ });
1018
+ return;
1019
+ }
1020
+ // `--agent <a>` here is the payoff of `brain connect`: the run loop's read
1021
+ // half runs against the brain that agent is bound to, without the caller
1022
+ // having to carry the id.
1023
+ const kbId = await selectBrainId("brain.attach", brainSelector(opts));
1024
+ const resp = await apiRequest("POST", "/v1/brain/attach", {
1025
+ goal,
1026
+ ...(opts.entity && { entities: opts.entity }),
1027
+ ...(opts.artifact && { artifacts_touched: opts.artifact }),
1028
+ ...(opts.since && { since_watermark: opts.since }),
1029
+ ...(kbId && { knowledge_base_id: kbId }),
1030
+ });
1031
+ handleApiError("brain.attach", resp);
1032
+ agentOutput({
1033
+ action: "brain.attach",
1034
+ result: resp.data,
1035
+ next_steps: [
1036
+ { command: 'naive brain consolidate --objective "..."', description: "Debrief at the end of the run" },
1037
+ { command: "naive brain proposals", description: "See what the run proposed writing back" },
1038
+ ],
1039
+ hints: ["A capsule is what the brain already knows about this goal. Read it before working, not after."],
1040
+ related_commands: ["naive brain consolidate", "naive brain recall", "naive brain proposals"],
1041
+ });
1042
+ });
1043
+ withBrainFlags(brainCmd
1044
+ .command("consolidate")
1045
+ .description("Run-end debrief — the write half. What was learned, decided and left open")
1046
+ .option("--objective <text>", "What the run was for")
1047
+ .option("--learned <s|p|o>", "A learned fact as subject|predicate|object (repeatable, max 100)", collect)
1048
+ .option("--decided <title>", "A decision the run made (repeatable, max 50)", collect)
1049
+ .option("--open <question>", "A question the run left open (repeatable, max 50)", collect)
1050
+ .option("--kb <id>", "Consolidate into a specific knowledge base"))
1051
+ .action(async (opts) => {
1052
+ // `--learned` is pipe-separated because the API's shape is a TRIPLE, and a
1053
+ // free-text sentence would have to be parsed into one by something. Refusing
1054
+ // a malformed triple here is better than posting a fact with an empty object.
1055
+ const learned = (opts.learned ?? []).map((raw) => {
1056
+ const [subject, predicate, object] = raw.split("|");
1057
+ if (!subject || !predicate || !object) {
1058
+ agentError({
1059
+ action: "brain.consolidate",
1060
+ error: {
1061
+ code: "invalid_input",
1062
+ message: `--learned expects subject|predicate|object, got: ${raw}`,
1063
+ },
1064
+ recovery_steps: [
1065
+ { command: 'naive brain consolidate --learned "acme|refund window|30 days"', description: "Use the triple form" },
1066
+ ],
1067
+ });
1068
+ }
1069
+ return { subject: subject, predicate: predicate, object: object };
1070
+ });
1071
+ const kbId = await selectBrainId("brain.consolidate", brainSelector(opts));
1072
+ const resp = await apiRequest("POST", "/v1/brain/consolidate", {
1073
+ ...(opts.objective && { objective: opts.objective }),
1074
+ ...(learned.length > 0 && { learned }),
1075
+ ...(opts.decided && { decided: opts.decided.map((title) => ({ title })) }),
1076
+ ...(opts.open && { open: opts.open }),
1077
+ ...(kbId && { knowledge_base_id: kbId }),
1078
+ });
1079
+ handleApiError("brain.consolidate", resp);
1080
+ const data = resp.data;
1081
+ agentOutput({
1082
+ action: "brain.consolidate",
1083
+ result: resp.data,
1084
+ next_steps: [
1085
+ { command: "naive brain proposals --status pending", description: "Review what this debrief proposed" },
1086
+ { command: "naive brain writebacks", description: "See the envelope this created" },
1087
+ ],
1088
+ hints: [
1089
+ `Accepted (202). Gateway mode: ${data.mode ?? "unknown"}; ${data.proposals?.length ?? 0} proposal(s).`,
1090
+ "A debrief PROPOSES; it does not commit. Nothing here is a belief until a proposal is accepted.",
1091
+ ],
1092
+ related_commands: ["naive brain proposals", "naive brain writebacks", "naive brain attach"],
1093
+ });
1094
+ });
1095
+ brainCmd
1096
+ .command("proposals [id]")
1097
+ .description("The proposal queue — list it, read one, or decide one")
1098
+ .option("--status <status>", "Filter by proposal status")
1099
+ .option("--type <objectType>", "Filter by proposed object type")
1100
+ .option("--run <id>", "Filter to one run")
1101
+ .option("--limit <n>", "Maximum proposals to return (1-200)", (v) => Number.parseInt(v, 10))
1102
+ .option("--events", "With <id>: show the proposal's event history instead of the proposal")
1103
+ .option("--accept", "With <id>: accept it (may require human approval)")
1104
+ .option("--reject", "With <id>: reject it")
1105
+ .option("--merge <targetId>", "With <id>: merge it into an existing object")
1106
+ .option("--quarantine", "With <id>: quarantine it")
1107
+ .option("--because <why>", "Why you decided as you did. Recorded on the proposal")
1108
+ .action(async (id, opts) => {
1109
+ const decisions = [
1110
+ opts.accept && "accept",
1111
+ opts.reject && "reject",
1112
+ opts.merge && "merge",
1113
+ opts.quarantine && "quarantine",
1114
+ ].filter(Boolean);
1115
+ if (decisions.length > 0 && !id) {
1116
+ agentError({
1117
+ action: "brain.proposals",
1118
+ error: { code: "invalid_input", message: `--${decisions[0]} needs a proposal id: naive brain proposals <id> --${decisions[0]}` },
1119
+ recovery_steps: [{ command: "naive brain proposals --status pending", description: "List proposals awaiting a decision" }],
1120
+ });
1121
+ return;
1122
+ }
1123
+ if (decisions.length > 1) {
1124
+ agentError({
1125
+ action: "brain.proposals",
1126
+ error: { code: "invalid_input", message: `A proposal gets one decision, not ${decisions.length}: ${decisions.join(", ")}` },
1127
+ recovery_steps: [{ command: "naive brain proposals <id>", description: "Read the proposal before deciding" }],
1128
+ });
1129
+ return;
1130
+ }
1131
+ if (decisions.length === 1) {
1132
+ const verb = decisions[0];
1133
+ const resp = await apiRequest("POST", `/v1/brain/proposals/${encodeURIComponent(id)}/${verb}`, {
1134
+ ...(opts.because && { reason: opts.because }),
1135
+ ...(opts.merge && { merge_target_id: opts.merge }),
1136
+ });
1137
+ handleApiError(`brain.proposals.${verb}`, resp);
1138
+ if (handlePendingApproval(`brain.proposals.${verb}`, resp))
1139
+ return;
1140
+ agentOutput({
1141
+ action: `brain.proposals.${verb}`,
1142
+ result: resp.data,
1143
+ next_steps: [{ command: "naive brain proposals --status pending", description: "Review the rest of the queue" }],
1144
+ hints: [
1145
+ `Proposal ${id} ${verb}ed.`,
1146
+ ...(verb === "accept" ? ["Accepting is a governed action — it may return pending_approval instead of promoting."] : []),
1147
+ ],
1148
+ related_commands: ["naive brain proposals", "naive brain writebacks"],
1149
+ });
1150
+ return;
1151
+ }
1152
+ if (id) {
1153
+ const suffix = opts.events ? "/events" : "";
1154
+ const resp = await apiRequest("GET", `/v1/brain/proposals/${encodeURIComponent(id)}${suffix}`);
1155
+ handleApiError("brain.proposals", resp);
1156
+ agentOutput({
1157
+ action: "brain.proposals",
1158
+ result: resp.data,
1159
+ next_steps: [
1160
+ { command: `naive brain proposals ${id} --accept --because "..."`, description: "Accept it" },
1161
+ { command: `naive brain proposals ${id} --reject --because "..."`, description: "Reject it" },
1162
+ { command: `naive brain proposals ${id} --events`, description: "See its history" },
1163
+ ],
1164
+ related_commands: ["naive brain proposals", "naive brain writebacks"],
1165
+ });
1166
+ return;
1167
+ }
1168
+ const params = new URLSearchParams();
1169
+ if (opts.status)
1170
+ params.set("status", opts.status);
1171
+ if (opts.type)
1172
+ params.set("object_type", opts.type);
1173
+ if (opts.run)
1174
+ params.set("run_id", opts.run);
1175
+ if (opts.limit !== undefined)
1176
+ params.set("limit", String(opts.limit));
1177
+ const qs = params.toString();
1178
+ const resp = await apiRequest("GET", `/v1/brain/proposals${qs ? `?${qs}` : ""}`);
1179
+ handleApiError("brain.proposals", resp);
1180
+ const data = resp.data;
1181
+ agentOutput({
1182
+ action: "brain.proposals",
1183
+ result: resp.data,
1184
+ next_steps: [
1185
+ { command: "naive brain proposals <id>", description: "Read one proposal" },
1186
+ { command: 'naive brain proposals <id> --accept --because "..."', description: "Accept one" },
1187
+ ],
1188
+ hints: [`${data.count ?? 0} proposal(s).`],
1189
+ related_commands: ["naive brain writebacks", "naive brain consolidate"],
1190
+ });
1191
+ });
1192
+ brainCmd
1193
+ .command("writebacks [id]")
1194
+ .description("Writeback envelopes — what a run asked the brain to record")
1195
+ .option("--run <id>", "Filter to one run")
1196
+ .option("--source <source>", "Filter by source")
1197
+ .option("--status <status>", "Filter by envelope status")
1198
+ .option("--limit <n>", "Maximum envelopes to return (1-200)", (v) => Number.parseInt(v, 10))
1199
+ .action(async (id, opts) => {
1200
+ if (id) {
1201
+ const resp = await apiRequest("GET", `/v1/brain/writebacks/${encodeURIComponent(id)}`);
1202
+ handleApiError("brain.writebacks", resp);
1203
+ agentOutput({
1204
+ action: "brain.writebacks",
1205
+ result: resp.data,
1206
+ next_steps: [{ command: "naive brain proposals --status pending", description: "Decide the proposals it raised" }],
1207
+ related_commands: ["naive brain proposals", "naive brain consolidate"],
1208
+ });
1209
+ return;
1210
+ }
1211
+ const params = new URLSearchParams();
1212
+ if (opts.run)
1213
+ params.set("run_id", opts.run);
1214
+ if (opts.source)
1215
+ params.set("source", opts.source);
1216
+ if (opts.status)
1217
+ params.set("status", opts.status);
1218
+ if (opts.limit !== undefined)
1219
+ params.set("limit", String(opts.limit));
1220
+ const qs = params.toString();
1221
+ const resp = await apiRequest("GET", `/v1/brain/writebacks${qs ? `?${qs}` : ""}`);
1222
+ handleApiError("brain.writebacks", resp);
1223
+ const data = resp.data;
1224
+ agentOutput({
1225
+ action: "brain.writebacks",
1226
+ result: resp.data,
1227
+ next_steps: [{ command: "naive brain writebacks <id>", description: "Read one envelope and its proposals" }],
1228
+ hints: [`${data.count ?? 0} envelope(s).`],
1229
+ related_commands: ["naive brain proposals", "naive brain consolidate"],
1230
+ });
1231
+ });
1232
+ brainCmd
1233
+ .command("status")
1234
+ .description("Operator view of the brain runtime for this company")
1235
+ .action(async () => {
1236
+ const resp = await apiRequest("GET", "/v1/brain/status");
1237
+ handleApiError("brain.status", resp);
1238
+ agentOutput({
1239
+ action: "brain.status",
1240
+ result: resp.data,
1241
+ next_steps: [
1242
+ { command: "naive brain metrics", description: "Process-global nervous-system counters" },
1243
+ { command: "naive brain list", description: "The company's knowledge bases" },
1244
+ ],
1245
+ related_commands: ["naive brain metrics", "naive brain list", "naive brain docs"],
1246
+ });
1247
+ });
1248
+ brainCmd
1249
+ .command("metrics")
1250
+ .description("Nervous-system counters. NOT company-scoped — see the hint")
1251
+ .action(async () => {
1252
+ const resp = await apiRequest("GET", "/v1/brain/metrics");
1253
+ handleApiError("brain.metrics", resp);
1254
+ agentOutput({
1255
+ action: "brain.metrics",
1256
+ result: resp.data,
1257
+ next_steps: [{ command: "naive brain status", description: "The company-scoped view" }],
1258
+ hints: [
1259
+ // MEASURED: routes/brain.ts:750 calls getCompanyId(req) as an AUTH GATE ONLY and
1260
+ // then returns snapshotBrainMetrics(), which takes no company and reads
1261
+ // process-wide counters (platform/brain/src/brain-metrics.ts:73). Saying so is
1262
+ // the difference between a number an operator can act on and one they cannot.
1263
+ "These counters are PROCESS-GLOBAL, not company-scoped: they aggregate every tenant served by the API process that answered. Use `naive brain status` for this company.",
1264
+ ],
1265
+ related_commands: ["naive brain status"],
507
1266
  });
508
1267
  });
509
1268
  //# sourceMappingURL=brain.js.map