quoxcli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -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/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,160 @@
1
+ /**
2
+ * NotesQ session bulk authoring (AQ-9D P8): pure helpers behind
3
+ * `quox notes batch` and `quox notes split`. A batch travels as a zip of
4
+ * markdown files (frontmatter: title, collection, tags, key) and is parsed
5
+ * server-side by the import pipeline's `batch` format.
6
+ */
7
+
8
+ import { readdirSync, readFileSync, statSync } from 'fs';
9
+ import { basename, join, relative } from 'path';
10
+ import { QuoxError } from '../core/errors.js';
11
+
12
+ // Mirrors MAX_BATCH_NOTES in services/collector/lib/notesq/import/index.js.
13
+ export const MAX_BATCH_NOTES = 200;
14
+ const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
15
+
16
+ // The server's frontmatter reader strips one pair of quotes and does no escape
17
+ // processing, so pick the quote the value does not contain.
18
+ function yamlStr(v) {
19
+ const s = String(v).replace(/[\r\n]+/g, ' ').trim();
20
+ if (!s.includes('"')) return `"${s}"`;
21
+ if (!s.includes("'")) return `'${s}'`;
22
+ return `"${s.replace(/"/g, "'")}"`;
23
+ }
24
+
25
+ /** One note as a markdown file with the frontmatter the batch parser reads. */
26
+ export function noteFile({ title, markdown, collection, tags, key }) {
27
+ const fm = [];
28
+ if (title) fm.push(`title: ${yamlStr(title)}`);
29
+ if (collection) fm.push(`collection: ${yamlStr(collection)}`);
30
+ if (key) fm.push(`key: ${yamlStr(key)}`);
31
+ const list = (tags || []).map((t) => String(t).trim()).filter(Boolean);
32
+ if (list.length) { fm.push('tags:'); for (const t of list) fm.push(` - ${yamlStr(t)}`); }
33
+ return `${fm.length ? `---\n${fm.join('\n')}\n---\n` : ''}${String(markdown || '')}`;
34
+ }
35
+
36
+ /** JSONL text -> items. Each line: {title, markdown|body, collection?, tags?, key?}. Key defaults to the title, so a re-run skips. */
37
+ export function parseJsonl(text) {
38
+ const items = [];
39
+ String(text || '').split(/\r?\n/).forEach((line, i) => {
40
+ if (!line.trim()) return;
41
+ let o;
42
+ try { o = JSON.parse(line); } catch (e) { throw new QuoxError(`JSONL line ${i + 1} is not valid JSON: ${e.message}`, 2); }
43
+ const title = typeof o.title === 'string' ? o.title.trim() : '';
44
+ const markdown = typeof o.markdown === 'string' ? o.markdown : (typeof o.body === 'string' ? o.body : '');
45
+ if (!title) throw new QuoxError(`JSONL line ${i + 1} has no title.`, 2);
46
+ if (o.tags !== undefined && !Array.isArray(o.tags)) throw new QuoxError(`JSONL line ${i + 1}: tags must be an array.`, 2);
47
+ items.push({ title, markdown, collection: typeof o.collection === 'string' ? o.collection : undefined, tags: o.tags || [], key: typeof o.key === 'string' && o.key ? o.key : title });
48
+ });
49
+ return items;
50
+ }
51
+
52
+ function walk(dir, root, out) {
53
+ for (const name of readdirSync(dir).sort()) {
54
+ if (name.startsWith('.')) continue;
55
+ const p = join(dir, name);
56
+ const st = statSync(p);
57
+ if (st.isDirectory()) walk(p, root, out);
58
+ else if (st.isFile()) out.push({ path: relative(root, p).split('\\').join('/'), size: st.size, read: () => readFileSync(p) });
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /**
64
+ * Build the zip for a batch. `source` is a folder (markdown + any embedded
65
+ * files, sent as-is), a .jsonl file, or '-' with `stdinText` holding JSONL.
66
+ * `items` (already-built notes, used by split) bypasses `source`.
67
+ * @returns {{bytes: Buffer, filename: string, notes: number}}
68
+ */
69
+ export async function buildBatchArchive({ source, stdinText, items, name }) {
70
+ const JSZip = (await import('jszip')).default;
71
+ const zip = new JSZip();
72
+ let root;
73
+ let notes = 0;
74
+ const addItems = (list, rootName) => {
75
+ root = rootName;
76
+ list.forEach((it, i) => { zip.file(`${root}/${String(i + 1).padStart(3, '0')}.md`, noteFile(it)); });
77
+ notes = list.length;
78
+ };
79
+ if (items) addItems(items, name || 'batch');
80
+ else if (source === '-') addItems(parseJsonl(stdinText), name || 'stdin');
81
+ else {
82
+ let st;
83
+ try { st = statSync(source); } catch (e) { throw new QuoxError(`Cannot read ${source}: ${e.message}`, 2); }
84
+ if (st.isDirectory()) {
85
+ root = name || basename(source.replace(/[\\/]+$/, '')) || 'batch';
86
+ const files = walk(source, source, []);
87
+ let total = 0;
88
+ for (const f of files) {
89
+ total += f.size;
90
+ if (total > MAX_ARCHIVE_BYTES) throw new QuoxError(`${source} is over the 100 MB batch limit.`, 2);
91
+ zip.file(`${root}/${f.path}`, f.read());
92
+ if (/\.(md|markdown)$/i.test(f.path)) notes++;
93
+ }
94
+ } else if (/\.jsonl$/i.test(source)) {
95
+ addItems(parseJsonl(readFileSync(source, 'utf8')), name || basename(source).replace(/\.jsonl$/i, ''));
96
+ } else {
97
+ throw new QuoxError(`${source} is not a folder or a .jsonl file (use - to read JSONL from stdin).`, 2);
98
+ }
99
+ }
100
+ if (!notes) throw new QuoxError('Nothing to write: the batch has no markdown notes.', 2);
101
+ if (notes > MAX_BATCH_NOTES) throw new QuoxError(`A batch may create at most ${MAX_BATCH_NOTES} notes (this one has ${notes}). Split it into smaller batches.`, 2);
102
+ const bytes = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
103
+ return { bytes, filename: `${root}.zip`, notes };
104
+ }
105
+
106
+ const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
107
+ const FENCE = /^\s*(```|~~~)/;
108
+ // Characters that would break a [[wikilink]] to the child.
109
+ const cleanTitle = (t) => t.replace(/[[\]|#^]/g, '').replace(/\s+/g, ' ').trim();
110
+
111
+ /**
112
+ * Cut a note at its headings. Level defaults to the shallowest heading below
113
+ * the title H1. Headings inside code fences are ignored. Text before the first
114
+ * cut stays with the parent; deeper headings inside a section are promoted so
115
+ * the child reads as a note of its own.
116
+ * @returns {{title:string|null, preamble:string, level:number|null, sections:{title:string, body:string}[]}}
117
+ */
118
+ export function splitMarkdown(markdown, { level } = {}) {
119
+ const lines = String(markdown || '').split('\n');
120
+ const heads = [];
121
+ let inFence = false;
122
+ lines.forEach((line, i) => {
123
+ if (FENCE.test(line)) { inFence = !inFence; return; }
124
+ if (inFence) return;
125
+ const m = HEADING.exec(line);
126
+ if (m) heads.push({ i, level: m[1].length, text: m[2] });
127
+ });
128
+ let title = null;
129
+ let start = 0;
130
+ const first = heads[0];
131
+ if (first && first.level === 1 && lines.slice(0, first.i).every((l) => !l.trim())) { title = first.text.trim(); start = first.i + 1; }
132
+ const below = heads.filter((h) => h.i >= start && (title === null || h.level > 1));
133
+ const lvl = level ? Number(level) : (below.length ? Math.min(...below.map((h) => h.level)) : null);
134
+ const cuts = below.filter((h) => h.level === lvl);
135
+ if (cuts.length < 2) return { title, preamble: '', level: lvl, sections: [] };
136
+ const preamble = lines.slice(start, cuts[0].i).join('\n').trim();
137
+ const seen = new Map();
138
+ const sections = cuts.map((c, k) => {
139
+ const end = k + 1 < cuts.length ? cuts[k + 1].i : lines.length;
140
+ let fence = false;
141
+ const body = lines.slice(c.i + 1, end).map((l) => {
142
+ if (FENCE.test(l)) { fence = !fence; return l; }
143
+ if (fence) return l;
144
+ const m = HEADING.exec(l);
145
+ return m && m[1].length > lvl ? `${'#'.repeat(Math.max(m[1].length - lvl + 1, 2))} ${m[2]}` : l;
146
+ }).join('\n').trim();
147
+ let t = cleanTitle(c.text) || `Part ${k + 1}`;
148
+ const n = (seen.get(t) || 0) + 1;
149
+ seen.set(t, n);
150
+ if (n > 1) t = `${t} (${n})`;
151
+ return { title: t, body };
152
+ });
153
+ return { title, preamble, level: lvl, sections };
154
+ }
155
+
156
+ /** The parent after a split: its own preamble, then links to every child. */
157
+ export function indexMarkdown({ title, preamble, children }) {
158
+ const head = `# ${title}\n\n${preamble ? `${preamble}\n\n` : ''}`;
159
+ return `${head}## Contents\n\n${children.map((c) => `- [[${c}]]`).join('\n')}\n`;
160
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * NotesQ AssistantQ wires (AQ-9D P4), pure helpers. Each wire composes at the
3
+ * edge: the CLI reads through the other plugin's own governed route (its
4
+ * licence, access rules and audit) and writes through NotesQ's, so no new
5
+ * server path reads another plugin's data.
6
+ */
7
+
8
+ import { createHash } from 'crypto';
9
+
10
+ const short = (s) => createHash('sha1').update(s).digest('hex').slice(0, 12);
11
+
12
+ /** An InboxQ message as a note. Body text preferred; HTML is reduced to text; snippet is the last resort. */
13
+ export function emailToNote(msg) {
14
+ const subject = String(msg.subject || '').replace(/[\r\n]+/g, ' ').trim() || '(no subject)';
15
+ const from = msg.fromName && msg.fromAddr ? `${msg.fromName} <${msg.fromAddr}>` : (msg.fromAddr || msg.fromName || 'unknown sender');
16
+ const date = msg.dateTs ? new Date(Number(msg.dateTs) < 1e12 ? Number(msg.dateTs) * 1000 : Number(msg.dateTs)).toISOString() : null;
17
+ let body = typeof msg.bodyText === 'string' && msg.bodyText.trim() ? msg.bodyText : null;
18
+ let bodySource = 'text';
19
+ if (!body && typeof msg.bodyHtml === 'string' && msg.bodyHtml.trim()) {
20
+ body = msg.bodyHtml
21
+ .replace(/<(script|style)[\s\S]*?<\/\1>/gi, '')
22
+ .replace(/<br\s*\/?>/gi, '\n').replace(/<\/(p|div|li|tr|h[1-6])>/gi, '\n')
23
+ .replace(/<[^>]+>/g, '')
24
+ .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
25
+ .replace(/\n{3,}/g, '\n\n');
26
+ bodySource = 'html';
27
+ }
28
+ if (!body) { body = msg.snippet || ''; bodySource = 'snippet'; }
29
+ const quoted = String(body).trim().split('\n').map((l) => (l ? `> ${l}` : '>')).join('\n');
30
+ const markdown = `# ${subject}\n\n**From:** ${from} \n${date ? `**Date:** ${date} \n` : ''}**Saved from InboxQ**\n\n${quoted}\n`;
31
+ const properties = { source: 'inboxq', inboxq_message_id: msg.id, email_from: msg.fromAddr || from, ...(date ? { email_date: date } : {}) };
32
+ return { title: subject, markdown, properties, bodySource };
33
+ }
34
+
35
+ /**
36
+ * Unchecked markdown tasks (`- [ ] text`, `* [ ]`, `1. [ ]`) outside code
37
+ * fences, each with a stable short hash so a re-run can skip ones already sent.
38
+ */
39
+ export function extractTasks(markdown) {
40
+ const out = [];
41
+ let fence = false;
42
+ for (const line of String(markdown || '').split('\n')) {
43
+ if (/^\s*(```|~~~)/.test(line)) { fence = !fence; continue; }
44
+ if (fence) continue;
45
+ const m = /^\s*(?:[-*+]|\d+[.)])\s+\[ \]\s+(.+?)\s*$/.exec(line);
46
+ if (m && m[1]) out.push({ text: m[1].slice(0, 200), hash: short(m[1].trim().toLowerCase()) });
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /** The QuoxPlan stream note for a promoted NotesQ note: title, link, and an excerpt. */
52
+ export function promoteBody(note, link) {
53
+ const body = String(note.markdown || '').replace(/^\s*#\s+.+\n+/, '').trim();
54
+ const excerpt = body.length > 1500 ? `${body.slice(0, 1500).trimEnd()}...` : body;
55
+ return `NotesQ: "${note.title || 'Untitled'}" ${link}${excerpt ? `\n\n${excerpt}` : ''}`;
56
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pure NotesQ link helpers (AQ-9D P5). The frontend keeps its own copy and a
3
+ * parity test imports this one, so behaviour here is the contract.
4
+ */
5
+
6
+ // Blank (same length) every region where a title must not be linked: front
7
+ // matter, fenced code, inline code, [[wikilinks]] and [text](url) links.
8
+ function maskNonProse(md) {
9
+ const blank = (m) => m.replace(/[^\n]/g, ' ');
10
+ return String(md || '')
11
+ .replace(/^---\r?\n[\s\S]*?\r?\n---(?=\r?\n|$)/, blank)
12
+ .replace(/(```|~~~)[\s\S]*?(\1|$)/g, blank)
13
+ .replace(/`[^`\n]*`/g, blank)
14
+ .replace(/!?\[\[[^\]\n]*\]\]/g, blank)
15
+ .replace(/!?\[[^\]\n]*\]\([^)\n]*\)/g, blank)
16
+ .replace(/^#[ \t]+[^\n]*/m, blank); // the note's own title heading
17
+ }
18
+
19
+ const escapeRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
20
+
21
+ /**
22
+ * Replace the first plain-text, case-insensitive, word-bounded occurrence of
23
+ * `title` with `[[title]]` (the title exactly as given).
24
+ * @returns {{markdown: string, changed: boolean}}
25
+ */
26
+ export function linkMention(markdown, title) {
27
+ const md = String(markdown ?? '');
28
+ const t = String(title ?? '').trim();
29
+ if (!t) return { markdown: md, changed: false };
30
+ const re = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRe(t)}(?![\\p{L}\\p{N}_])`, 'iu');
31
+ const m = re.exec(maskNonProse(md));
32
+ if (!m) return { markdown: md, changed: false };
33
+ return { markdown: `${md.slice(0, m.index)}[[${t}]]${md.slice(m.index + m[0].length)}`, changed: true };
34
+ }
35
+
36
+ /** Append `- [[title]]` to the `## See also` section, creating it at the end if absent. */
37
+ export function addSeeAlso(markdown, title) {
38
+ const md = String(markdown ?? '');
39
+ const link = `[[${title}]]`;
40
+ const item = `- ${link}`;
41
+ const lines = md.split('\n');
42
+ // Lines inside fenced code never count as headings.
43
+ const fenced = [];
44
+ let open = null;
45
+ for (const l of lines) {
46
+ const f = /^\s*(```|~~~)/.exec(l);
47
+ if (open) { fenced.push(true); if (f && f[1] === open) open = null; } else if (f) { fenced.push(true); open = f[1]; } else fenced.push(false);
48
+ }
49
+ const start = lines.findIndex((l, i) => !fenced[i] && /^## See also\s*$/.test(l));
50
+ if (start >= 0) {
51
+ let end = lines.length;
52
+ for (let i = start + 1; i < lines.length; i++) if (!fenced[i] && /^#{1,6}\s/.test(lines[i])) { end = i; break; }
53
+ if (lines.slice(start + 1, end).some((l) => l.includes(link))) return md;
54
+ let last = end - 1;
55
+ while (last > start && lines[last].trim() === '') last--;
56
+ lines.splice(last + 1, 0, item);
57
+ return lines.join('\n');
58
+ }
59
+ return `${md.replace(/\n+$/, '')}\n\n## See also\n\n${item}\n`;
60
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Notification commands
3
+ *
4
+ * quox notification channels list|set
5
+ */
6
+
7
+ import { formatOutput, formatDetail } from '../core/output.js';
8
+ import { resolveEndpoint } from '../api/endpoints.js';
9
+
10
+ export function registerNotificationCommands(program, ctx) {
11
+ const { client, auth } = ctx;
12
+
13
+ const notification = program
14
+ .command('notification')
15
+ .description('Manage notification settings');
16
+
17
+ const channels = notification
18
+ .command('channels')
19
+ .description('Notification channel management');
20
+
21
+ channels
22
+ .command('list')
23
+ .description('List notification channels')
24
+ .action(async () => {
25
+ auth.requireAuth();
26
+ const data = await client.get(resolveEndpoint('users.me.notification-channels.list').path, { service: resolveEndpoint('users.me.notification-channels.list').service });
27
+ const chans = data.channels || data || [];
28
+ formatOutput({
29
+ // fleet #392: rows carry channel_type and a nested config object;
30
+ // `type` and a flat `destination` don't exist on them.
31
+ columns: ['type', 'enabled', 'destination'],
32
+ headers: ['Type', 'Enabled', 'Destination'],
33
+ rows: chans.map(c => ({
34
+ type: c.channel_type ?? c.type,
35
+ enabled: c.enabled ? 'Yes' : 'No',
36
+ destination: c.config?.destination ?? c.destination ?? '-',
37
+ })),
38
+ }, { format: program.opts().output });
39
+ });
40
+
41
+ channels
42
+ .command('set')
43
+ .description('Configure a notification channel')
44
+ .requiredOption('--type <type>', 'Channel type (email|slack|telegram|...)')
45
+ .option('--enable', 'Enable channel')
46
+ .option('--disable', 'Disable channel')
47
+ .option('--destination <dest>', 'Channel destination (email, webhook URL, chat ID)')
48
+ .action(async (opts) => {
49
+ auth.requireAuth();
50
+ const body = {};
51
+ if (opts.enable) body.enabled = true;
52
+ if (opts.disable) body.enabled = false;
53
+ if (opts.destination) body.config = { destination: opts.destination };
54
+ const data = await client.put(resolveEndpoint('users.me.notification-channels.update', { type: opts.type }).path, { service: resolveEndpoint('users.me.notification-channels.update', { type: opts.type }).service, body });
55
+ formatDetail(data, { format: program.opts().output, title: 'Channel Updated' });
56
+ });
57
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `quox nucleus` — QUOXIFY PASS 2 (DEVSUITE phase 15).
3
+ *
4
+ * RepoBrain has been the cheap way to answer structural questions about the
5
+ * Quox repos for a while (targeted snippets instead of whole files), but it
6
+ * was reachable only through its own standalone binary and, for agents, a raw
7
+ * tool. The quoxification audit called that out twice: no unified `quox`
8
+ * verb (S-Q3), and an ungoverned side-path where the agent tool hit the
9
+ * engine directly while the CLI did not (S-Q5).
10
+ *
11
+ * This verb closes the CLI half, and closes it the RIGHT way: it goes through
12
+ * the collector's governed /api/v1/tools/execute, so the query is org-scoped
13
+ * and audited exactly like every other tool call, rather than reaching the
14
+ * engine behind the collector's back.
15
+ *
16
+ * Announce convention: RepoBrain surfaces announce themselves with 🧠 (see
17
+ * the icon registry in CLAUDE.md), so a query is visible in a wall of
18
+ * terminal output.
19
+ */
20
+
21
+ import { formatDetail, printWarning } from '../core/output.js';
22
+ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
23
+ import { resolveEndpoint } from '../api/endpoints.js';
24
+ import { resolveOrgId } from './helpers.js';
25
+
26
+ export function registerNucleusCommands(program, { client, auth }) {
27
+ const nucleus = program
28
+ .command('nucleus')
29
+ .description('Ask the codebase a question (RepoBrain, governed and audited)');
30
+
31
+ nucleus
32
+ .command('query', { isDefault: true })
33
+ .description('Query the codebase and get cited file:line snippets')
34
+ .argument('<question...>', 'the question, e.g. "where is the slot bridge verified?"')
35
+ .option('-t, --target <repo>', 'repo to search (default: quox-dashboard)')
36
+ .option('-n, --limit <n>', 'max snippets to return', '8')
37
+ .action(async (questionParts, options) => {
38
+ auth.requireAuth();
39
+ const question = questionParts.join(' ').trim();
40
+ if (!question) throw new Error('ask something: quox nucleus "where is X?"');
41
+
42
+ printEvent(glyphLine('repobrain', `query "${question}"`));
43
+ const started = Date.now();
44
+
45
+ const ep = resolveEndpoint('v1.tools.execute');
46
+ const body = {
47
+ tool_name: 'codebase_query',
48
+ input: {
49
+ query: question,
50
+ ...(options.target ? { repo: options.target } : {}),
51
+ ...(options.limit ? { limit: parseInt(options.limit, 10) || 8 } : {}),
52
+ },
53
+ };
54
+ const orgId = resolveOrgId(auth, program);
55
+ if (orgId) body.org_id = orgId;
56
+
57
+ const data = await client.post(ep.path, { service: ep.service, body, timeout: 60000 });
58
+ const ms = Date.now() - started;
59
+
60
+ // The tool result shape varies by engine version, so normalise rather
61
+ // than assuming: an unexpected shape must still print SOMETHING true.
62
+ const result = data && (data.result || data.output || data);
63
+ const results = Array.isArray(result?.results) ? result.results
64
+ : Array.isArray(result?.snippets) ? result.snippets
65
+ : Array.isArray(result) ? result
66
+ : null;
67
+
68
+ if (program.opts().output === 'json') {
69
+ formatDetail(data, { format: 'json' });
70
+ return;
71
+ }
72
+
73
+ if (!results || results.length === 0) {
74
+ printEvent(glyphLine('repobrain', `query "${question}"`, { result: '0 hits', ms }));
75
+ printWarning('Nothing matched. Try naming a concrete symbol, filename or behaviour rather than a general description.');
76
+ // Still show the raw payload when the shape was unexpected, so a
77
+ // parsing gap here never looks like an empty index.
78
+ if (result && !results) formatDetail(result, { format: program.opts().output });
79
+ return;
80
+ }
81
+
82
+ printEvent(glyphLine('repobrain', `query "${question}"`, { result: `${results.length} hits`, ms }));
83
+ for (const r of results) {
84
+ const loc = r.file || r.path || r.source || '(unknown file)';
85
+ const line = r.line || r.start_line || null;
86
+ console.log('');
87
+ console.log(` ${loc}${line ? `:${line}` : ''}`);
88
+ const snippet = r.snippet || r.content || r.text || '';
89
+ for (const l of String(snippet).split('\n').slice(0, 20)) console.log(` ${l}`);
90
+ }
91
+ });
92
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Objective commands: create, list, get, start, approve, plus the HITL
3
+ * checkpoint verbs (checkpoints, approve-checkpoint, deny-checkpoint).
4
+ *
5
+ * Objectives are served by the tasks service (port 3104), not the auth
6
+ * service. Org/user scoping comes from the JWT (org_id/sub claims) rather
7
+ * than a URL path segment — see services/tasks/middleware/auth.js.
8
+ *
9
+ * Two approval gates exist and must not be confused:
10
+ * `approve` resolves a decomposed objective's DAG plan.
11
+ * `approve-checkpoint` resolves one HITL checkpoint (pre-start gate or a
12
+ * live loop gate: budget, risk, interval).
13
+ */
14
+
15
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
16
+ import { parseJSON } from './helpers.js';
17
+ import { resolveEndpoint } from '../api/endpoints.js';
18
+
19
+ /**
20
+ * Format a timestamp as a short relative-time string ("5m ago").
21
+ *
22
+ * Checkpoint rows carry SQLite `datetime('now')` values ("2026-08-14
23
+ * 04:00:00"), which are UTC but have no zone marker, so Node would read them
24
+ * as local time and skew the age by the offset. Normalise that shape to ISO
25
+ * UTC first; anything already ISO passes straight through.
26
+ */
27
+ function relativeTime(ts) {
28
+ if (!ts) return '—';
29
+ const iso = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(ts)
30
+ ? `${ts.replace(' ', 'T')}Z`
31
+ : ts;
32
+ const then = new Date(iso).getTime();
33
+ if (Number.isNaN(then)) return '—';
34
+ const diffSec = Math.max(0, Math.floor((Date.now() - then) / 1000));
35
+ if (diffSec < 60) return `${diffSec}s ago`;
36
+ const diffMin = Math.floor(diffSec / 60);
37
+ if (diffMin < 60) return `${diffMin}m ago`;
38
+ const diffHr = Math.floor(diffMin / 60);
39
+ if (diffHr < 24) return `${diffHr}h ago`;
40
+ const diffDay = Math.floor(diffHr / 24);
41
+ return `${diffDay}d ago`;
42
+ }
43
+
44
+ /**
45
+ * Accumulate repeated `--capability-scope` flags into one array of raw
46
+ * strings. Each value may itself be a comma-separated list; splitting and
47
+ * trimming happen in the action, alongside the other body fields.
48
+ */
49
+ function collectScope(value, previous) {
50
+ return (previous || []).concat(value);
51
+ }
52
+
53
+ export function registerObjectiveCommands(program, ctx) {
54
+ const { client, auth } = ctx;
55
+
56
+ const objective = program
57
+ .command('objective')
58
+ .description('Manage objectives (autonomous multi-step goals)');
59
+
60
+ objective
61
+ .command('create')
62
+ .description('Create a new objective (DAG plan by default, or a QLoop loop with --execution-mode loop)')
63
+ .argument('<goal>', 'objective title / goal statement')
64
+ .option('--description <text>', 'longer description')
65
+ .option('--team <id>', 'team ID to run the objective under')
66
+ .option('--max-tokens <n>', 'max token budget')
67
+ .option('--max-tool-calls <n>', 'max tool call budget')
68
+ .option('--max-duration-ms <n>', 'max wall-clock duration in ms')
69
+ .option('--require-approval', 'require approval before starting')
70
+ .option('--checkpoint', 'checkpoint after each task')
71
+ .option('--goal-condition <text>', 're-observe loop exit condition')
72
+ .option('--max-iterations <n>', 'max re-observe loop iterations')
73
+ .option('--metadata <json>', 'arbitrary metadata as JSON')
74
+ .option('--execution-mode <mode>', 'dag|loop. A loop runs a QLoop backend instead of a reviewable DAG plan, so it ALWAYS requires a before_start approval (see `quox objective checkpoints` and `approve-checkpoint`)')
75
+ .option('--loop-backend <backend>', 'claude|codex, loop mode only (default claude)')
76
+ .option('--capability-scope <tools>', 'comma-separated tool names the loop may call (repeatable); empty scope escalates every tool', collectScope)
77
+ .option('--system-prompt <text>', 'per-objective system prompt, carried verbatim to the run')
78
+ .action(async (goal, options) => {
79
+ auth.requireAuth();
80
+ const body = { title: goal };
81
+ if (options.description) body.description = options.description;
82
+ if (options.team) body.team_id = options.team;
83
+ if (options.maxTokens) body.max_tokens = Number(options.maxTokens);
84
+ if (options.maxToolCalls) body.max_tool_calls = Number(options.maxToolCalls);
85
+ if (options.maxDurationMs) body.max_duration_ms = Number(options.maxDurationMs);
86
+ if (options.requireApproval) body.require_approval_before_start = true;
87
+ if (options.checkpoint) body.checkpoint_after_each_task = true;
88
+ if (options.goalCondition) body.goal_condition = options.goalCondition;
89
+ if (options.maxIterations) body.max_iterations = Number(options.maxIterations);
90
+ if (options.metadata) body.metadata = parseJSON(options.metadata, '--metadata');
91
+ if (options.executionMode) body.execution_mode = options.executionMode;
92
+ if (options.loopBackend) body.loop_backend = options.loopBackend;
93
+ if (options.capabilityScope) {
94
+ const scope = options.capabilityScope
95
+ .flatMap((raw) => raw.split(','))
96
+ .map((t) => t.trim())
97
+ .filter(Boolean);
98
+ if (scope.length > 0) body.capability_scope = scope;
99
+ }
100
+ if (options.systemPrompt) body.system_prompt = options.systemPrompt;
101
+
102
+ const data = await client.post(resolveEndpoint('objectives.create').path, { service: resolveEndpoint('objectives.create').service, body });
103
+ printSuccess(`Objective created: ${data.id}`);
104
+ });
105
+
106
+ objective
107
+ .command('list')
108
+ .description('List objectives')
109
+ .option('--status <status>', 'filter by status')
110
+ .option('--team <id>', 'filter by team ID')
111
+ .option('--limit <n>', 'max results', '50')
112
+ .action(async (options) => {
113
+ auth.requireAuth();
114
+ const query = { limit: options.limit };
115
+ if (options.status) query.status = options.status;
116
+ if (options.team) query.team_id = options.team;
117
+ const data = await client.get(resolveEndpoint('objectives.list').path, { service: resolveEndpoint('objectives.list').service, query });
118
+ formatOutput({
119
+ columns: ['id', 'title', 'status', 'team_id', 'created_at'],
120
+ headers: ['ID', 'Title', 'Status', 'Team', 'Created'],
121
+ rows: data.objectives || data,
122
+ }, { format: program.opts().output });
123
+ });
124
+
125
+ objective
126
+ .command('get')
127
+ .description('Get objective details (tasks, checkpoints, DAG)')
128
+ .argument('<id>', 'objective ID')
129
+ .action(async (id) => {
130
+ auth.requireAuth();
131
+ const data = await client.get(resolveEndpoint('objectives.get', { id }).path, { service: resolveEndpoint('objectives.get', { id }).service });
132
+ formatDetail(data, { format: program.opts().output, title: data.title });
133
+ });
134
+
135
+ objective
136
+ .command('start')
137
+ .description('Start an objective (decompose into tasks)')
138
+ .argument('<id>', 'objective ID')
139
+ .option('--skip-checkpoint', 'bypass pre-start approval checkpoint')
140
+ .action(async (id, options) => {
141
+ auth.requireAuth();
142
+ const body = {};
143
+ if (options.skipCheckpoint) body.skip_checkpoint = true;
144
+ const data = await client.post(resolveEndpoint('objectives.start', { id }).path, { service: resolveEndpoint('objectives.start', { id }).service, body });
145
+ printSuccess(`Objective ${id}: ${data.status}${data.message ? ` — ${data.message}` : ''}`);
146
+ });
147
+
148
+ objective
149
+ .command('approve')
150
+ .description('Approve a decomposed objective\'s plan (starts execution)')
151
+ .argument('<id>', 'objective ID')
152
+ .action(async (id) => {
153
+ auth.requireAuth();
154
+ const data = await client.post(resolveEndpoint('objectives.approve-plan', { id }).path, { service: resolveEndpoint('objectives.approve-plan', { id }).service });
155
+ printSuccess(`Objective ${id}: ${data.status}${data.message ? ` — ${data.message}` : ''}`);
156
+ });
157
+
158
+ objective
159
+ .command('checkpoints')
160
+ .description('List pending HITL checkpoints across the org (approval queue)')
161
+ .option('--include-reviewed', 'include checkpoints that have already been reviewed')
162
+ .option('--limit <n>', 'max results', '100')
163
+ .action(async (options) => {
164
+ auth.requireAuth();
165
+ const query = { limit: options.limit };
166
+ if (options.includeReviewed) query.include_reviewed = 'true';
167
+ const data = await client.get(resolveEndpoint('objectives.checkpoints-pending').path, { service: resolveEndpoint('objectives.checkpoints-pending').service, query });
168
+ const rows = (data.checkpoints || data || []).map((cp) => ({
169
+ id: cp.id,
170
+ objective_title: cp.objectiveTitle,
171
+ checkpoint_type: cp.checkpointType,
172
+ age: relativeTime(cp.createdAt),
173
+ title: cp.title,
174
+ }));
175
+ formatOutput({
176
+ columns: ['id', 'objective_title', 'checkpoint_type', 'age', 'title'],
177
+ headers: ['Checkpoint', 'Objective', 'Type', 'Age', 'Title'],
178
+ rows,
179
+ }, { format: program.opts().output, emptyMessage: 'No pending checkpoints.' });
180
+ });
181
+
182
+ objective
183
+ .command('approve-checkpoint')
184
+ .description('Approve one HITL checkpoint (not the DAG plan, see `approve`)')
185
+ .argument('<objectiveId>', 'objective ID')
186
+ .argument('<checkpointId>', 'checkpoint ID')
187
+ .option('--notes <text>', 'review notes recorded against the decision')
188
+ .action(async (objectiveId, checkpointId, options) => {
189
+ auth.requireAuth();
190
+ const body = {};
191
+ if (options.notes) body.notes = options.notes;
192
+ const params = { id: objectiveId, checkpointId };
193
+ const data = await client.post(resolveEndpoint('objectives.checkpoint-approve', params).path, { service: resolveEndpoint('objectives.checkpoint-approve', params).service, body });
194
+ printSuccess(`Checkpoint ${checkpointId}: ${data.status}${data.objective_status ? ` (objective ${data.objective_status})` : ''}`);
195
+ });
196
+
197
+ objective
198
+ .command('deny-checkpoint')
199
+ .description('Reject one HITL checkpoint (not the DAG plan, see `approve`)')
200
+ .argument('<objectiveId>', 'objective ID')
201
+ .argument('<checkpointId>', 'checkpoint ID')
202
+ .option('--notes <text>', 'review notes recorded against the decision')
203
+ .action(async (objectiveId, checkpointId, options) => {
204
+ auth.requireAuth();
205
+ const body = {};
206
+ if (options.notes) body.notes = options.notes;
207
+ const params = { id: objectiveId, checkpointId };
208
+ const data = await client.post(resolveEndpoint('objectives.checkpoint-reject', params).path, { service: resolveEndpoint('objectives.checkpoint-reject', params).service, body });
209
+ printSuccess(`Checkpoint ${checkpointId}: ${data.status}${data.objective_status ? ` (objective ${data.objective_status})` : ''}`);
210
+ });
211
+
212
+ objective
213
+ .command('halt')
214
+ .description('Halt a running QLoop loop ON DEMAND (unconditional human stop; no pending gate needed). Fires the same signed-kill as a gate deny.')
215
+ .argument('<objectiveId>', 'loop objective ID')
216
+ .option('--notes <text>', 'reason recorded against the halt')
217
+ .action(async (objectiveId, options) => {
218
+ auth.requireAuth();
219
+ const body = {};
220
+ if (options.notes) body.notes = options.notes;
221
+ const params = { id: objectiveId };
222
+ const data = await client.post(resolveEndpoint('objectives.halt', params).path, { service: resolveEndpoint('objectives.halt', params).service, body });
223
+ if (data.status === 'halted') {
224
+ printSuccess(`Loop ${objectiveId} halted (interrupted=${data.interrupted}, objective ${data.objective_status})`);
225
+ } else {
226
+ printSuccess(`Loop ${objectiveId}: ${data.status}${data.reason ? ` (${data.reason})` : ''}`);
227
+ }
228
+ });
229
+ }