quoxcli 0.3.1 → 0.5.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 (306) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `quox blog draft submit|list|show|mark-published` (BLOG-DRAFT).
3
+ *
4
+ * Submit saves the draft as a NotesQ note in the blog-drafts collection and files a hand-off to
5
+ * the blog publisher (a line in the publisher's slot plus an inbox item). It NEVER publishes:
6
+ * publishing is the publisher's governed pipeline after the owner's yes. `mark-published` is the
7
+ * publisher-side verb that records the URL and tells the originating conversation/objective.
8
+ */
9
+
10
+ import { readFileSync } from 'fs';
11
+ import { formatOutput, formatDetail, printSuccess, printInfo } from '../core/output.js';
12
+ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
13
+ import { resolveEndpoint } from '../api/endpoints.js';
14
+
15
+ const isJson = (program) => program.opts().output === 'json';
16
+ const printJson = (d) => console.log(JSON.stringify(d, null, 2));
17
+
18
+ async function readBody(options, ctx) {
19
+ if (options.file) return readFileSync(options.file, 'utf-8');
20
+ if (options.stdin) {
21
+ const input = ctx.stdin || process.stdin;
22
+ let buf = '';
23
+ for await (const chunk of input) buf += chunk;
24
+ return buf;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ export function registerBlogCommands(program, ctx) {
30
+ const { client, auth } = ctx;
31
+ const blog = program.command('blog').description('Blog: hand drafts to the blog publisher (never publishes)');
32
+ const draft = blog.command('draft').description('Blog drafts: saved as revisioned NotesQ notes in the blog-drafts collection');
33
+
34
+ draft
35
+ .command('submit')
36
+ .description('Save a draft and hand it to the blog publisher (slot + inbox). Does NOT publish.')
37
+ .requiredOption('--title <title>', 'post title')
38
+ .option('--file <path>', 'read the post markdown from a file')
39
+ .option('--stdin', 'read the post markdown from stdin')
40
+ .option('--hero <image>', 'hero image path or URL')
41
+ .option('--tag <tag...>', 'tag (repeatable)')
42
+ .option('--conversation <id>', 'originating conversation id (so its publication is reported back)')
43
+ .option('--objective <id>', 'originating objective id')
44
+ .action(async (options) => {
45
+ auth.requireAuth();
46
+ const markdown = await readBody(options, ctx);
47
+ if (markdown === null) throw new Error('give the post body with --file <path> or --stdin');
48
+ if (!markdown.trim()) throw new Error('the draft body is empty; nothing to submit');
49
+ const body = { title: options.title, markdown };
50
+ if (options.hero) body.hero = options.hero;
51
+ if (options.tag && options.tag.length) body.tags = options.tag;
52
+ if (options.conversation) body.conversation_id = options.conversation;
53
+ if (options.objective) body.objective_id = options.objective;
54
+ const ep = resolveEndpoint('blog.drafts.submit');
55
+ const data = await client.post(ep.path, { service: ep.service, body });
56
+ if (isJson(program)) return printJson(data);
57
+ printEvent(glyphLine('quox', `blog draft submit "${options.title}"`, `note ${data.note_id}`));
58
+ printSuccess(`Draft saved as note ${data.note_id} and handed to the publisher slot ${data.slot} (hand-off ${data.handoff_id}).`);
59
+ if (data.link) console.log(`link: ${data.link}`);
60
+ printInfo('Not published: nothing goes live until the publisher publishes after the owner approves.');
61
+ });
62
+
63
+ draft
64
+ .command('list')
65
+ .description('List blog drafts')
66
+ .option('--status <status>', 'draft | published')
67
+ .action(async (options) => {
68
+ auth.requireAuth();
69
+ const ep = resolveEndpoint('blog.drafts.list');
70
+ const data = await client.get(ep.path, { service: ep.service, query: options.status ? { status: options.status } : {} });
71
+ if (isJson(program)) return printJson(data);
72
+ const rows = (data.drafts || []).map((d) => ({ id: d.id, title: d.title, status: d.status, url: d.url || '', updated: d.updated || '' }));
73
+ if (!rows.length) return printInfo('No blog drafts.');
74
+ formatOutput({ columns: ['id', 'title', 'status', 'url', 'updated'], rows });
75
+ });
76
+
77
+ draft
78
+ .command('show')
79
+ .description('Show one blog draft (body and hand-off facts)')
80
+ .argument('<id>', 'draft (note) id')
81
+ .action(async (id) => {
82
+ auth.requireAuth();
83
+ const ep = resolveEndpoint('blog.drafts.get', { id });
84
+ const data = await client.get(ep.path, { service: ep.service });
85
+ if (isJson(program)) return printJson(data);
86
+ const d = data.draft || {};
87
+ const { markdown, ...facts } = d;
88
+ formatDetail(facts);
89
+ console.log('');
90
+ console.log(markdown || '');
91
+ });
92
+
93
+ draft
94
+ .command('mark-published')
95
+ .description('Publisher side: record the published URL and tell the originating conversation/objective')
96
+ .argument('<id>', 'draft (note) id')
97
+ .requiredOption('--url <url>', 'the published https URL')
98
+ .action(async (id, options) => {
99
+ auth.requireAuth();
100
+ if (!/^https:\/\//i.test(options.url)) throw new Error('--url must be an https URL');
101
+ const ep = resolveEndpoint('blog.drafts.published', { id });
102
+ const data = await client.post(ep.path, { service: ep.service, body: { url: options.url } });
103
+ if (isJson(program)) return printJson(data);
104
+ printSuccess(data.already ? `Draft ${id} was already marked published at ${data.url}.` : `Draft ${id} marked published at ${data.url}.`);
105
+ if (data.notified) {
106
+ const n = data.notified;
107
+ printInfo(`Told: ${[n.conversation_id && `conversation ${n.conversation_id}`, n.objective_id && `objective ${n.objective_id}`, n.inbox_item_id && `inbox ${n.inbox_item_id}`].filter(Boolean).join(', ') || 'inbox only'}.`);
108
+ }
109
+ });
110
+ }
@@ -8,18 +8,19 @@
8
8
  */
9
9
 
10
10
  import { resolveEndpoint } from '../api/endpoints.js';
11
- import { capabilityLines, findCapability, offerFor } from '../quoxon/capabilities.js';
11
+ import { capabilityLines, findCapability, offerFor, needsSetupNames } from '../quoxon/capabilities.js';
12
12
 
13
13
  export function registerCapabilitiesCommands(program, ctx) {
14
14
  const { client, auth } = ctx;
15
15
  program
16
16
  .command('capabilities [name]')
17
17
  .description('What this Quox can do for your org: ready, needs setup, not available, planned')
18
+ .addHelpText('after', '\nAdd --json for the full list (id, name, availability, reason, setup, permissions_ok).')
18
19
  .action(async (name) => {
19
20
  auth.requireAuth();
20
21
  const ep = resolveEndpoint('v1.capabilities');
21
22
  const data = await client.get(ep.path, { service: ep.service });
22
- if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
23
+ if (program.opts().output === 'json') { console.log(JSON.stringify({ needs_setup: needsSetupNames(data?.capabilities), ...data }, null, 2)); return; }
23
24
  const res = { ok: Array.isArray(data?.capabilities), capabilities: data?.capabilities, error: 'unexpected response' };
24
25
  if (name) {
25
26
  const c = findCapability(name, res);
@@ -4,7 +4,12 @@
4
4
 
5
5
  import chalk from 'chalk';
6
6
  import { formatDetail } from '../core/output.js';
7
+ import { eventLine, announceMuted } from '../core/announce.js';
7
8
  import { resolveEndpoint } from '../api/endpoints.js';
9
+ import { existsSync } from 'node:fs';
10
+ import { lastConversation, saveLastConversation } from './chatState.js';
11
+ import { loadChatCompanion } from './chatCompanion.js';
12
+ import { companionPlan, motionPolicy, VENDORED_DRAGONLAB } from '../dragon/select.js';
8
13
 
9
14
  /**
10
15
  * Build the POST body for /chat/stream (quox#782). The prompt and context
@@ -66,6 +71,14 @@ function printTrace(ev) {
66
71
  if (f) process.stderr.write((TONE_PAINT[f.tone] || chalk.cyan)(f.text) + '\n');
67
72
  }
68
73
 
74
+ /** A turn's quox_event frame as its QuoxGlyph line on stderr (stdout stays the answer); silent under --json/--quiet. */
75
+ export function printQuoxEvent(ev) {
76
+ if (announceMuted()) return null;
77
+ const q = eventLine(ev);
78
+ if (q) process.stderr.write(`${q}\n`);
79
+ return q;
80
+ }
81
+
69
82
  function generateQueryId() {
70
83
  return `CLI_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
71
84
  }
@@ -74,9 +87,10 @@ function generateQueryId() {
74
87
  * Consume a chat SSE stream, printing chunks as they arrive.
75
88
  * Returns { text, sessionId, agentUsed } once the stream completes.
76
89
  */
77
- export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel, trace = false } = {}) {
90
+ export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel, trace = false, companionPath, companionDisplay } = {}) {
78
91
  const queryId = generateQueryId();
79
92
  const body = buildStreamQuery({ queryId, message, agent, sessionId, context });
93
+ body.turn_id = queryId; // an explicit turn id selects the v1 event stream: tool and quox_event frames only flow with it
80
94
 
81
95
  let text = '';
82
96
  let resultSessionId = sessionId;
@@ -90,19 +104,25 @@ export async function consumeStream(client, { message, agent, sessionId, context
90
104
  onAgentLabel?.(agentUsed);
91
105
  };
92
106
 
93
- const iter = client.stream(resolveEndpoint('chat.stream').path, { service: resolveEndpoint('chat.stream').service, body, untilClose: Boolean(trace) })[Symbol.asyncIterator]();
107
+ const companion = await loadChatCompanion(companionPath, { display: companionDisplay });
108
+ const write = text => companion ? companion.write(text) : process.stdout.write(text);
109
+ // Native terminal status follows legacy `done`; observe it when opted in.
110
+ let iter;
111
+ try {
112
+ iter = client.stream(resolveEndpoint('chat.stream').path, { service: resolveEndpoint('chat.stream').service, body, untilClose: Boolean(trace || companion) })[Symbol.asyncIterator]();
94
113
  while (true) {
95
114
  let step;
96
115
  try {
97
116
  step = await iter.next();
98
117
  } catch (err) {
99
118
  // A refusal before the stream opened carries the layers it really evaluated.
100
- if (trace && Array.isArray(err?.body?.aocl_layers)) for (const l of err.body.aocl_layers) printTrace({ type: 'aocl.layer', ...l });
119
+ if (trace && Array.isArray(err?.body?.aocl_layers)) { companion?.suspend(); for (const l of err.body.aocl_layers) printTrace({ type: 'aocl.layer', ...l }); }
101
120
  throw err;
102
121
  }
103
122
  if (step.done) break;
104
123
  const event = step.value;
105
- if (trace) printTrace(event);
124
+ companion?.event(event);
125
+ if (trace) { companion?.suspend(); printTrace(event); }
106
126
  switch (event.type) {
107
127
  case 'connected':
108
128
  break;
@@ -110,9 +130,9 @@ export async function consumeStream(client, { message, agent, sessionId, context
110
130
  case 'thinking': {
111
131
  const stage = event.stage || '';
112
132
  if (stage === 'tool_call' || stage.includes('tool')) {
113
- console.log(chalk.dim(` ⚙ using tool: ${event.message}`));
133
+ write(chalk.dim(` ⚙ using tool: ${event.message}`) + '\n');
114
134
  } else if (process.argv.includes('--verbose') || process.argv.includes('-v')) {
115
- console.log(chalk.dim(` … ${event.message}`));
135
+ write(chalk.dim(` … ${event.message}`) + '\n');
116
136
  }
117
137
  break;
118
138
  }
@@ -120,19 +140,23 @@ export async function consumeStream(client, { message, agent, sessionId, context
120
140
  case 'chunk':
121
141
  printLabel();
122
142
  sawChunk = true;
123
- process.stdout.write(event.content || '');
143
+ write(event.content || '');
124
144
  text += event.content || '';
125
145
  break;
126
146
 
127
147
  case 'done':
128
148
  if (!sawChunk && event.full_response) {
129
149
  printLabel();
130
- process.stdout.write(event.full_response);
150
+ write(event.full_response);
131
151
  text = event.full_response;
132
152
  }
133
153
  if (event.session_id) resultSessionId = event.session_id;
134
154
  if (event.agent_used) agentUsed = event.agent_used;
135
- console.log();
155
+ write('\n');
156
+ break;
157
+
158
+ case 'quox_event':
159
+ printQuoxEvent(event);
136
160
  break;
137
161
 
138
162
  case 'error':
@@ -142,28 +166,99 @@ export async function consumeStream(client, { message, agent, sessionId, context
142
166
  break;
143
167
  }
144
168
  }
169
+ } catch (err) {
170
+ companion?.event({ type: 'error' });
171
+ throw err;
172
+ } finally {
173
+ await companion?.close();
174
+ try { await iter?.return?.(); } catch { /* cleanup must not mask a stream error */ }
175
+ }
145
176
 
146
177
  return { text, sessionId: resultSessionId, agentUsed };
147
178
  }
148
179
 
149
- /** One-shot (non-streaming) chat via POST /chat/claude. */
150
- async function sendOneShot(client, { message, agent, sessionId }) {
151
- const body = { prompt: message };
152
- if (agent) body.selected_agent = agent;
153
- if (sessionId) body.session_id = sessionId;
180
+ /** The words for a failed turn: the server's plain message first, the bare error code only as a fallback. */
181
+ export function turnFailureText(result, httpStatus) {
182
+ return result?.message || result?.error || `chat turn failed (HTTP ${httpStatus})`;
183
+ }
154
184
 
155
- const data = await client.post(resolveEndpoint('chat.claude').path, {
156
- service: resolveEndpoint('chat.claude').service,
157
- body,
158
- timeout: 120000,
159
- });
185
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
186
+
187
+ /**
188
+ * Poll a server-side chat turn until it is done. The turn keeps running on the server whatever this
189
+ * client does, so giving up early would hide a result (and any objects it created) from the user.
190
+ */
191
+ async function awaitTurn(client, turnId) {
192
+ const capMs = Number(process.env.QUOX_CHAT_TURN_TIMEOUT_MS) || 20 * 60 * 1000;
193
+ const fixed = Number(process.env.QUOX_CHAT_POLL_MS) || 0;
194
+ const started = Date.now();
195
+ let wait = fixed || 1500;
196
+ let failures = 0;
197
+ const ep = resolveEndpoint('chat.turn.get', { id: turnId });
198
+ while (true) {
199
+ let t;
200
+ try {
201
+ t = await client.get(ep.path, { service: ep.service });
202
+ failures = 0;
203
+ } catch (err) {
204
+ if (err?.status === 404 || /not.found/i.test(err?.message || '')) {
205
+ throw new Error(`The server no longer has chat turn ${turnId} (expired, or the collector restarted). Check \`quox conversation list\` to see whether it finished.`);
206
+ }
207
+ if (++failures >= 5) throw err;
208
+ t = { status: 'running' };
209
+ }
210
+ if (t.status === 'done') {
211
+ if (t.http_status >= 400) throw new Error(turnFailureText(t.result, t.http_status));
212
+ return t.result || {};
213
+ }
214
+ if (t.status === 'error') throw new Error(`chat turn ${turnId} failed on the server: ${t.error || 'unknown error'}`);
215
+ if (Date.now() - started > capMs) {
216
+ throw new Error(`Chat turn ${turnId} is still running after ${Math.round(capMs / 1000)}s. It has not been lost: fetch it with \`quox chat --resume ${turnId}\`.`);
217
+ }
218
+ await sleep(wait);
219
+ if (!fixed) wait = Math.min(wait + 500, 5000);
220
+ }
221
+ }
222
+
223
+ /** One-shot (non-streaming) chat via POST /chat/claude, kept server side as a pollable turn. */
224
+ async function sendOneShot(client, { message, agent, sessionId, resumeTurn }) {
225
+ let data;
226
+ if (resumeTurn) {
227
+ data = await awaitTurn(client, resumeTurn);
228
+ } else {
229
+ const body = { prompt: message };
230
+ if (agent) body.selected_agent = agent;
231
+ if (sessionId) body.session_id = sessionId;
232
+
233
+ // Prefer: respond-async returns 202 {turn_id} at once, so no client timeout can orphan a
234
+ // multi-tool turn. An older collector ignores it and answers synchronously (handled below).
235
+ data = await client.post(resolveEndpoint('chat.claude').path, {
236
+ service: resolveEndpoint('chat.claude').service,
237
+ body,
238
+ headers: { Prefer: 'respond-async' },
239
+ timeout: 30000,
240
+ });
241
+ if (data && data.turn_id && data.status === 'running') data = await awaitTurn(client, data.turn_id);
242
+ }
160
243
 
161
244
  const response = data.result || data.response || data.text || JSON.stringify(data);
162
245
  return { data, response, sessionId: data.session_id || sessionId };
163
246
  }
164
247
 
248
+ /** Explicit --dragon-module wins; otherwise pick the dragon for the attached agent (null path = ordinary chat). */
249
+ export function chooseCompanion({ options, config, env = process.env, isTTY = Boolean(process.stdout.isTTY), exists = existsSync }) {
250
+ const policy = motionPolicy(env);
251
+ if (options.dragonModule) return { companionPath: options.dragonModule, companionDisplay: { still: policy.still, mono: policy.mono } };
252
+ let settings = {};
253
+ try { settings = config?.getActiveProfile?.()?.quoxon || {}; } catch { /* no profile settings */ }
254
+ const e = options.dragon ? { ...env, QUOX_DRAGON_COMPANION: options.dragon } : env;
255
+ const plan = companionPlan({ agent: options.agent, settings, env: e, isTTY, exists, bundled: VENDORED_DRAGONLAB });
256
+ if (!plan.enabled) return { companionPath: undefined, companionDisplay: undefined };
257
+ return { companionPath: plan.file, companionDisplay: { still: plan.still, mono: plan.mono, persist: plan.persist } };
258
+ }
259
+
165
260
  export function registerChatCommands(program, ctx) {
166
- const { client, auth } = ctx;
261
+ const { client, auth, config } = ctx;
167
262
 
168
263
  const chat = program
169
264
  .command('chat')
@@ -172,13 +267,25 @@ export function registerChatCommands(program, ctx) {
172
267
  .option('--agent <name>', 'route to a specific agent (e.g. sentinel, cipher, nova)')
173
268
  .option('--no-stream', 'disable streaming output (one-shot request/response)')
174
269
  .option('--conversation <id>', 'continue a conversation (session id)')
270
+ .option('--continue', 'continue the last conversation for this profile and directory')
271
+ .option('--resume <turn>', 'fetch a chat turn that was still running (its id is printed if a turn outlives the wait)')
175
272
  .option('--trace', 'print the turn\'s protocol events (per-layer AOCL, traces, summary) to stderr as they arrive')
273
+ .option('--dragon <mode>', 'dragon companion for dragon-backed agents: on, greeting or off (default from /dragon companion; art is bundled; QUOX_DRAGONLAB overrides it)')
274
+ .option('--dragon-module <path>', 'opt in to a trusted local dragon companion module (streaming, single-message mode)')
176
275
  .action(async (message, options) => {
177
276
  auth.requireAuth();
178
277
  const streaming = options.stream !== false;
278
+ const profileName = (() => { try { return config?.getActiveProfileName?.(); } catch { return undefined; } })();
279
+ const noteConversation = (id) => {
280
+ if (!id) return;
281
+ saveLastConversation(profileName, process.cwd(), id);
282
+ if (program.opts().output !== 'json') process.stderr.write(`conversation: ${id}\n`);
283
+ };
284
+ if (!options.conversation && options.continue) options.conversation = lastConversation(profileName, process.cwd());
285
+ if (options.dragonModule && (!message || !streaming)) throw new Error('--dragon-module requires a single message with streaming enabled.');
179
286
 
180
- // Interactive mode if no message
181
- if (!message) {
287
+ // Interactive mode if no message (a --resume fetch is not interactive)
288
+ if (!message && !options.resume) {
182
289
  const { default: inquirer } = await import('inquirer');
183
290
  let currentAgent = options.agent || 'quox';
184
291
  let sessionId = options.conversation;
@@ -216,7 +323,7 @@ export function registerChatCommands(program, ctx) {
216
323
  onAgentLabel: (agentUsed) => process.stdout.write(chalk.green(`${(agentUsed || currentAgent).toUpperCase()}: `)),
217
324
  trace: Boolean(options.trace),
218
325
  });
219
- if (result.sessionId) sessionId = result.sessionId;
326
+ if (result.sessionId) { sessionId = result.sessionId; saveLastConversation(profileName, process.cwd(), sessionId); }
220
327
  console.log();
221
328
  } else {
222
329
  const { response, sessionId: newSessionId } = await sendOneShot(client, {
@@ -224,7 +331,7 @@ export function registerChatCommands(program, ctx) {
224
331
  agent: currentAgent,
225
332
  sessionId,
226
333
  });
227
- if (newSessionId) sessionId = newSessionId;
334
+ if (newSessionId) { sessionId = newSessionId; saveLastConversation(profileName, process.cwd(), sessionId); }
228
335
  console.log(chalk.green(`${currentAgent.toUpperCase()}:`), response);
229
336
  console.log();
230
337
  }
@@ -236,16 +343,19 @@ export function registerChatCommands(program, ctx) {
236
343
  }
237
344
 
238
345
  // Single message mode
239
- if (streaming) {
346
+ if (streaming && !options.resume) {
347
+ const companion = chooseCompanion({ options, config });
240
348
  try {
241
- await consumeStream(client, {
349
+ const result = await consumeStream(client, {
242
350
  message,
243
351
  agent: options.agent,
244
352
  sessionId: options.conversation,
245
353
  }, {
246
354
  onAgentLabel: () => {}, // no label prefix in single-shot streaming; just print tokens
247
355
  trace: Boolean(options.trace),
356
+ ...companion,
248
357
  });
358
+ noteConversation(result?.sessionId);
249
359
  } catch (err) {
250
360
  console.error(chalk.red(`Error: ${err.message}`));
251
361
  process.exitCode = 1;
@@ -257,15 +367,18 @@ export function registerChatCommands(program, ctx) {
257
367
  const spinner = ora('Thinking...').start();
258
368
 
259
369
  try {
260
- const { data, response } = await sendOneShot(client, {
370
+ const { data, response, sessionId: convId } = await sendOneShot(client, {
261
371
  message,
262
372
  agent: options.agent,
263
373
  sessionId: options.conversation,
374
+ resumeTurn: options.resume,
264
375
  });
265
376
  spinner.stop();
377
+ noteConversation(convId);
378
+ for (const qe of Array.isArray(data.quox_events) ? data.quox_events : []) printQuoxEvent(qe); // the server's per-turn Quox effects, when it returns them
266
379
 
267
380
  if (program.opts().output === 'json') {
268
- console.log(JSON.stringify(data, null, 2));
381
+ console.log(JSON.stringify({ ...data, conversation_id: convId || null }, null, 2));
269
382
  } else {
270
383
  console.log(response);
271
384
 
@@ -293,3 +406,4 @@ export function registerChatCommands(program, ctx) {
293
406
  });
294
407
  });
295
408
  }
409
+
@@ -0,0 +1,27 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ /** Explicit local opt-in only. A display failure must not interrupt a chat. */
5
+ export async function loadChatCompanion(path, { output = process.stdout, warn = console.error, display = {} } = {}) {
6
+ if (!path || !output.isTTY) return null;
7
+ try {
8
+ const mod = await import(pathToFileURL(resolve(path)).href);
9
+ const c = await mod.createCompanion({ output, ...display });
10
+ if (!c || !['event', 'write', 'close', 'suspend'].every(k => typeof c[k] === 'function')) {
11
+ try { c?.close?.(); } catch {}
12
+ throw new Error('Expected createCompanion() with event/write/close/suspend');
13
+ }
14
+ let active = true;
15
+ const stop = async () => { active = false; try { await c.close(); } catch {} };
16
+ return {
17
+ event(e) { if (active) try { c.event(e); } catch { stop(); } },
18
+ write(text) { if (!active) output.write(text); else c.write(text); },
19
+ // Trace writes to stderr: relinquish the block first to prevent overlap.
20
+ suspend() { if (active) try { c.suspend(); } catch { stop(); } },
21
+ close: stop,
22
+ };
23
+ } catch (e) {
24
+ warn('Dragon companion unavailable; continuing normal chat: ' + e.message);
25
+ return null;
26
+ }
27
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Last conversation per (profile, cwd), so `quox chat --continue` keeps context without the user
3
+ * copying a session id. Best-effort: a read/write failure never breaks a chat turn.
4
+ */
5
+ import { homedir } from 'node:os';
6
+ import { join, dirname } from 'node:path';
7
+ import { readFileSync, writeFileSync, mkdirSync, readlinkSync } from 'node:fs';
8
+
9
+ const stateFile = () => process.env.QUOX_CHAT_STATE || join(homedir(), '.config', 'quox', 'chat-last.json');
10
+ /**
11
+ * Who is calling: $QUOX_SESSION when set, else this terminal's tty, else nothing (a headless
12
+ * caller with no session id falls back to profile + cwd only). Parallel terminals/sessions must
13
+ * never share a "last conversation".
14
+ */
15
+ export function callerScope(env = process.env) {
16
+ if (env.QUOX_SESSION) return `session:${String(env.QUOX_SESSION).slice(0, 64)}`;
17
+ for (const fd of [0, 1, 2]) {
18
+ try {
19
+ const t = readlinkSync(`/proc/self/fd/${fd}`);
20
+ if (/^\/dev\/(pts\/\d+|tty\w*)$/.test(t)) return `tty:${t}`;
21
+ } catch { /* not linux, or fd closed */ }
22
+ }
23
+ return '';
24
+ }
25
+ const keyOf = (profile, cwd) => `${profile || 'default'}|${cwd || process.cwd()}|${callerScope()}`;
26
+
27
+ function read() {
28
+ try { return JSON.parse(readFileSync(stateFile(), 'utf8')) || {}; } catch { return {}; }
29
+ }
30
+
31
+ export function lastConversation(profile, cwd) {
32
+ const v = read()[keyOf(profile, cwd)];
33
+ return typeof v === 'string' && v ? v : undefined;
34
+ }
35
+
36
+ export function saveLastConversation(profile, cwd, id) {
37
+ if (!id) return;
38
+ try {
39
+ const all = read();
40
+ all[keyOf(profile, cwd)] = id;
41
+ const f = stateFile();
42
+ mkdirSync(dirname(f), { recursive: true });
43
+ writeFileSync(f, JSON.stringify(all, null, 2));
44
+ } catch { /* best-effort */ }
45
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * quox claude / codex / gemini / run / sessions: governed tmux sessions. The runner
2
+ * quox claude / codex / run / sessions: governed tmux sessions. The runner
3
3
  * lives in core/governedSession.js; this file holds argument parsing and the
4
4
  * commander wiring. argv arrays only; user input never reaches a shell string.
5
5
  */
@@ -29,6 +29,7 @@ export {
29
29
  */
30
30
  export function parseClaudeArgs(raw) {
31
31
  let noTmux = false;
32
+ let force = false;
32
33
  let as;
33
34
  let slot;
34
35
  const args = [];
@@ -38,6 +39,7 @@ export function parseClaudeArgs(raw) {
38
39
  if (past) { args.push(a); continue; }
39
40
  if (a === '--') { past = true; args.push(a); continue; }
40
41
  if (a === '--no-tmux') noTmux = true;
42
+ else if (a === '--force') force = true;
41
43
  else if ((a === '--as' || a === '--slot') && raw[i + 1] && !raw[i + 1].startsWith('-')) {
42
44
  if (a === '--as') as = raw[i + 1]; else slot = raw[i + 1];
43
45
  i++;
@@ -72,7 +74,7 @@ export function parseClaudeArgs(raw) {
72
74
  }
73
75
  }
74
76
  if (name !== undefined && !validateName(name)) name = undefined;
75
- return { args, name, resume, noTmux, as, slot };
77
+ return { args, name, resume, noTmux, ...(force ? { force } : {}), as, slot };
76
78
  }
77
79
 
78
80
  /**
@@ -102,22 +104,6 @@ export const runClaude = ({ as, slot, ...opts } = {}, over) => gs.runGoverned({
102
104
  program: 'claude',
103
105
  ...(as || slot ? { preattach: { agent: as, slot, session: opts.name } } : {}),
104
106
  }, over);
105
- /**
106
- * quox gemini <args>: verbatim to `gemini`. Gemini resumes by index or
107
- * "latest" (`--resume latest`, `-r 5`), not by name, so the tmux session is
108
- * auto-named; reattach with `quox sessions attach NAME`.
109
- */
110
- export function parseGeminiArgs(raw) {
111
- let noTmux = false;
112
- const args = [];
113
- for (const a of raw) {
114
- if (a === '--no-tmux') noTmux = true;
115
- else args.push(a);
116
- }
117
- const resume = args.some((a) => a === '--resume' || a === '-r' || a.startsWith('--resume='));
118
- return { args, name: undefined, resume, noTmux };
119
- }
120
-
121
107
  /**
122
108
  * quox wrap <program> [args...]: wrap ANY command. Known programs use their
123
109
  * registry entry (so `quox wrap codex` == `quox codex`); unknown ones get the
@@ -135,12 +121,11 @@ export function parseRunArgs(raw) {
135
121
  }
136
122
 
137
123
  export function runRun({ bin, args = [], noTmux = false } = {}, over) {
138
- const known = ['claude', 'codex', 'gemini'].find((k) => PROGRAMS[k].bin === bin);
124
+ const known = ['claude', 'codex'].find((k) => PROGRAMS[k].bin === bin);
139
125
  if (known) return gs.runGoverned({ program: known, args, noTmux }, over);
140
126
  return gs.runGoverned({ program: 'run', bin, args, noTmux }, over);
141
127
  }
142
128
 
143
- export const runGemini = (opts, over) => gs.runGoverned({ ...opts, program: 'gemini' }, over);
144
129
  export const runCodex = (opts, over) => gs.runGoverned({ ...opts, program: 'codex' }, over);
145
130
  export const runTui = (opts, over) => gs.runGoverned({ ...opts, program: 'tui' }, over);
146
131
  export const runHelp = gs.runHelp;
@@ -168,6 +153,7 @@ quox's own flags (consumed here, not passed to claude):
168
153
  --as <agent> start already attached to a Quox agent
169
154
  --slot <name> start already holding a dev slot
170
155
  --no-tmux run claude directly, without tmux
156
+ --force with --resume: start a second copy even if that conversation is already running
171
157
  -n, --name <n> name the session (also names the tmux session; claude's own flag)
172
158
  --resume <n> resume the named session (claude's own flag)
173
159
  -h, --help show this help
@@ -219,18 +205,6 @@ Claude's own --agent is untouched and passes through to claude.`)
219
205
  const parsed = parseCodexArgs(i >= 0 ? raw.slice(i + 1) : []);
220
206
  process.exitCode = runCodex(parsed, over);
221
207
  });
222
- markForwardsRawArgs(program
223
- .command('gemini'), 'forwards argv after the verb verbatim')
224
- .description('Run Gemini CLI (same arguments as gemini) in a tmux session that survives sleep and drops')
225
- .argument('[args...]', 'passed to gemini exactly as given (quox adds only --no-tmux)')
226
- .helpOption(false)
227
- .allowUnknownOption()
228
- .allowExcessArguments()
229
- .action((_a, _o, cmd) => {
230
- const raw = (cmd.parent && cmd.parent.rawArgs) || process.argv;
231
- const i = raw.indexOf('gemini', 2);
232
- process.exitCode = runGemini(parseGeminiArgs(i >= 0 ? raw.slice(i + 1) : []), over);
233
- });
234
208
  markForwardsRawArgs(program
235
209
  .command('wrap'), 'forwards argv after the verb verbatim')
236
210
  .description('Run any program in a governed tmux session: quox wrap <program> [args...]')
@@ -299,7 +273,7 @@ Claude's own --agent is untouched and passes through to claude.`)
299
273
  sessions.command('heartbeat <tmuxSession>', { hidden: true })
300
274
  .description('Heartbeat one wrapped tmux session to the org session index (started by quox claude)')
301
275
  .option('--loop', 'keep beating until the tmux session ends')
302
- .option('--backend <program>', 'claude, codex, gemini, ...')
276
+ .option('--backend <program>', 'claude, codex, ...')
303
277
  .option('--agent <agent>', 'agent the session started attached to (--as)')
304
278
  .option('--slot <slot>', 'slot the session started holding (--slot)')
305
279
  .action(async (name, o) => {