quoxcli 0.0.0-stage → 0.3.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 (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,1085 @@
1
+ /**
2
+ * Brain commands: search, status, vaults, notes (Quox Qortex)
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
6
+ import { execFileSync } from 'child_process';
7
+ import { homedir } from 'os';
8
+ import { dirname, join } from 'path';
9
+ import { Option } from 'commander';
10
+ import { buildBatchArchive } from './notesBatch.js';
11
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
12
+ import { QuoxApiError, QuoxError } from '../core/errors.js';
13
+ import { resolveEndpoint } from '../api/endpoints.js';
14
+ import { line as glyphLine, brandName } from '../core/quoxglyph.js';
15
+ import { resolveOrgId } from './helpers.js';
16
+
17
+ // Which surface a hit came from, in words a person recognises.
18
+ function recallSource(r) {
19
+ if (r.kind === 'transcript_session') return 'past session';
20
+ if (r.kind === 'quoxplan_note' || r.kind === 'quoxplan_stream') return 'QuoxPlan';
21
+ if (r.kind === 'commit') return 'git history';
22
+ if (r.kind === 'code') return 'codebase';
23
+ if (r.kind === 'memory') return 'QuoxMemory';
24
+ if (r.kind === 'tasks_execution_wisdom') return 'execution wisdom';
25
+ if ((r.relPath || '').startsWith('memory/')) return 'saved memory';
26
+ return r.kind === 'note' ? 'knowledge brain' : (r.kind || 'knowledge brain');
27
+ }
28
+
29
+ // A readable one-line snippet: strip FTS highlight brackets, YAML
30
+ // frontmatter scaffolding, and a leading echo of the title.
31
+ function recallSnippet(r) {
32
+ let s = (r.snippet || '').replace(/\[([^\]]*)\]/g, '$1');
33
+ const desc = s.match(/description:\s*(.+)/);
34
+ if (desc) s = desc[1];
35
+ else {
36
+ s = s.replace(/^---$|^name:.*$/gm, ' ');
37
+ if (r.title && s.startsWith(r.title)) s = s.slice(r.title.length);
38
+ }
39
+ s = s.replace(/\s+/g, ' ').trim().replace(/"/g, "'");
40
+ return s.length > 110 ? `${s.slice(0, 107)}...` : (s || r.title || '');
41
+ }
42
+
43
+ const slug = (t) => String(t || 'untitled').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 50) || 'untitled';
44
+
45
+ const words = (t) => new Set(String(t || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2));
46
+ /** Share of the smaller word set found in the other (0..1); the decide dedupe score. */
47
+ export function titleOverlap(a, b) {
48
+ const x = words(a); const y = words(b);
49
+ if (!x.size || !y.size) return 0;
50
+ let hit = 0;
51
+ for (const w of x) if (y.has(w)) hit++;
52
+ return hit / Math.min(x.size, y.size);
53
+ }
54
+ const NEGATIVE_WORDS = new Set(['disable', 'disabled', 'disabling', 'off', 'deny', 'denied', 'block', 'blocked', 'stop', 'stopped', 'remove', 'removed', 'drop', 'dropped', 'abandon', 'abandoned', 'reject', 'rejected', 'exclude', 'excluded', 'not', 'never', 'no', 'dont', 'cannot', 'cant', 'wont', 'without', 'revoke', 'revoked']);
55
+ /** Odd count of negating words = negative polarity ("not", "stop", "disable", "reject", ...). */
56
+ function negativeParity(t) {
57
+ const toks = String(t || '').toLowerCase().replace(/['\u2019]/g, '').split(/[^a-z0-9]+/).filter(Boolean);
58
+ let n = 0;
59
+ for (const w of toks) if (NEGATIVE_WORDS.has(w)) n++;
60
+ return n % 2;
61
+ }
62
+ /** True when two decision titles read as opposites (enable/disable, on/off, stop X, no longer X...). */
63
+ export function oppositePolarity(a, b) {
64
+ return negativeParity(a) !== negativeParity(b);
65
+ }
66
+ export const DECIDE_DUPE_THRESHOLD = 0.6;
67
+ export const DISTILL_CAP = 50;
68
+
69
+ function gitOut(args) {
70
+ try { return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return ''; }
71
+ }
72
+
73
+ /** Where and when a decision was made: time, repo + commit, stream, held slot. */
74
+ export function decisionProvenance(now = new Date()) {
75
+ const prov = { at: now.toISOString() };
76
+ const top = gitOut(['rev-parse', '--show-toplevel']);
77
+ if (top) {
78
+ prov.repo = top.split('/').pop();
79
+ const sha = gitOut(['rev-parse', '--short', 'HEAD']);
80
+ if (sha) prov.commit = sha;
81
+ }
82
+ if (process.env.QUOX_STREAM) prov.stream = process.env.QUOX_STREAM;
83
+ try {
84
+ const f = join(homedir(), '.quox', 'held-slots.json');
85
+ if (existsSync(f)) {
86
+ const held = JSON.parse(readFileSync(f, 'utf8'));
87
+ const sess = process.env.QUOX_SESSION_ID;
88
+ const me = Object.keys(held).find((k) => sess && held[k]?.session === sess);
89
+ if (me) prov.slot = me;
90
+ }
91
+ } catch { /* no slot file: leave it out */ }
92
+ return prov;
93
+ }
94
+
95
+ export function decisionMarkdown({ what, why, rejected = [], prov }) {
96
+ const lines = [`# Decision: ${what}`, '', 'Kind: decision', '', '## What', what, '', '## Why', why || 'Not recorded.', '', '## Rejected alternatives'];
97
+ if (rejected.length) for (const r of rejected) lines.push(`- ${r}`); else lines.push('None recorded.');
98
+ lines.push('', '## Provenance');
99
+ for (const [k, v] of Object.entries(prov)) lines.push(`- ${k}: ${v}`);
100
+ return `${lines.join('\n')}\n`;
101
+ }
102
+
103
+ const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
104
+
105
+ const OUTPUT_HELP = 'Output: add -o json (global flag) for machine-readable output; note ids from search are full ids usable with quox brain note <id>.';
106
+
107
+ /**
108
+ * ONE-BRAIN CX5: score judgements against search responses (pure, unit-tested).
109
+ * judgements: [{query, note_id, verdict}]; searches: {query: searchResponse};
110
+ * a search response abstains when insufficient is true (top-level or in relevance).
111
+ * Rank = position of the first relevant note in the top k (1-based); MRR uses
112
+ * that rank. A query abstaining counts as a miss for relevant-judged queries.
113
+ */
114
+ export function scoreRetrievalEval(judgements, searches, k = 8) {
115
+ const idOf = (r) => r.noteId || r.note_id || r.id;
116
+ const byQuery = new Map();
117
+ for (const j of judgements) {
118
+ if (!byQuery.has(j.query)) byQuery.set(j.query, { relevant: new Set(), irrelevant: new Set(), abstain: false });
119
+ const e = byQuery.get(j.query);
120
+ if (j.verdict === 'relevant') e.relevant.add(j.note_id);
121
+ else if (j.verdict === 'irrelevant') e.irrelevant.add(j.note_id);
122
+ else if (j.verdict === 'should_abstain') e.abstain = true;
123
+ }
124
+ let relQ = 0, h1 = 0, h3 = 0, hk = 0, rr = 0, irrTop3 = 0, absQ = 0, absOk = 0;
125
+ for (const [q, e] of byQuery) {
126
+ const data = searches[q] || {};
127
+ const insufficient = data.insufficient === true || data.relevance?.insufficient === true;
128
+ const ids = (data.results || []).slice(0, k).map(idOf);
129
+ if (e.relevant.size) {
130
+ relQ++;
131
+ const idx = insufficient ? -1 : ids.findIndex((id) => e.relevant.has(id));
132
+ if (idx >= 0) { const rank = idx + 1; if (rank <= 1) h1++; if (rank <= 3) h3++; hk++; rr += 1 / rank; }
133
+ }
134
+ if (e.irrelevant.size && !insufficient) irrTop3 += ids.slice(0, 3).filter((id) => e.irrelevant.has(id)).length;
135
+ if (e.abstain) { absQ++; if (insufficient) absOk++; }
136
+ }
137
+ const frac = (n) => (relQ ? n / relQ : null);
138
+ return {
139
+ k,
140
+ queries: byQuery.size,
141
+ judgements: judgements.length,
142
+ relevant: { queries: relQ, hitAt1: frac(h1), hitAt3: frac(h3), hitAtK: frac(hk), mrr: relQ ? rr / relQ : null },
143
+ irrelevantInTop3: irrTop3,
144
+ abstain: { queries: absQ, abstained: absOk, rate: absQ ? absOk / absQ : null },
145
+ };
146
+ }
147
+
148
+ export function registerBrainCommands(program, ctx) {
149
+ const { client, auth } = ctx;
150
+
151
+ // quox#934: read/history/related go through the NotesQ API, which only knows
152
+ // editable NotesQ notes. A feed note (git_commit, email, ...) that `brain note`
153
+ // resolves came back as a bare 404. On a NotesQ 404, ask the brain itself;
154
+ // rethrow the original error when the brain does not know the note either.
155
+ const brainNoteOn404 = async (noteId, err) => {
156
+ if (!(err instanceof QuoxApiError) || err.status !== 404) throw err;
157
+ let note;
158
+ try {
159
+ const ep = resolveEndpoint('brain.note.get', { id: noteId });
160
+ note = await client.get(ep.path, { service: ep.service });
161
+ } catch {
162
+ throw err;
163
+ }
164
+ if (!note || !note.id) throw err;
165
+ return note;
166
+ };
167
+ const feedReason = (n, what) => `${n.id} is a ${n.kind || 'feed'} note${n.source_type ? ` from the ${n.source_type} source` : ''}, not an editable NotesQ note, so it has no ${what}.`;
168
+
169
+ const brain = program
170
+ .command('brain')
171
+ .description('Query Quox Qortex (vaults, notes, semantic search)')
172
+ .addHelpText('after', `\n${OUTPUT_HELP}`);
173
+
174
+ brain
175
+ .command('search')
176
+ .description('Search the brain')
177
+ .addHelpText('after', `\n${OUTPUT_HELP}`)
178
+ .argument('<query>', 'search query')
179
+ .option('--limit <n>', 'max results', '10')
180
+ .option('--since <date>', 'only notes dated on or after this ISO date/datetime (e.g. 2026-10-05)')
181
+ .option('--until <date>', 'only notes dated on or before this ISO date/datetime (a date alone means the whole day)')
182
+ .option('--brief', 'announce + one recall line per hit (what was found, from where); for sessions')
183
+ .action(async (query, options) => {
184
+ auth.requireAuth();
185
+ const body = { query, limit: Number(options.limit) };
186
+ if (options.since) body.since = options.since;
187
+ if (options.until) body.until = options.until;
188
+ const data = await client.post(resolveEndpoint('brain.search.list').path, { service: resolveEndpoint('brain.search.list').service, body });
189
+ // BRAIN2-W5 141: Qortex flags weak matches `insufficient` (top-level or in
190
+ // `relevance`). Never present them as successful recall.
191
+ const insufficient = data.insufficient === true || data.relevance?.insufficient === true;
192
+ if (program.opts().output === 'json') {
193
+ // Keep the full relevance envelope; scripts must be able to see the abstention.
194
+ console.log(JSON.stringify(data, null, 2));
195
+ return;
196
+ }
197
+ // quox#853: a keyword-only fallback fooled both the product and a
198
+ // verifier ("0 hits" read as "does not exist"). Say it in words.
199
+ const degradedNote = data.retrieval === 'fts' && data.degraded
200
+ ? '⚠ Semantic search was unavailable for this query, so this is keyword-only: a missing or zero result does not prove the note is absent. Retry shortly.'
201
+ : '';
202
+ if (insufficient) {
203
+ const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
204
+ console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `0 relevant hits${legLabel}`, ms: data.tookMs ?? '?' }));
205
+ console.log('⚠ Qortex judged the matches for this query insufficient (no relevant notes). The weak hits are withheld; use -o json to see them with the relevance details.');
206
+ if (degradedNote) console.log(degradedNote);
207
+ return;
208
+ }
209
+ // BRAIN2-AUDIT: sessions must SEE what Qortex recalled, not just a hit
210
+ // count, in a few cheap lines they can cite (note id prefix included).
211
+ if (options.brief && program.opts().output !== 'json') {
212
+ const results = data.results || [];
213
+ const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
214
+ console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `${results.length} hits${legLabel}`, ms: data.tookMs ?? '?' }));
215
+ if (degradedNote) console.log(degradedNote);
216
+ if (results.length === 0) {
217
+ console.log(glyphLine('brain2', 'recall', { result: 'nothing on record for this query' }));
218
+ return;
219
+ }
220
+ for (const r of results) {
221
+ const miss = Array.isArray(r.missingEntities) && r.missingEntities.length ? ` · does not mention ${r.missingEntities.join(', ')}` : '';
222
+ console.log(`${brandName('🌌 QORTEX')} recall │ "${recallSnippet(r)}" · ${recallSource(r)} · ${r.title || 'untitled'} · note ${r.noteId || '?'}${miss}`);
223
+ }
224
+ return;
225
+ }
226
+ // BRAIN2-SEMANTIC: the endpoint reports which retrieval path actually
227
+ // served (hybrid = dense+sparse fused, fts = keyword-only fallback) —
228
+ // announce it so a degraded search is visible, never silent.
229
+ if (program.opts().output !== 'json') {
230
+ const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
231
+ console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `${(data.results || []).length} hits${legLabel}`, ms: data.tookMs ?? '?' }));
232
+ if (degradedNote) console.log(degradedNote);
233
+ }
234
+ formatOutput({
235
+ columns: ['title', 'kind', 'snippet', 'score'],
236
+ headers: ['Title', 'Kind', 'Snippet', 'Score'],
237
+ rows: (data.results || []).map(r => ({
238
+ ...r,
239
+ snippet: (r.snippet || '').slice(0, 80),
240
+ score: r.score != null ? r.score.toFixed(2) : '-',
241
+ })),
242
+ }, { format: program.opts().output });
243
+ });
244
+
245
+ // BRAIN2-AUDIT: "I can never think of anything to ask it." Questions are
246
+ // generated server-side from the org's own corpus (no LLM), so each line is
247
+ // something the brain can actually answer.
248
+ brain
249
+ .command('suggest')
250
+ .description('Suggest questions to ask the brain, generated from your own notes')
251
+ .action(async () => {
252
+ auth.requireAuth();
253
+ const ep = resolveEndpoint('brain.suggestions.list');
254
+ const data = await client.get(ep.path, { service: ep.service });
255
+ const suggestions = data.suggestions || [];
256
+ if (program.opts().output === 'json') {
257
+ console.log(JSON.stringify(data, null, 2));
258
+ return;
259
+ }
260
+ console.log(glyphLine('brain2', 'suggest', { result: `${suggestions.length} questions` }));
261
+ if (suggestions.length === 0) {
262
+ console.log(data.message || 'Nothing in your brain to suggest from yet.');
263
+ return;
264
+ }
265
+ for (const s of suggestions) {
266
+ console.log(` ${s.question} (${s.source})`);
267
+ }
268
+ });
269
+
270
+ // BRAIN2-V3 B3: what entered the brain since T. Deterministic, scoped like search.
271
+ brain
272
+ .command('brief')
273
+ .description('What is new in your brain (counts by source, top new notes, open questions)')
274
+ .option('--since <window>', 'time window, e.g. 24h, 7d, 90m', '24h')
275
+ .option('--stream <id>', 'narrow to one roadmap stream (QuoxPlan stream id, e.g. AQ-9F)')
276
+ .action(async (options) => {
277
+ auth.requireAuth();
278
+ const ep = resolveEndpoint('brain.brief.get');
279
+ const qs = `since=${encodeURIComponent(options.since)}${options.stream ? `&stream=${encodeURIComponent(options.stream)}` : ''}`;
280
+ const data = await client.get(`${ep.path}?${qs}`, { service: ep.service });
281
+ if (program.opts().output === 'json') {
282
+ console.log(JSON.stringify(data, null, 2));
283
+ return;
284
+ }
285
+ console.log(glyphLine('brain2', `brief${options.stream ? ` ${options.stream}` : ''} since ${options.since}`, { result: `${data.total || 0} new` }));
286
+ console.log(data.summary || 'Nothing new entered your brain in this window.');
287
+ for (const n of data.topNotes || []) console.log(` ${n.title} (${n.source})`);
288
+ if ((data.openQuestions || []).length) {
289
+ console.log('Open questions:');
290
+ for (const q of data.openQuestions) console.log(` ${q.question}`);
291
+ }
292
+ });
293
+
294
+ // BRAIN2-V3 A2: capture everywhere. Text, or a URL to clip (HTML/PDF), lands
295
+ // as an editable NotesQ note, searchable immediately.
296
+ brain
297
+ .command('add')
298
+ .description('Save text (or clip a URL) into your brain as a note')
299
+ .argument('[text]', 'text to save (omit with --url to clip the page)')
300
+ .option('--title <title>', 'note title')
301
+ .option('--url <url>', 'source URL (clipped server-side if no text is given)')
302
+ .option('--tag <tag>', 'tag (repeatable)', (v, acc) => acc.concat(v), [])
303
+ .action(async (text, options) => {
304
+ auth.requireAuth();
305
+ if (!text && !options.url) {
306
+ console.error('Give some text, or --url <url> to clip a page.');
307
+ process.exitCode = 1;
308
+ return;
309
+ }
310
+ const body = {};
311
+ if (text) body.text = text;
312
+ if (options.title) body.title = options.title;
313
+ if (options.url) body.url = options.url;
314
+ if (options.tag && options.tag.length) body.tags = options.tag;
315
+ const ep = resolveEndpoint('brain.capture.create');
316
+ const data = await client.post(ep.path, { service: ep.service, body });
317
+ if (program.opts().output === 'json') {
318
+ console.log(JSON.stringify(data, null, 2));
319
+ return;
320
+ }
321
+ const verb = data.status === 'created' ? 'saved' : data.status === 'unchanged' ? 'already saved' : 'updated';
322
+ console.log(glyphLine('brain2', 'capture', { result: `${verb} · note ${String(data.noteId || '?').slice(0, 8)}` }));
323
+ });
324
+
325
+ // ONE-BRAIN CX3: session-native verbs. `decide` reuses /brain/capture;
326
+ // `distill` reuses the NotesQ batch import (keys = idempotency, batch id = undo).
327
+ brain
328
+ .command('decide')
329
+ .description('Record a decision (what, why, rejected alternatives, provenance). Checks the brain for an existing match first')
330
+ .argument('<what>', 'what was decided')
331
+ .requiredOption('--why <reason>', 'why it was decided')
332
+ .option('--rejected <alt>', 'alternative considered and rejected (repeatable)', (v, acc) => acc.concat(v), [])
333
+ .option('--scope <scope>', 'private (default) or org', 'private')
334
+ .option('--tags <a,b>', 'comma-separated extra tags')
335
+ .option('--force', 'save even when a strong existing match is found')
336
+ .option('--dry-run', 'print the decision note and save nothing')
337
+ .action(async (what, options) => {
338
+ auth.requireAuth();
339
+ if (!['private', 'org'].includes(options.scope)) throw new QuoxError('--scope must be private or org.', 2);
340
+ const prov = decisionProvenance();
341
+ const title = `Decision: ${what}`.slice(0, 200);
342
+ let markdown = decisionMarkdown({ what, why: options.why, rejected: options.rejected, prov });
343
+ const tags = ['decision', ...String(options.tags || '').split(',').map((t) => t.trim()).filter(Boolean)];
344
+ const json = program.opts().output === 'json';
345
+ if (options.dryRun) {
346
+ if (json) console.log(JSON.stringify({ dry_run: true, title, scope: options.scope, tags, markdown }, null, 2));
347
+ else { console.log(markdown); console.log(glyphLine('brain2', 'decide', { result: 'dry run, nothing saved' })); }
348
+ return;
349
+ }
350
+ const sp = resolveEndpoint('brain.search.list');
351
+ const found = await client.post(sp.path, { service: sp.service, body: { query: what, limit: 5 } });
352
+ const insufficient = found.insufficient === true || found.relevance?.insufficient === true;
353
+ const bare = (r) => String(r.title || '').replace(/^Decision:\s*/i, '');
354
+ const matches = insufficient ? [] : (found.results || []).filter((r) => titleOverlap(what, bare(r)) >= DECIDE_DUPE_THRESHOLD);
355
+ // An opposite-polarity match is a reversal of the earlier decision, never a duplicate of it.
356
+ const dupe = matches.find((r) => !oppositePolarity(what, bare(r)));
357
+ const reversed = dupe ? null : matches[0];
358
+ if (reversed) {
359
+ const ref = reversed.noteId || reversed.title;
360
+ if (ref) markdown = decisionMarkdown({ what, why: options.why, rejected: options.rejected, prov: { ...prov, supersedes: ref } });
361
+ }
362
+ if (dupe && !options.force) {
363
+ if (json) console.log(JSON.stringify({ saved: false, reason: 'existing_match', match: { noteId: dupe.noteId, title: dupe.title, kind: dupe.kind } }, null, 2));
364
+ else {
365
+ console.log(glyphLine('brain2', 'decide', { result: 'existing match, not saved' }));
366
+ console.log(` ${dupe.title || 'untitled'} (${dupe.kind || 'note'}) note ${dupe.noteId || '?'}`);
367
+ console.log('Re-run with --force to record it anyway.');
368
+ }
369
+ process.exitCode = 1;
370
+ return;
371
+ }
372
+ const ep = resolveEndpoint('brain.capture.create');
373
+ const data = await client.post(ep.path, { service: ep.service, body: { title, text: markdown.replace(/^# .*\n\n/, ''), tags, scope: options.scope === 'org' ? 'org' : 'user' } });
374
+ if (json) { console.log(JSON.stringify(reversed ? { ...data, reverses: { noteId: reversed.noteId, title: reversed.title } } : data, null, 2)); return; }
375
+ const verb = data.status === 'created' ? 'saved' : data.status === 'unchanged' ? 'already saved' : 'updated';
376
+ console.log(glyphLine('brain2', 'decide', { result: `${verb} · note ${String(data.noteId || '?').slice(0, 8)}` }));
377
+ if (reversed) console.log(` reverses ${reversed.title || 'untitled'} (note ${reversed.noteId || '?'})`);
378
+ });
379
+
380
+ brain
381
+ .command('distill')
382
+ .description(`Write session learnings as notes from JSONL ({key,title,text,tags?} per line, max ${DISTILL_CAP}). Re-running skips existing keys; --undo removes a batch`)
383
+ .argument('[file]', 'JSONL file, or - for stdin')
384
+ .option('--undo <batchId>', 'soft-delete every note a previous distill batch created')
385
+ .option('--dry-run', 'validate and count; write nothing')
386
+ .option('--scope <scope>', 'org|user (default user, i.e. private)', 'user')
387
+ .addOption(new Option('--poll-ms <n>', 'job poll interval').default('1000').hideHelp())
388
+ .action(async (file, options) => {
389
+ auth.requireAuth();
390
+ const json = program.opts().output === 'json';
391
+ if (options.undo) {
392
+ if (!/^[0-9a-f-]{36}$/i.test(options.undo)) throw new QuoxError('Pass the full batch id (36 characters) printed by quox brain distill.', 2);
393
+ const ep = resolveEndpoint('notesq.batch.undo', { batchId: options.undo });
394
+ const r = await client.post(ep.path, { service: ep.service, body: {} });
395
+ if (json) { console.log(JSON.stringify(r, null, 2)); return; }
396
+ console.log(glyphLine('brain2', 'distill undo', { result: `${r.deleted} deleted, ${r.failed} failed of ${r.found}` }));
397
+ if (r.failed > 0) process.exitCode = 1;
398
+ return;
399
+ }
400
+ if (!file) throw new QuoxError('Give a JSONL file (or - for stdin), or --undo <batchId>.', 2);
401
+ if (!['org', 'user'].includes(options.scope)) throw new QuoxError('--scope must be org or user.', 2);
402
+ let text;
403
+ if (file === '-') { const chunks = []; for await (const c of process.stdin) chunks.push(c); text = Buffer.concat(chunks).toString('utf8'); }
404
+ else { try { text = readFileSync(file, 'utf8'); } catch (e) { throw new QuoxError(`Cannot read ${file}: ${e.message}`, 2); } }
405
+ const items = [];
406
+ text.split(/\r?\n/).forEach((l, i) => {
407
+ if (!l.trim()) return;
408
+ let o;
409
+ try { o = JSON.parse(l); } catch (e) { throw new QuoxError(`Line ${i + 1} is not valid JSON: ${e.message}`, 2); }
410
+ for (const f of ['key', 'title', 'text']) if (typeof o[f] !== 'string' || !o[f].trim()) throw new QuoxError(`Line ${i + 1} needs a non-empty "${f}".`, 2);
411
+ if (o.tags !== undefined && !Array.isArray(o.tags)) throw new QuoxError(`Line ${i + 1}: tags must be an array.`, 2);
412
+ items.push({ key: `distill:${o.key.trim()}`, title: o.title.trim(), markdown: `# ${o.title.trim()}\n\n${o.text.trim()}\n`, tags: ['distilled', ...o.tags || []] });
413
+ });
414
+ if (!items.length) throw new QuoxError('No learnings in the input.', 2);
415
+ if (items.length > DISTILL_CAP) throw new QuoxError(`distill takes at most ${DISTILL_CAP} learnings per call (got ${items.length}). Split the file.`, 2);
416
+ if (new Set(items.map((i) => i.key)).size !== items.length) throw new QuoxError('Duplicate keys in the input; each learning needs a unique key.', 2);
417
+ const archive = await buildBatchArchive({ items, name: 'distill' });
418
+ const query = { format: 'batch', scope: options.scope };
419
+ const session = process.env.QUOX_SESSION_ID;
420
+ if (session && /^[\w.:@-]{1,80}$/.test(session)) query.session = session;
421
+ if (options.dryRun) query.dry_run = 'true';
422
+ const form = new FormData();
423
+ form.append('file', new Blob([archive.bytes]), archive.filename);
424
+ const ep = resolveEndpoint('notesq.import');
425
+ const data = await client.post(ep.path, { service: ep.service, body: form, query, timeout: 300000, retries: 0 });
426
+ if (options.dryRun) {
427
+ if (json) { console.log(JSON.stringify({ dry_run: true, ...data }, null, 2)); return; }
428
+ console.log(glyphLine('brain2', 'distill', { result: `dry run, ${data.notes ?? items.length} learnings, nothing written` }));
429
+ return;
430
+ }
431
+ if (!data.batch_id) throw new QuoxError('Distill did not return a batch id.', 1);
432
+ const sp = resolveEndpoint('notesq.import.status', { jobId: data.batch_id });
433
+ let st;
434
+ for (;;) {
435
+ st = await client.get(sp.path, { service: sp.service });
436
+ if (st.status !== 'running') break;
437
+ await sleepMs(Math.max(Number(options.pollMs) || 1000, 0));
438
+ }
439
+ if (json) console.log(JSON.stringify({ batch_id: data.batch_id, ...st }, null, 2));
440
+ else {
441
+ console.log(glyphLine('brain2', 'distill', { result: `${st.created} created, ${st.skipped} skipped, ${st.failed} failed` }));
442
+ console.log(`batch: ${data.batch_id}`);
443
+ console.log(`undo: quox brain distill --undo ${data.batch_id}`);
444
+ }
445
+ if (st.status === 'failed' || st.failed > 0) process.exitCode = 1;
446
+ });
447
+
448
+ brain
449
+ .command('status')
450
+ .description('Show brain status (note count + milestone)')
451
+ .action(async () => {
452
+ auth.requireAuth();
453
+ const data = await client.get(resolveEndpoint('brain.status.list').path, { service: resolveEndpoint('brain.status.list').service });
454
+ // BRAIN2-SEMANTIC: surface the semantic-search state (ready | indexing
455
+ // | no_provider | unknown) + pending count alongside the base stats.
456
+ const emb = data.embeddings;
457
+ const detail = {
458
+ ...(data.stats || data),
459
+ ...(emb && !emb.byOrg ? {
460
+ semantic_search: emb.state || 'unknown',
461
+ semantic_provider: emb.provider || 'none',
462
+ embedded_chunks: `${emb.embedded ?? '?'} / ${emb.total ?? '?'}`,
463
+ ...(emb.pending ? { embedding_pending: emb.pending } : {}),
464
+ } : {}),
465
+ };
466
+ formatDetail(detail, {
467
+ format: program.opts().output,
468
+ title: 'Brain Status',
469
+ });
470
+ });
471
+
472
+ // BRAIN2-AUDIT: the usage ledger, so "is the brain actually being used?"
473
+ // is a number in the terminal rather than a guess. Counts are org-scoped
474
+ // server-side; surfaces are fixed (search/context/note_read/graph) so a
475
+ // zero row still prints instead of silently vanishing.
476
+ // BRAIN2-W13 A 118: what did the agent cite, and was it supported? Rows are
477
+ // persisted server-side per finalized turn (org+user scoped).
478
+ brain
479
+ .command('citations')
480
+ .description('Show persisted agent citations (what was cited, and whether it was supported)')
481
+ .option('--session <id>', 'only this session / turn id')
482
+ .option('--limit <n>', 'max rows (default 50, cap 200)', '50')
483
+ .action(async (options) => {
484
+ auth.requireAuth();
485
+ const ep = resolveEndpoint('brain.citations.list');
486
+ const query = { limit: Number(options.limit) };
487
+ if (options.session) query.session = options.session;
488
+ const data = await client.get(ep.path, { service: ep.service, query });
489
+ const format = program.opts().output;
490
+ if (format === 'json') {
491
+ formatDetail(data, { format });
492
+ return;
493
+ }
494
+ const verdict = (r) => (r.fabricated || r.support_method === 'fabricated' ? 'fabricated' : r.support_method === 'unchecked' ? 'unchecked' : r.contradicted ? 'contradicted' : r.supported === true ? 'supported' : r.support_method === 'overlap' ? 'overlap' : r.supported === false ? 'unsupported' : 'not measured');
495
+ formatOutput({
496
+ columns: ['session', 'handle', 'note', 'span', 'rev', 'verdict', 'at'],
497
+ headers: ['Session', 'Handle', 'Note', 'Span', 'Rev', 'Verdict', 'At'],
498
+ rows: (data.records || []).map((r) => ({
499
+ session: r.session || '-',
500
+ handle: r.handle || '-',
501
+ note: r.note_id || '-',
502
+ span: r.char_start != null && r.char_end != null ? `${r.char_start}-${r.char_end}` : '-',
503
+ rev: r.note_hash ? String(r.note_hash).slice(0, 8) : '-',
504
+ verdict: verdict(r),
505
+ at: r.created_at,
506
+ })),
507
+ }, { format, emptyMessage: 'No citations recorded yet.' });
508
+ });
509
+
510
+ brain
511
+ .command('usage')
512
+ .description('Show brain usage (queries per surface, this week and total)')
513
+ .action(async () => {
514
+ auth.requireAuth();
515
+ const data = await client.get(resolveEndpoint('brain.usage.list').path, { service: resolveEndpoint('brain.usage.list').service });
516
+ const format = program.opts().output;
517
+ // Scripts want the ledger's own shape in one blob, not the three
518
+ // human tables below.
519
+ if (format === 'json') {
520
+ formatDetail(data, { format });
521
+ return;
522
+ }
523
+
524
+ const week = data.week || {};
525
+ const total = data.total || {};
526
+ const SURFACES = ['search', 'context', 'note_read', 'graph'];
527
+ formatOutput({
528
+ columns: ['surface', 'week', 'total'],
529
+ headers: ['Surface', 'Week', 'Total'],
530
+ rows: SURFACES.map(s => ({ surface: s, week: week[s] ?? 0, total: total[s] ?? 0 })),
531
+ }, { format });
532
+
533
+ const weekSources = week.byExternalSource || {};
534
+ const totalSources = total.byExternalSource || {};
535
+ const sources = [...new Set([...Object.keys(weekSources), ...Object.keys(totalSources)])].sort();
536
+ formatOutput({
537
+ columns: ['source', 'week', 'total'],
538
+ headers: ['Source', 'Week', 'Total'],
539
+ rows: sources.map(s => ({ source: s, week: weekSources[s] ?? 0, total: totalSources[s] ?? 0 })),
540
+ }, { format, emptyMessage: 'No usage recorded yet.' });
541
+
542
+ // BRAIN2-V2-1: agent grounding — answers that followed a brain search,
543
+ // and how many actually cited a retrieved note. Absent-tolerant: older
544
+ // servers without the citations aggregate just skip the block.
545
+ const cites = data.citations;
546
+ if (cites && (cites.week || cites.total)) {
547
+ const cw = cites.week || {};
548
+ const ct = cites.total || {};
549
+ if ((ct.answers ?? 0) === 0) {
550
+ formatDetail({ grounding: 'No brain-grounded answers recorded yet.' }, { format, title: 'Agent grounding' });
551
+ } else {
552
+ const pct = (g, a) => (a > 0 ? `${Math.round((g / a) * 100)}%` : '-');
553
+ formatOutput({
554
+ columns: ['metric', 'week', 'total'],
555
+ headers: ['Grounding', 'Week', 'Total'],
556
+ rows: [
557
+ { metric: 'answers after brain search', week: cw.answers ?? 0, total: ct.answers ?? 0 },
558
+ { metric: 'answers citing a note', week: `${cw.grounded ?? 0} (${pct(cw.grounded ?? 0, cw.answers ?? 0)})`, total: `${ct.grounded ?? 0} (${pct(ct.grounded ?? 0, ct.answers ?? 0)})` },
559
+ ...((cw.supportMeasured ?? 0) + (ct.supportMeasured ?? 0) > 0
560
+ ? [{ metric: 'answers with supported citations (entailment)', week: `${cw.supportedAnswers ?? 0} (${pct(cw.supportedAnswers ?? 0, cw.supportMeasured ?? 0)})`, total: `${ct.supportedAnswers ?? 0} (${pct(ct.supportedAnswers ?? 0, ct.supportMeasured ?? 0)})` },
561
+ { metric: 'answers with overlap only (not checked for meaning)', week: `${cw.overlapAnswers ?? 0}`, total: `${ct.overlapAnswers ?? 0}` }]
562
+ : []),
563
+ { metric: 'notes cited / retrieved', week: `${cw.citedNotes ?? 0}/${cw.retrievedNotes ?? 0}`, total: `${ct.citedNotes ?? 0}/${ct.retrievedNotes ?? 0}` },
564
+ ],
565
+ }, { format });
566
+ }
567
+ }
568
+
569
+ formatDetail({
570
+ last_agent_query: data.lastAgentQueryAt || 'never',
571
+ last_search: data.lastSearchAt || 'never',
572
+ last_grounded: cites?.lastGroundedAt || 'never',
573
+ }, { format, title: 'Recency' });
574
+ });
575
+
576
+ brain
577
+ .command('vaults')
578
+ .description('List brain vaults')
579
+ .action(async () => {
580
+ auth.requireAuth();
581
+ const data = await client.get(resolveEndpoint('brain.vaults.list').path, { service: resolveEndpoint('brain.vaults.list').service });
582
+ formatOutput({
583
+ columns: ['id', 'name', 'source_type', 'status', 'note_count'],
584
+ headers: ['ID', 'Name', 'Source', 'Status', 'Notes'],
585
+ rows: (data.vaults || data),
586
+ }, { format: program.opts().output });
587
+ });
588
+
589
+ brain
590
+ .command('note')
591
+ .description('Get note details')
592
+ .argument('<id>', 'note ID')
593
+ .action(async (id) => {
594
+ auth.requireAuth();
595
+ const data = await client.get(resolveEndpoint('brain.note.get', { id }).path, { service: resolveEndpoint('brain.note.get', { id }).service });
596
+ formatDetail(data.note || data, { format: program.opts().output });
597
+ });
598
+
599
+ // BRAIN2-W8: external-agent / portability contract, same routes the Cortex
600
+ // brain_read / brain_update / brain_delete tools use (collector /api/v1/notesq
601
+ // -> quoxbrain /notesq/notes/:id). Edits honour base_hash: a stale hash is a
602
+ // 409 and nothing is written.
603
+ brain
604
+ .command('read')
605
+ .description('Read one note in full (markdown + content hash for a later update)')
606
+ .argument('<noteId>', 'note id (full id)')
607
+ .option('--raw', 'print only the markdown body')
608
+ .action(async (noteId, options) => {
609
+ auth.requireAuth();
610
+ const ep = resolveEndpoint('notesq.notes.get', { id: noteId });
611
+ let data;
612
+ try {
613
+ data = await client.get(ep.path, { service: ep.service });
614
+ } catch (e) {
615
+ const bn = await brainNoteOn404(noteId, e);
616
+ data = { note: bn, source: 'brain', editable: false };
617
+ }
618
+ const n = data.note || data;
619
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
620
+ if (options.raw) { console.log(n.body ?? n.markdown ?? ''); return; }
621
+ formatDetail({ id: n.id, title: n.title, scope: n.scope, content_hash: n.content_hash, updated_at: n.updated_at }, { format: program.opts().output });
622
+ console.log('');
623
+ console.log(n.markdown ?? n.body ?? '');
624
+ });
625
+
626
+ brain
627
+ .command('update')
628
+ .description('Replace a note\'s markdown from a file. A stale --base-hash is a conflict (409), nothing is written.')
629
+ .argument('<noteId>', 'note id')
630
+ .requiredOption('--file <path>', 'file holding the full new markdown')
631
+ .option('--base-hash <hash>', 'content_hash you read (see `quox brain read`); omit to use the current one')
632
+ .action(async (noteId, options) => {
633
+ auth.requireAuth();
634
+ const markdown = readFileSync(options.file, 'utf8');
635
+ let baseHash = options.baseHash;
636
+ if (!baseHash) {
637
+ const gep = resolveEndpoint('notesq.notes.get', { id: noteId });
638
+ baseHash = ((await client.get(gep.path, { service: gep.service })).note || {}).content_hash;
639
+ }
640
+ const ep = resolveEndpoint('notesq.notes.update', { id: noteId });
641
+ let data;
642
+ try {
643
+ data = await client.put(ep.path, { service: ep.service, body: { markdown, base_hash: baseHash } });
644
+ } catch (e) {
645
+ if (e instanceof QuoxApiError && e.status === 409) {
646
+ const h = e.body && e.body.current_hash;
647
+ throw new QuoxError(`Conflict (409): note ${noteId} changed since hash ${baseHash} (current_hash ${h || 'unknown'}). Nothing was written. Re-run \`quox brain read ${noteId}\`, merge, and update again.`, 1);
648
+ }
649
+ throw e;
650
+ }
651
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
652
+ printSuccess(data.unchanged ? `Note ${noteId} unchanged.` : `Updated note ${noteId} (rev ${data.revision?.rev ?? '?'}).`);
653
+ });
654
+
655
+ brain
656
+ .command('delete')
657
+ .description('Delete a note (recorded as a revision, restorable in NotesQ). Requires --yes.')
658
+ .argument('<noteId>', 'note id')
659
+ .option('--yes', 'confirm deletion')
660
+ .option('--expected-hash <hash>', "the note's content_hash you read (see `quox brain read`); a stale value is a conflict (409) and nothing is deleted")
661
+ .action(async (noteId, options) => {
662
+ auth.requireAuth();
663
+ if (!options.yes) throw new QuoxError(`Refusing to delete ${noteId} without --yes.`, 2);
664
+ if (!options.expectedHash) throw new QuoxError(`Refusing to delete ${noteId} without --expected-hash (the content_hash from \`quox brain read ${noteId}\`), so a concurrent edit is never deleted.`, 2);
665
+ const ep = resolveEndpoint('notesq.notes.delete', { id: noteId });
666
+ let data;
667
+ try {
668
+ data = await client.delete(ep.path, { service: ep.service, query: { expected_hash: options.expectedHash } });
669
+ } catch (e) {
670
+ if (e instanceof QuoxApiError && e.status === 409) {
671
+ const h = e.body && (e.body.currentHash || e.body.current_hash);
672
+ throw new QuoxError(`Conflict (409): note ${noteId} changed since hash ${options.expectedHash} (current_hash ${h || 'unknown'}). Nothing was deleted. Re-run \`quox brain read ${noteId}\` and retry.`, 1);
673
+ }
674
+ throw e;
675
+ }
676
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
677
+ printSuccess(`Deleted note ${noteId}.`);
678
+ });
679
+
680
+ // ONE-BRAIN CX2: same routes the Cortex brain_related / brain_history /
681
+ // brain_restore tools use.
682
+ brain
683
+ .command('related')
684
+ .description('Notes related to one note (by similarity)')
685
+ .argument('<noteId>', 'note id (full id)')
686
+ .option('--limit <n>', 'how many (default 8, max 20)', '8')
687
+ .action(async (noteId, options) => {
688
+ auth.requireAuth();
689
+ const ep = resolveEndpoint('notesq.notes.related', { id: noteId });
690
+ let data;
691
+ try {
692
+ data = await client.get(ep.path, { service: ep.service, query: { limit: Math.min(Math.max(parseInt(options.limit, 10) || 8, 1), 20) } });
693
+ } catch (e) {
694
+ const bn = await brainNoteOn404(noteId, e);
695
+ data = { related: [], editable: false, reason: `${feedReason(bn, 'similarity links')} Use \`quox brain neighbours ${noteId}\` for its graph links.` };
696
+ if (program.opts().output !== 'json') console.log(data.reason);
697
+ }
698
+ const related = data.related || [];
699
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
700
+ console.log(glyphLine('brain2', `related ${noteId.slice(0, 8)}`, { result: `${related.length} notes` }));
701
+ formatOutput({ columns: ['id', 'title', 'scope', 'score'], headers: ['ID', 'Title', 'Scope', 'Score'], rows: related }, { format: program.opts().output });
702
+ });
703
+
704
+ // ONE-BRAIN CX2: same data the Cortex brain_neighbours tool returns
705
+ // (quoxbrain GET /notes/:id outlinks + backlinks, scope-checked server-side).
706
+ brain
707
+ .command('neighbours')
708
+ .alias('neighbors')
709
+ .description('Notes this note links to and notes that link to it (graph neighbours)')
710
+ .argument('<noteId>', 'note id (full id)')
711
+ .action(async (noteId) => {
712
+ auth.requireAuth();
713
+ const ep = resolveEndpoint('brain.note.get', { id: noteId });
714
+ const data = await client.get(ep.path, { service: ep.service });
715
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
716
+ const outlinks = Array.isArray(data.outlinks) ? data.outlinks : [];
717
+ const backlinks = Array.isArray(data.backlinks) ? data.backlinks : [];
718
+ const rows = [
719
+ ...outlinks.map((l) => (l.resolved && l.id
720
+ ? { direction: 'out', id: l.id, title: l.title || '' }
721
+ : { direction: 'out (dangling)', id: '', title: l.targetRaw || '' })),
722
+ ...backlinks.map((l) => ({ direction: 'in', id: l.id, title: l.title || '' })),
723
+ ];
724
+ console.log(glyphLine('brain2', `neighbours ${noteId.slice(0, 8)}`, { result: `${outlinks.length} out, ${backlinks.length} in` }));
725
+ formatOutput({ columns: ['direction', 'id', 'title'], headers: ['Direction', 'ID', 'Title'], rows }, { format: program.opts().output });
726
+ });
727
+
728
+ brain
729
+ .command('context')
730
+ .description('Token-budgeted pack of the most relevant cited passages on a topic (what the Cortex brain_context tool returns)')
731
+ .argument('<query>', 'topic or task')
732
+ .option('--budget <n>', 'approximate token budget (200-4000)', '1200')
733
+ .option('--since <date>', 'only notes dated on or after this ISO date/datetime (e.g. 2026-10-05)')
734
+ .option('--until <date>', 'only notes dated on or before this ISO date/datetime (a date alone means the whole day)')
735
+ .action(async (query, options) => {
736
+ auth.requireAuth();
737
+ const tokenBudget = Math.min(Math.max(parseInt(options.budget, 10) || 1200, 200), 4000);
738
+ const ep = resolveEndpoint('brain.context.pack');
739
+ const data = await client.post(ep.path, { service: ep.service, body: { query, tokenBudget, ...(options.since ? { since: options.since } : {}), ...(options.until ? { until: options.until } : {}) } });
740
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
741
+ const passages = data.passages || [];
742
+ const insufficient = data.insufficient === true || data.relevance?.insufficient === true || passages.length === 0;
743
+ if (insufficient) {
744
+ console.log(glyphLine('brain2', `context "${query.slice(0, 40)}"`, { result: '0 relevant passages' }));
745
+ console.log('Nothing relevant was found in the brain for this topic.');
746
+ return;
747
+ }
748
+ console.log(glyphLine('brain2', `context "${query.slice(0, 40)}"`, { result: `${passages.length} passages · ${data.tokensUsed ?? '?'} tokens` }));
749
+ passages.forEach((p, i) => {
750
+ const c = p.citation || {};
751
+ console.log(`${i + 1}. ${c.title || 'Untitled'} (${p.noteId || c.noteId}${Number.isFinite(c.charStart) ? `, chars ${c.charStart}-${c.charEnd}` : ''})`);
752
+ console.log(` ${String(p.text || '').replace(/\s+/g, ' ').trim().slice(0, 600)}`);
753
+ });
754
+ });
755
+
756
+ // ONE-BRAIN CX4: the Cortex brain_check tool, via the governed tool endpoint
757
+ // (same path as `quox linkedin`): verdict + evidence for a claim.
758
+ brain
759
+ .command('check')
760
+ .description('Check whether the knowledge brain supports, contradicts or says nothing about a claim (what the Cortex brain_check tool returns)')
761
+ .argument('<claim>', 'the statement to check (max 500 characters)')
762
+ .action(async (claim) => {
763
+ auth.requireAuth();
764
+ if (claim.length > 500) throw new QuoxError(`The claim is ${claim.length} characters; the limit is 500.`, 2);
765
+ const ep = resolveEndpoint('v1.tools.execute');
766
+ const data = await client.post(ep.path, { service: ep.service, body: { tool_name: 'brain_check', input: { claim }, org_id: resolveOrgId(auth, program) } });
767
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
768
+ if (!data || data.success === false) {
769
+ throw new QuoxError(`brain_check failed: ${(data && (data.error || data.output)) || 'unknown error'}`, 1);
770
+ }
771
+ const mark = { supported: '✔', contradicted: '✖', overlap: '◐', unknown: '◐' }[data.verdict] || '';
772
+ console.log(glyphLine('brain2', `check "${claim.slice(0, 40)}"`, { result: `${data.verdict} ${mark}`.trim() }));
773
+ const ev = data.evidence || [];
774
+ for (const e of ev) {
775
+ console.log(` ${e.verdict.padEnd(12)} ${e.title || 'Untitled'} (${e.noteId || ''}${e.relPath ? `, ${e.relPath}` : ''})${e.conditionDropped ? ' [condition dropped]' : ''}`);
776
+ }
777
+ if (data.output) console.log(data.output);
778
+ });
779
+
780
+ // ONE-BRAIN CX6: the Cortex brain_why tool, via the governed tool endpoint:
781
+ // why a note ranked where it did for a query, or why it is missing.
782
+ brain
783
+ .command('why')
784
+ .description('Two modes. `why <target>`: the commits, dev sessions and decisions behind a commit sha or file path. `why "<query>" <noteId>`: why a note ranked where it did, or why it is missing (Cortex brain_why tool)')
785
+ .argument('<queryOrTarget>', 'a commit sha / file path (one argument), or the search query (with <noteId>)')
786
+ .argument('[noteId]', 'the full note id to explain (ranking mode)')
787
+ .action(async (first, noteId) => {
788
+ auth.requireAuth();
789
+ const provenance = noteId === undefined;
790
+ const input = provenance ? { target: first } : { query: first, noteId };
791
+ const ep = resolveEndpoint('v1.tools.execute');
792
+ const data = await client.post(ep.path, { service: ep.service, body: { tool_name: 'brain_why', input, org_id: resolveOrgId(auth, program) } });
793
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
794
+ if (!data || data.success === false) {
795
+ throw new QuoxError(`brain_why failed: ${(data && (data.error || data.output)) || 'unknown error'}`, 1);
796
+ }
797
+ if (provenance) {
798
+ const n = (data.commits || []).length + (data.sessions || []).length + (data.decisions || []).length;
799
+ console.log(glyphLine('brain2', `why ${first.slice(0, 40)}`, { result: `${n} sources` }));
800
+ if (data.output) console.log(data.output);
801
+ return;
802
+ }
803
+ const inResults = data.target?.status === 'in_results';
804
+ console.log(glyphLine('brain2', `why ${noteId.slice(0, 8)} for "${first.slice(0, 40)}"`, { result: inResults ? `rank ${data.target.rank} ✔` : 'not returned ⚠' }));
805
+ if (data.output) console.log(data.output);
806
+ });
807
+
808
+ brain
809
+ .command('history')
810
+ .description("A note's revision history, or with --rev the text of that revision")
811
+ .argument('<noteId>', 'note id (full id)')
812
+ .option('--rev <n>', "show this revision's markdown")
813
+ .action(async (noteId, options) => {
814
+ auth.requireAuth();
815
+ if (options.rev !== undefined) {
816
+ const rev = Number(options.rev);
817
+ if (!Number.isInteger(rev) || rev < 1) throw new QuoxError('--rev must be a positive whole number.', 2);
818
+ const ep = resolveEndpoint('notesq.notes.revision', { id: noteId, rev });
819
+ const data = await client.get(ep.path, { service: ep.service });
820
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
821
+ formatDetail({ rev: data.rev, action: data.action, author_kind: data.author_kind, created_at: data.created_at }, { format: program.opts().output });
822
+ console.log('');
823
+ console.log(data.markdown ?? '');
824
+ return;
825
+ }
826
+ const ep = resolveEndpoint('notesq.notes.revisions', { id: noteId });
827
+ let data;
828
+ try {
829
+ data = await client.get(ep.path, { service: ep.service });
830
+ } catch (e) {
831
+ const bn = await brainNoteOn404(noteId, e);
832
+ data = { revisions: [], editable: false, reason: feedReason(bn, 'revision history') };
833
+ if (program.opts().output !== 'json') { console.log(data.reason); return; }
834
+ }
835
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
836
+ formatOutput({ columns: ['rev', 'action', 'author_kind', 'created_at', 'content_hash'], headers: ['Rev', 'Action', 'Author', 'When', 'Hash'], rows: data.revisions || [] }, { format: program.opts().output });
837
+ });
838
+
839
+ brain
840
+ .command('restore')
841
+ .description("Restore a note to an earlier revision (recorded as a new revision). --expected-hash must be the note's current content_hash (see `quox brain read`).")
842
+ .argument('<noteId>', 'note id')
843
+ .argument('<rev>', 'revision number to restore (see `quox brain history`)')
844
+ .requiredOption('--expected-hash <hash>', 'content_hash you read; a stale value is a conflict and nothing is restored')
845
+ .action(async (noteId, revArg, options) => {
846
+ auth.requireAuth();
847
+ const rev = Number(revArg);
848
+ if (!Number.isInteger(rev) || rev < 1) throw new QuoxError('<rev> must be a positive whole number.', 2);
849
+ const gep = resolveEndpoint('notesq.notes.get', { id: noteId });
850
+ const current = ((await client.get(gep.path, { service: gep.service })).note || {}).content_hash;
851
+ if (current !== options.expectedHash) {
852
+ throw new QuoxError(`Conflict: note ${noteId} changed since hash ${options.expectedHash} (current_hash ${current || 'unknown'}). Nothing was restored. Re-run \`quox brain read ${noteId}\` and try again.`, 1);
853
+ }
854
+ const ep = resolveEndpoint('notesq.notes.restore', { id: noteId });
855
+ let data;
856
+ try {
857
+ data = await client.post(ep.path, { service: ep.service, body: { rev, expected_hash: options.expectedHash } });
858
+ } catch (e) {
859
+ if (e instanceof QuoxApiError && e.status === 409) {
860
+ const h = e.body && (e.body.currentHash || e.body.current_hash);
861
+ throw new QuoxError(`Conflict (409): note ${noteId} changed since hash ${options.expectedHash} (current_hash ${h || 'unknown'}). Nothing was restored. Re-run \`quox brain read ${noteId}\` and try again.`, 1);
862
+ }
863
+ throw e;
864
+ }
865
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
866
+ printSuccess(data.unchanged ? `Note ${noteId} already matches revision ${rev}.` : `Restored note ${noteId} to revision ${rev} (new rev ${data.revision?.rev ?? '?'}).`);
867
+ });
868
+
869
+ // ONE-BRAIN CX5: turn real usage into a labelled retrieval eval set.
870
+ brain
871
+ .command('judge')
872
+ .description('Label a query: a note is relevant/irrelevant for it, or the query should abstain')
873
+ .argument('<query>', 'the search query being judged')
874
+ .argument('[noteId]', 'the note being judged (not needed with --should-abstain)')
875
+ .option('--relevant', 'this note is a correct answer for the query')
876
+ .option('--irrelevant', 'this note should not surface for the query')
877
+ .option('--should-abstain', 'nothing in the brain should answer this query')
878
+ .action(async (query, noteId, options) => {
879
+ auth.requireAuth();
880
+ const picked = ['relevant', 'irrelevant', 'shouldAbstain'].filter((k) => options[k]);
881
+ if (picked.length !== 1) throw new QuoxError('Pick exactly one of --relevant, --irrelevant, --should-abstain.', 2);
882
+ const verdict = options.shouldAbstain ? 'should_abstain' : options.relevant ? 'relevant' : 'irrelevant';
883
+ if (verdict === 'should_abstain' && noteId) throw new QuoxError('--should-abstain judges the query, drop the note id.', 2);
884
+ if (verdict !== 'should_abstain' && !noteId) throw new QuoxError(`--${verdict} needs a <noteId>.`, 2);
885
+ const ep = resolveEndpoint('brain.judgements.create');
886
+ const body = { query, verdict, source: 'cli' };
887
+ if (noteId) body.noteId = noteId;
888
+ const data = await client.post(ep.path, { service: ep.service, body });
889
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
890
+ console.log(glyphLine('brain2', `judge "${query.slice(0, 40)}"`, { result: `${verdict}${noteId ? ` · note ${noteId}` : ''}${data.created === false ? ' · already recorded' : ''}` }));
891
+ });
892
+
893
+ brain
894
+ .command('judgements')
895
+ .description('List your relevance judgements (the eval set)')
896
+ .option('--limit <n>', 'max rows (default 200, cap 1000)', '200')
897
+ .action(async (options) => {
898
+ auth.requireAuth();
899
+ const ep = resolveEndpoint('brain.judgements.list');
900
+ const data = await client.get(ep.path, { service: ep.service, query: { limit: Number(options.limit) } });
901
+ const format = program.opts().output;
902
+ if (format === 'json') { formatDetail(data, { format }); return; }
903
+ formatOutput({
904
+ columns: ['verdict', 'query', 'note', 'at'],
905
+ headers: ['Verdict', 'Query', 'Note', 'At'],
906
+ rows: (data.judgements || []).map((j) => ({ verdict: j.verdict, query: j.query, note: j.note_id || '-', at: j.created_at })),
907
+ }, { format, emptyMessage: 'No judgements yet. Label some with `quox brain judge`.' });
908
+ });
909
+
910
+ // ONE-BRAIN CX6: topic subscriptions. A scheduler on the collector searches
911
+ // each topic as you and posts new notes to your slot.
912
+ brain
913
+ .command('subscribe')
914
+ .description('Tell a slot when new notes about a topic land in your brain')
915
+ .argument('<topic>', 'the topic (a search query)')
916
+ .requiredOption('--slot <name>', 'slot to notify')
917
+ .action(async (topic, options) => {
918
+ auth.requireAuth();
919
+ const ep = resolveEndpoint('brain.subscriptions.create');
920
+ const data = await client.post(ep.path, { service: ep.service, body: { query: topic, slot: options.slot } });
921
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
922
+ const sub = data.subscription || {};
923
+ console.log(glyphLine('brain2', `subscribe "${topic.slice(0, 40)}"`, { result: `slot ${options.slot} · ${data.created === false ? (data.revived ? 'revived' : 'already subscribed') : 'subscribed'} · id ${sub.id || '?'}` }));
924
+ });
925
+
926
+ brain
927
+ .command('subscriptions')
928
+ .description('List your topic subscriptions')
929
+ .action(async () => {
930
+ auth.requireAuth();
931
+ const ep = resolveEndpoint('brain.subscriptions.list');
932
+ const data = await client.get(ep.path, { service: ep.service });
933
+ const format = program.opts().output;
934
+ if (format === 'json') { formatDetail(data, { format }); return; }
935
+ formatOutput({
936
+ columns: ['id', 'topic', 'slot', 'state', 'checked'],
937
+ headers: ['ID', 'Topic', 'Slot', 'State', 'Last checked'],
938
+ rows: (data.subscriptions || []).map((s) => ({
939
+ id: s.id, topic: s.query, slot: s.slot,
940
+ state: s.disabledAt ? `disabled: ${s.disabledReason || 'unknown'}` : (s.consecutiveFailures ? `failing (${s.consecutiveFailures})` : 'active'),
941
+ checked: s.lastCheckedAt || 'not yet',
942
+ })),
943
+ }, { format, emptyMessage: 'No subscriptions. Add one with `quox brain subscribe "<topic>" --slot <name>`.' });
944
+ });
945
+
946
+ brain
947
+ .command('unsubscribe')
948
+ .description('Remove a topic subscription')
949
+ .argument('<id>', 'subscription id from `quox brain subscriptions`')
950
+ .action(async (id) => {
951
+ auth.requireAuth();
952
+ const ep = resolveEndpoint('brain.subscriptions.delete', { id });
953
+ const data = await client.delete(ep.path, { service: ep.service });
954
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
955
+ console.log(glyphLine('brain2', 'unsubscribe', { result: `removed ${id}` }));
956
+ });
957
+
958
+ brain
959
+ .command('sources')
960
+ .description('List the Feed sources that fill your brain (state and last sync)')
961
+ .action(async () => {
962
+ auth.requireAuth();
963
+ const ep = resolveEndpoint('brain.sources.list');
964
+ const data = await client.get(ep.path, { service: ep.service });
965
+ const format = program.opts().output;
966
+ if (format === 'json') { formatDetail(data, { format }); return; }
967
+ formatOutput({
968
+ columns: ['type', 'state', 'scope', 'last'],
969
+ headers: ['Type', 'State', 'Scope', 'Last sync'],
970
+ rows: (data.sources || []).map((s) => ({
971
+ type: s.sourceType,
972
+ state: !s.connected ? s.status : (s.enabled ? 'on' : 'off'),
973
+ scope: s.scope || '',
974
+ last: s.last_status ? `${s.last_status}${s.last_count ? ` (${s.last_count})` : ''}` : 'never',
975
+ })),
976
+ }, { format, emptyMessage: 'No sources.' });
977
+ });
978
+
979
+ brain
980
+ .command('source')
981
+ .description('Turn a Feed source on or off and set its config (host paths need an org admin and the instance allow-list)')
982
+ .argument('<type>', 'source type from `quox brain sources`, e.g. git, folder, transcript')
983
+ .option('--enable', 'turn the source on')
984
+ .option('--disable', 'turn the source off')
985
+ .option('--scope <scope>', 'org or user')
986
+ .option('--path <path>', 'set config.path')
987
+ .option('--roots <paths>', 'set config.roots (comma-separated absolute paths)')
988
+ .option('--config <json>', 'merge a JSON object into the config')
989
+ .action(async (type, options) => {
990
+ auth.requireAuth();
991
+ if (options.enable && options.disable) throw new Error('Pass --enable or --disable, not both');
992
+ let config;
993
+ if (options.config !== undefined) {
994
+ let parsed;
995
+ try { parsed = JSON.parse(options.config); } catch { parsed = null; }
996
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('--config must be a JSON object');
997
+ config = { ...parsed };
998
+ }
999
+ if (options.path) config = { ...(config || {}), path: options.path };
1000
+ if (options.roots) config = { ...(config || {}), roots: options.roots.split(',').map((r) => r.trim()).filter(Boolean) };
1001
+ const body = {};
1002
+ if (options.enable) body.enabled = true;
1003
+ if (options.disable) body.enabled = false;
1004
+ if (options.scope) body.scope = options.scope;
1005
+ if (config) body.config = config;
1006
+ if (Object.keys(body).length === 0) throw new Error('Nothing to change: pass --enable, --disable, --scope, --path, --roots or --config');
1007
+ const ep = resolveEndpoint('brain.sources.update', { type });
1008
+ const data = await client.put(ep.path, { service: ep.service, body });
1009
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
1010
+ const src = data.source || {};
1011
+ console.log(glyphLine('brain2', `source ${type}`, { result: `${src.enabled ? 'on' : 'off'}${src.config ? ` · config ${JSON.stringify(src.config).slice(0, 80)}` : ''}` }));
1012
+ });
1013
+
1014
+ brain
1015
+ .command('eval')
1016
+ .description('Replay your judged queries against live search and score retrieval (hit@1/3/k, MRR, abstention)')
1017
+ .option('--limit <n>', 'max judgements to load', '1000')
1018
+ .option('--k <n>', 'results fetched per query', '8')
1019
+ .action(async (options) => {
1020
+ auth.requireAuth();
1021
+ const k = Math.min(Math.max(parseInt(options.k, 10) || 8, 1), 50);
1022
+ const jep = resolveEndpoint('brain.judgements.list');
1023
+ const jdata = await client.get(jep.path, { service: jep.service, query: { limit: Number(options.limit) } });
1024
+ const judgements = jdata.judgements || [];
1025
+ const sep = resolveEndpoint('brain.search.list');
1026
+ const searches = {};
1027
+ for (const q of [...new Set(judgements.map((j) => j.query))]) {
1028
+ searches[q] = await client.post(sep.path, { service: sep.service, body: { query: q, limit: k } });
1029
+ }
1030
+ const report = scoreRetrievalEval(judgements, searches, k);
1031
+ if (program.opts().output === 'json') { console.log(JSON.stringify(report, null, 2)); return; }
1032
+ console.log(glyphLine('brain2', 'eval', { result: `${report.queries} queries · ${report.judgements} judgements` }));
1033
+ const pct = (v) => (v == null ? 'n/a' : `${(v * 100).toFixed(0)}%`);
1034
+ formatOutput({
1035
+ columns: ['metric', 'value'],
1036
+ headers: ['Metric', 'Value'],
1037
+ rows: [
1038
+ { metric: `relevant-judged queries`, value: String(report.relevant.queries) },
1039
+ { metric: 'hit@1', value: pct(report.relevant.hitAt1) },
1040
+ { metric: 'hit@3', value: pct(report.relevant.hitAt3) },
1041
+ { metric: `hit@${k}`, value: pct(report.relevant.hitAtK) },
1042
+ { metric: 'MRR', value: report.relevant.mrr == null ? 'n/a' : report.relevant.mrr.toFixed(3) },
1043
+ { metric: 'irrelevant notes in top 3 (precision problems)', value: String(report.irrelevantInTop3) },
1044
+ { metric: 'should-abstain queries', value: String(report.abstain.queries) },
1045
+ { metric: 'abstained correctly', value: pct(report.abstain.rate) },
1046
+ ],
1047
+ }, { format: 'table' });
1048
+ });
1049
+
1050
+ // BRAIN2-W8: portable export. Server streams every note visible to you (same
1051
+ // scope rule as search, vault exclusions respected). jsonl: one note per line
1052
+ // to stdout or --out <file> (-o is the global --output flag). markdown:
1053
+ // one .md file with YAML front matter per note, written under --out <dir>.
1054
+ brain
1055
+ .command('export')
1056
+ .description('Export your visible notes (jsonl to stdout/--out file, or markdown files into --out dir)')
1057
+ .option('--out <path>', 'file (jsonl) or directory (markdown); default stdout for jsonl')
1058
+ .option('--format <fmt>', 'jsonl or markdown', 'jsonl')
1059
+ .option('--scope <scope>', 'org | user | shared (default: all visible)')
1060
+ .action(async (options) => {
1061
+ auth.requireAuth();
1062
+ if (!['jsonl', 'markdown'].includes(options.format)) throw new QuoxError('--format must be jsonl or markdown.', 2);
1063
+ if (options.format === 'markdown' && !options.out) throw new QuoxError('--format markdown needs --out <directory>.', 2);
1064
+ const ep = resolveEndpoint('brain.export.get');
1065
+ const query = { format: 'jsonl' };
1066
+ if (options.scope) query.scope = options.scope;
1067
+ const data = await client.get(ep.path, { service: ep.service, query });
1068
+ const text = typeof data.text === 'string' ? data.text : '';
1069
+ const recs = text.split('\n').filter(Boolean).map((l) => JSON.parse(l));
1070
+ if (options.format === 'jsonl') {
1071
+ if (options.out) writeFileSync(options.out, text);
1072
+ else process.stdout.write(text);
1073
+ } else {
1074
+ for (const r of recs) {
1075
+ const fm = { id: r.id, title: r.title, scope: r.scope, source: r.source, created_at: r.created_at, updated_at: r.updated_at, tags: r.tags, links: r.links, revision_count: r.revision_count };
1076
+ const file = join(options.out, r.scope || 'note', `${slug(r.title)}-${String(r.id).slice(0, 8)}.md`);
1077
+ mkdirSync(dirname(file), { recursive: true });
1078
+ writeFileSync(file, `---\n${Object.entries(fm).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n')}\n---\n${r.body || ''}\n`);
1079
+ }
1080
+ }
1081
+ if (options.out || program.opts().output !== 'json') {
1082
+ console.error(glyphLine('brain2', 'export', { result: `${recs.length} notes${options.out ? ` -> ${options.out}` : ''}` }));
1083
+ }
1084
+ });
1085
+ }