quoxcli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/admin.js +26 -0
  5. package/lib/api/endpoints/agent-schedules.js +26 -0
  6. package/lib/api/endpoints/agents.js +7 -0
  7. package/lib/api/endpoints/assistants.js +12 -0
  8. package/lib/api/endpoints/away.js +15 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/contactsq.js +2 -0
  11. package/lib/api/endpoints/conversations.js +14 -0
  12. package/lib/api/endpoints/executions.js +13 -0
  13. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  14. package/lib/api/endpoints/files.js +14 -0
  15. package/lib/api/endpoints/hat.js +30 -0
  16. package/lib/api/endpoints/inbox.js +20 -0
  17. package/lib/api/endpoints/index.js +14 -0
  18. package/lib/api/endpoints/memory.js +19 -0
  19. package/lib/api/endpoints/mind.js +13 -0
  20. package/lib/api/endpoints/mon.js +6 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/plugins.js +18 -0
  23. package/lib/api/endpoints/presence.js +33 -0
  24. package/lib/api/endpoints/quoxplan.js +7 -0
  25. package/lib/api/endpoints/quoxseo.js +6 -0
  26. package/lib/api/endpoints/router.js +17 -0
  27. package/lib/api/endpoints/safety.js +7 -0
  28. package/lib/api/endpoints/skills.js +20 -0
  29. package/lib/api/endpoints/stepup.js +20 -0
  30. package/lib/api/endpoints/teams.js +2 -2
  31. package/lib/api/endpoints/triggers.js +26 -0
  32. package/lib/api/endpoints/v1.js +86 -0
  33. package/lib/api/endpoints/ward.js +7 -0
  34. package/lib/api/endpoints/workflows.js +7 -0
  35. package/lib/api/endpoints/x.js +8 -0
  36. package/lib/commands/actions.js +61 -0
  37. package/lib/commands/admin.js +16 -0
  38. package/lib/commands/agent.js +163 -27
  39. package/lib/commands/agentAttach.js +94 -6
  40. package/lib/commands/approvals.js +73 -1
  41. package/lib/commands/aside.js +24 -0
  42. package/lib/commands/ask.js +144 -0
  43. package/lib/commands/assistant.js +59 -4
  44. package/lib/commands/auth.js +8 -2
  45. package/lib/commands/backup.js +8 -4
  46. package/lib/commands/brain.js +105 -14
  47. package/lib/commands/build.js +19 -5
  48. package/lib/commands/chat.js +57 -2
  49. package/lib/commands/claude.js +34 -8
  50. package/lib/commands/contacts.js +49 -0
  51. package/lib/commands/conversation.js +35 -4
  52. package/lib/commands/dashboard.js +43 -0
  53. package/lib/commands/department.js +28 -7
  54. package/lib/commands/devResident.js +15 -3
  55. package/lib/commands/devSupervisor.js +13 -1
  56. package/lib/commands/discord.js +75 -39
  57. package/lib/commands/done.js +98 -28
  58. package/lib/commands/engagement.js +17 -6
  59. package/lib/commands/fetchAllowlist.js +71 -0
  60. package/lib/commands/file.js +38 -1
  61. package/lib/commands/fleet.js +311 -4
  62. package/lib/commands/governance.js +12 -3
  63. package/lib/commands/gripe.js +44 -13
  64. package/lib/commands/hat.js +178 -9
  65. package/lib/commands/helpers.js +18 -1
  66. package/lib/commands/hyperion.js +58 -0
  67. package/lib/commands/inbox.js +74 -23
  68. package/lib/commands/licence.js +14 -155
  69. package/lib/commands/lucid.js +10 -4
  70. package/lib/commands/memory.js +85 -8
  71. package/lib/commands/mind.js +71 -0
  72. package/lib/commands/mon.js +69 -2
  73. package/lib/commands/monitoring.js +1 -1
  74. package/lib/commands/motion.js +2 -2
  75. package/lib/commands/nucleus.js +48 -17
  76. package/lib/commands/objective.js +11 -0
  77. package/lib/commands/offload.js +89 -0
  78. package/lib/commands/onboard.js +2 -1
  79. package/lib/commands/org.js +11 -1
  80. package/lib/commands/peek.js +28 -0
  81. package/lib/commands/plan.js +38 -0
  82. package/lib/commands/plugin.js +110 -0
  83. package/lib/commands/presence.js +89 -0
  84. package/lib/commands/provider.js +159 -0
  85. package/lib/commands/qlarity.js +19 -3
  86. package/lib/commands/quoxchat.js +13 -4
  87. package/lib/commands/quoxseo.js +38 -0
  88. package/lib/commands/reach.js +2 -1
  89. package/lib/commands/router.js +77 -0
  90. package/lib/commands/rows.js +358 -0
  91. package/lib/commands/run.js +64 -9
  92. package/lib/commands/server.js +292 -0
  93. package/lib/commands/setup.js +33 -7
  94. package/lib/commands/skills.js +48 -5
  95. package/lib/commands/slot.js +260 -7
  96. package/lib/commands/status.js +1 -1
  97. package/lib/commands/team.js +3 -1
  98. package/lib/commands/tips.js +23 -0
  99. package/lib/commands/tool.js +133 -45
  100. package/lib/commands/trace.js +12 -7
  101. package/lib/commands/trigger.js +95 -0
  102. package/lib/commands/update.js +46 -0
  103. package/lib/commands/vault.js +31 -8
  104. package/lib/commands/vm.js +237 -0
  105. package/lib/commands/voice.js +34 -3
  106. package/lib/commands/volt.js +45 -3
  107. package/lib/commands/walkthrough.js +32 -0
  108. package/lib/commands/watch.js +73 -21
  109. package/lib/commands/welcome.js +14 -0
  110. package/lib/commands/workflow.js +189 -32
  111. package/lib/commands/x.js +13 -4
  112. package/lib/core/actingAs.js +2 -2
  113. package/lib/core/activeSummary.js +186 -0
  114. package/lib/core/aside.js +54 -0
  115. package/lib/core/askDecision.js +60 -0
  116. package/lib/core/auth.js +164 -13
  117. package/lib/core/callerSource.js +1 -1
  118. package/lib/core/cliBuild.js +62 -0
  119. package/lib/core/client.js +144 -35
  120. package/lib/core/commandRegistrars.js +121 -0
  121. package/lib/core/config.js +142 -24
  122. package/lib/core/customHats.js +168 -0
  123. package/lib/core/doneEngineFetch.js +8 -0
  124. package/lib/core/errors.js +329 -20
  125. package/lib/core/global-flags.js +105 -5
  126. package/lib/core/hyperion.js +102 -0
  127. package/lib/core/inboxResolution.js +65 -0
  128. package/lib/core/lazyCommands.js +4 -1
  129. package/lib/core/offload.js +504 -0
  130. package/lib/core/org-override.js +1 -1
  131. package/lib/core/orgResolve.js +70 -0
  132. package/lib/core/output.js +4 -4
  133. package/lib/core/peek.js +126 -0
  134. package/lib/core/profileUsers.js +51 -0
  135. package/lib/core/promptCancel.js +18 -0
  136. package/lib/core/repoPath.js +11 -0
  137. package/lib/core/residentSeed.js +3 -1
  138. package/lib/core/rootOptions.js +39 -0
  139. package/lib/core/router.js +168 -0
  140. package/lib/core/selfUpdate.js +188 -0
  141. package/lib/core/sessionsOverview.js +64 -0
  142. package/lib/core/slotKeeper.js +248 -3
  143. package/lib/core/slotWake.js +120 -0
  144. package/lib/core/supervisorEngine.js +71 -2
  145. package/lib/core/version.js +1 -1
  146. package/lib/core/vm.js +361 -0
  147. package/lib/core/vmTui.js +86 -0
  148. package/lib/core/walkthrough.js +35 -0
  149. package/lib/hyperion/deck.js +29 -0
  150. package/lib/lucid/LucidCard.jsx +68 -0
  151. package/lib/lucid/LucidIntro.jsx +45 -58
  152. package/lib/lucid/cards.js +86 -0
  153. package/lib/lucid/markdown.js +81 -0
  154. package/lib/lucid/motion.js +56 -0
  155. package/lib/quoxon/activity.js +14 -2
  156. package/lib/quoxon/aocl.js +117 -0
  157. package/lib/quoxon/api.js +504 -25
  158. package/lib/quoxon/app.js +3000 -230
  159. package/lib/quoxon/approvalFacts.js +94 -0
  160. package/lib/quoxon/approvalsView.js +325 -0
  161. package/lib/quoxon/asideView.js +40 -0
  162. package/lib/quoxon/ask.js +121 -0
  163. package/lib/quoxon/away.js +52 -0
  164. package/lib/quoxon/bang.js +224 -0
  165. package/lib/quoxon/bulkRisk.js +17 -0
  166. package/lib/quoxon/capabilities.js +13 -6
  167. package/lib/quoxon/cells.js +53 -0
  168. package/lib/quoxon/commandShort.js +112 -0
  169. package/lib/quoxon/commands.js +363 -38
  170. package/lib/quoxon/connect.js +210 -23
  171. package/lib/quoxon/connectCard.js +19 -2
  172. package/lib/quoxon/consent.js +86 -0
  173. package/lib/quoxon/copy.js +4 -3
  174. package/lib/quoxon/countdownLine.js +11 -0
  175. package/lib/quoxon/demo.autoplay.js +29 -0
  176. package/lib/quoxon/demo.fixtures.js +1 -1
  177. package/lib/quoxon/demo.js +44 -3
  178. package/lib/quoxon/devObserver.js +20 -0
  179. package/lib/quoxon/dividerAocl.js +191 -0
  180. package/lib/quoxon/dividerRule.js +57 -0
  181. package/lib/quoxon/dragonModes.js +150 -0
  182. package/lib/quoxon/dragonView.js +41 -0
  183. package/lib/quoxon/earlyInput.js +40 -0
  184. package/lib/quoxon/echo.js +15 -0
  185. package/lib/quoxon/egg.js +2 -2
  186. package/lib/quoxon/events.js +244 -25
  187. package/lib/quoxon/feedback.js +50 -0
  188. package/lib/quoxon/gallery.js +21 -7
  189. package/lib/quoxon/handoffOffer.js +78 -0
  190. package/lib/quoxon/hatSwitch.js +37 -0
  191. package/lib/quoxon/headless.js +161 -0
  192. package/lib/quoxon/hostUrls.js +41 -0
  193. package/lib/quoxon/indicator.js +6 -2
  194. package/lib/quoxon/intent.js +12 -1
  195. package/lib/quoxon/keys.js +49 -6
  196. package/lib/quoxon/launcher.js +103 -0
  197. package/lib/quoxon/main.js +92 -160
  198. package/lib/quoxon/markdown.js +45 -21
  199. package/lib/quoxon/markdownSettle.js +19 -0
  200. package/lib/quoxon/matrixDomains.json +46 -0
  201. package/lib/quoxon/mentions.js +90 -0
  202. package/lib/quoxon/mind.js +103 -0
  203. package/lib/quoxon/mode.js +46 -0
  204. package/lib/quoxon/model.js +132 -26
  205. package/lib/quoxon/modelPrefs.js +345 -0
  206. package/lib/quoxon/motion/clock.js +3 -2
  207. package/lib/quoxon/motion/level.js +91 -0
  208. package/lib/quoxon/mouse.js +211 -0
  209. package/lib/quoxon/noai.js +57 -0
  210. package/lib/quoxon/objects/away.js +72 -0
  211. package/lib/quoxon/objects/fetch.js +17 -5
  212. package/lib/quoxon/objects/fleet.js +149 -0
  213. package/lib/quoxon/objects/health.js +186 -0
  214. package/lib/quoxon/objects/index.js +24 -3
  215. package/lib/quoxon/objects/mirror.js +137 -0
  216. package/lib/quoxon/objects/peek.js +17 -0
  217. package/lib/quoxon/objects/prove.js +130 -0
  218. package/lib/quoxon/objects/replay.js +125 -0
  219. package/lib/quoxon/objects/sitrep.js +58 -0
  220. package/lib/quoxon/objects/view.js +11 -0
  221. package/lib/quoxon/onboarding/copy.js +32 -4
  222. package/lib/quoxon/onboarding/credential.js +17 -4
  223. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  224. package/lib/quoxon/onboarding/failureCause.js +33 -0
  225. package/lib/quoxon/onboarding/finish.js +15 -0
  226. package/lib/quoxon/onboarding/index.js +34 -7
  227. package/lib/quoxon/onboarding/installRunner.js +64 -5
  228. package/lib/quoxon/onboarding/preflight.js +17 -3
  229. package/lib/quoxon/onboarding/probe.js +65 -0
  230. package/lib/quoxon/onboarding/view.js +76 -19
  231. package/lib/quoxon/orgLabel.js +10 -0
  232. package/lib/quoxon/orgSwitch.js +33 -0
  233. package/lib/quoxon/ownBroadcast.js +22 -0
  234. package/lib/quoxon/palette.js +43 -0
  235. package/lib/quoxon/print.js +9 -5
  236. package/lib/quoxon/profileSwitch.js +53 -0
  237. package/lib/quoxon/promptHistory.js +41 -0
  238. package/lib/quoxon/providers.js +48 -0
  239. package/lib/quoxon/qart.js +20 -51
  240. package/lib/quoxon/qdragon.js +410 -0
  241. package/lib/quoxon/qdragonFly.js +61 -0
  242. package/lib/quoxon/qdragonScenes.js +282 -0
  243. package/lib/quoxon/qeyes.js +213 -0
  244. package/lib/quoxon/qmotion.js +58 -37
  245. package/lib/quoxon/render.js +638 -119
  246. package/lib/quoxon/resend.js +72 -0
  247. package/lib/quoxon/roster.js +95 -9
  248. package/lib/quoxon/route.js +6 -5
  249. package/lib/quoxon/rows.js +711 -0
  250. package/lib/quoxon/rowsStore.js +80 -0
  251. package/lib/quoxon/screenContext.js +150 -0
  252. package/lib/quoxon/screenFrames.js +254 -0
  253. package/lib/quoxon/session.js +172 -2
  254. package/lib/quoxon/sessionDead.js +17 -0
  255. package/lib/quoxon/signin.js +125 -0
  256. package/lib/quoxon/slash/agents.js +112 -0
  257. package/lib/quoxon/slash/argSources.js +65 -0
  258. package/lib/quoxon/slash/args.js +65 -0
  259. package/lib/quoxon/slash/automate.js +89 -0
  260. package/lib/quoxon/slash/broadcast.js +140 -0
  261. package/lib/quoxon/slash/broadcastRun.js +108 -0
  262. package/lib/quoxon/slash/card.js +30 -0
  263. package/lib/quoxon/slash/challenge.js +114 -0
  264. package/lib/quoxon/slash/cliCommand.js +53 -0
  265. package/lib/quoxon/slash/cliLeaves.json +948 -0
  266. package/lib/quoxon/slash/compact.js +1 -1
  267. package/lib/quoxon/slash/confirm.js +30 -0
  268. package/lib/quoxon/slash/crystallize.js +69 -0
  269. package/lib/quoxon/slash/dashboard.js +136 -0
  270. package/lib/quoxon/slash/draft.js +54 -0
  271. package/lib/quoxon/slash/hatWalk.js +206 -0
  272. package/lib/quoxon/slash/inverses.js +77 -0
  273. package/lib/quoxon/slash/killer.js +24 -0
  274. package/lib/quoxon/slash/killerApi.js +16 -0
  275. package/lib/quoxon/slash/ledger.js +13 -8
  276. package/lib/quoxon/slash/listView.js +3 -1
  277. package/lib/quoxon/slash/modelCmd.js +261 -0
  278. package/lib/quoxon/slash/palette.js +21 -7
  279. package/lib/quoxon/slash/paletteController.js +117 -7
  280. package/lib/quoxon/slash/panels.js +21 -6
  281. package/lib/quoxon/slash/picker.js +20 -2
  282. package/lib/quoxon/slash/restart.js +71 -13
  283. package/lib/quoxon/slash/rowCmd.js +157 -0
  284. package/lib/quoxon/slash/rowWalk.js +175 -0
  285. package/lib/quoxon/slash/settings.js +16 -6
  286. package/lib/quoxon/slash/spawn.js +177 -0
  287. package/lib/quoxon/slash/stepUp.js +274 -0
  288. package/lib/quoxon/slash/stepUpCard.js +60 -0
  289. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  290. package/lib/quoxon/slash/undoApi.js +38 -0
  291. package/lib/quoxon/slash/undoCmd.js +49 -0
  292. package/lib/quoxon/slash/undoWish.js +15 -0
  293. package/lib/quoxon/slash/watch.js +164 -0
  294. package/lib/quoxon/slash/wish.js +23 -0
  295. package/lib/quoxon/slash/wishCmd.js +32 -0
  296. package/lib/quoxon/slotChat.js +300 -0
  297. package/lib/quoxon/slotFormat.js +195 -0
  298. package/lib/quoxon/slotRun.js +131 -0
  299. package/lib/quoxon/sound/mapping.js +2 -1
  300. package/lib/quoxon/stray.js +22 -0
  301. package/lib/quoxon/suggestCommand.js +59 -0
  302. package/lib/quoxon/term.js +46 -11
  303. package/lib/quoxon/textBuffer.js +176 -2
  304. package/lib/quoxon/theme.js +58 -25
  305. package/lib/quoxon/tips.js +433 -0
  306. package/lib/quoxon/toolError.js +41 -0
  307. package/lib/quoxon/transcript.js +42 -4
  308. package/lib/quoxon/web/qface.js +18 -10
  309. package/lib/quoxon/welcome.js +1 -1
  310. package/lib/quoxon/whatsnew.js +136 -0
  311. package/lib/quoxon/whatsnew.jsonl +89 -0
  312. package/lib/quoxon/windowOrg.js +113 -0
  313. package/lib/server/diag.js +129 -0
  314. package/lib/server/doctor.js +71 -0
  315. package/lib/server/install.js +149 -0
  316. package/lib/server/pluginEnable.js +161 -0
  317. package/lib/server/skew.js +56 -0
  318. package/lib/server/snapshot.js +131 -0
  319. package/lib/server/status.js +128 -0
  320. package/lib/server/sys.js +65 -0
  321. package/lib/server/uninstall.js +63 -0
  322. package/lib/server/update.js +234 -0
  323. package/lib/toolOutputShape.js +315 -0
  324. package/lib/tui/views/HyperionDeck.jsx +79 -0
  325. package/lib/tui/views/LucidDeck.jsx +209 -217
  326. package/package.json +4 -1
  327. package/lib/core/websiteSession.js +0 -148
@@ -55,6 +55,7 @@ import path from 'node:path';
55
55
  import {
56
56
  resolveFetchDestination,
57
57
  fetchEngine,
58
+ inspectInstall,
58
59
  OPT_GAUGE_DIR,
59
60
  userLocalGaugeDir,
60
61
  legacyGaugeDir,
@@ -88,19 +89,21 @@ export function resolveEnginePath({ env = process.env, existsSync = fs.existsSyn
88
89
 
89
90
  const ENGINE = resolveEnginePath();
90
91
 
91
- function runEngine(args) {
92
- if (!fs.existsSync(ENGINE)) {
93
- process.stderr.write(
94
- `DoneEngine not found at ${ENGINE}\n` +
95
- 'Self-heal: `quox done fetch` pulls the engine bundle straight from ' +
96
- 'the collector, no human in the loop. Otherwise `quox dev enable` ' +
97
- 'delivers it to /opt/quoxgauge as part of full dev-server ' +
98
- 'provisioning. Or set QUOX_DONEENGINE_PATH to a checkout.\n',
99
- );
92
+ function engineMissingMessage(engine) {
93
+ return `DoneEngine not found at ${engine}\n`
94
+ + 'Self-heal: `quox done fetch` pulls the engine bundle straight from '
95
+ + 'the collector, no human in the loop. Otherwise `quox dev enable` '
96
+ + 'delivers it to /opt/quoxgauge as part of full dev-server '
97
+ + 'provisioning. Or set QUOX_DONEENGINE_PATH to a checkout.\n';
98
+ }
99
+
100
+ function runEngine(args, engine = ENGINE) {
101
+ if (!fs.existsSync(engine)) {
102
+ process.stderr.write(engineMissingMessage(engine));
100
103
  // 2, not 1: a missing engine is "cannot tell", not "refused".
101
104
  return 2;
102
105
  }
103
- const r = spawnSync(process.execPath, [ENGINE, ...args], { stdio: 'inherit' });
106
+ const r = spawnSync(process.execPath, [engine, ...args], { stdio: 'inherit' });
104
107
  if (r.error) {
105
108
  process.stderr.write(`DoneEngine failed to run: ${r.error.message}\n`);
106
109
  return 2;
@@ -108,6 +111,23 @@ function runEngine(args) {
108
111
  return r.status ?? 2;
109
112
  }
110
113
 
114
+ /**
115
+ * VISIBILITY GATE (#988 follow-on). Public installs do not ship DoneEngine
116
+ * (its bundle is built from the private quox-gauge repo and delivered to dev
117
+ * servers by `quox dev enable`), so listing `quox done` in their help
118
+ * advertises a command that can only fail. It is listed only when an engine
119
+ * is installed locally, decided from the filesystem alone with the same
120
+ * resolution the runner uses: building help never makes a network call.
121
+ * QUOX_HELP_INCLUDE_GATED=1 lists it regardless; the generators that scrape
122
+ * `--help` into the command trees set it so those trees do not depend on
123
+ * which host ran them.
124
+ */
125
+ export function isDoneEngineInstalled({ env = process.env, home = os.homedir(), fsOps } = {}) {
126
+ const ops = fsOps || { existsSync: fs.existsSync, readdirSync: fs.readdirSync };
127
+ const engine = resolveEnginePath({ env, existsSync: ops.existsSync, home });
128
+ return inspectInstall(path.dirname(engine), ops).healthy;
129
+ }
130
+
111
131
  function defaultExecFn(cmd, args) {
112
132
  return spawnSync(cmd, args, { encoding: 'utf8' });
113
133
  }
@@ -140,8 +160,56 @@ function defaultFsOps() {
140
160
  }
141
161
 
142
162
  export function registerDoneCommands(program, ctx = {}) {
163
+ const env = ctx.env || process.env;
164
+ const home = ctx.home || os.homedir();
165
+ // Tests inject fsOps/env/home; the real CLI keeps the import-time ENGINE.
166
+ const injected = !!(ctx.fsOps || ctx.env || ctx.home);
167
+ const currentEngine = () => (injected
168
+ ? resolveEnginePath({ env, existsSync: (ctx.fsOps || fs).existsSync, home })
169
+ : ENGINE);
170
+ const visible = env.QUOX_HELP_INCLUDE_GATED === '1'
171
+ || isDoneEngineInstalled({ env, home, fsOps: ctx.fsOps });
172
+
173
+ const fetchDeps = () => ({
174
+ execFn: ctx.execFn || defaultExecFn,
175
+ isWritable: ctx.isWritable || defaultIsWritable,
176
+ fsOps: ctx.fsOps || defaultFsOps(),
177
+ });
178
+
179
+ /**
180
+ * Resolve a runnable engine for a subcommand. Present: use it, no network.
181
+ * Absent: try the existing fetch path once. A collector 404
182
+ * (DONEENGINE_NOT_SHIPPED) means this is a public install: refuse with that
183
+ * message and exit 2, having changed nothing. A bundle that IS available
184
+ * (a dev collector) installs and the freshly fetched engine runs.
185
+ */
186
+ async function ensureEngine() {
187
+ const engine = currentEngine();
188
+ const exists = (ctx.fsOps || fs).existsSync(engine);
189
+ if (exists || !ctx.client || env.QUOX_DONEENGINE_PATH) return { engine };
190
+ const { execFn, isWritable, fsOps } = fetchDeps();
191
+ const { dest, needsSudo } = resolveFetchDestination({ env, isWritable, execFn });
192
+ try {
193
+ await fetchEngine({ dest, needsSudo, force: false, client: ctx.client, execFn, fsOps });
194
+ return { engine: path.join(dest, 'gauge.mjs') };
195
+ } catch (err) {
196
+ if (err.code === 'DONEENGINE_NOT_SHIPPED') {
197
+ process.stderr.write(`quox done: ${err.message}\n`);
198
+ return { code: 2 };
199
+ }
200
+ process.stderr.write(`quox done: engine fetch failed: ${err.message}\n`);
201
+ return { engine };
202
+ }
203
+ }
204
+
205
+ async function runGuarded(args) {
206
+ const r = await ensureEngine();
207
+ if (r.code !== undefined) return r.code;
208
+ return runEngine(args, r.engine);
209
+ }
210
+
143
211
  const done = program
144
- .command('done')
212
+ .command('done', { hidden: !visible })
145
213
  .description('Quox DoneEngine: is this actually done, and what is the evidence?');
146
214
 
147
215
  done
@@ -150,7 +218,7 @@ export function registerDoneCommands(program, ctx = {}) {
150
218
  .option('--pack <id>', 'rule pack to judge against', 'launch-spine')
151
219
  .option('--json', 'structured verdict for agents')
152
220
  .option('--root <path>', 'judge <subject> in this repo, reading its own STATUS map with the selected pack (e.g. a plugin with --pack plugin)')
153
- .action((subject, options) => {
221
+ .action(async (subject, options) => {
154
222
  const args = ['--can-ship', subject || 'all', '--pack', options.pack];
155
223
  if (options.root) {
156
224
  if (!subject) {
@@ -161,17 +229,17 @@ export function registerDoneCommands(program, ctx = {}) {
161
229
  args.push('--subject', subject, '--root', path.resolve(options.root));
162
230
  }
163
231
  if (options.json) args.push('--json');
164
- process.exitCode = runEngine(args);
232
+ process.exitCode = await runGuarded(args);
165
233
  });
166
234
 
167
235
  done
168
236
  .command('checklist [pack]')
169
237
  .description('The builder checklist GENERATED from a pack (every gate the engine judges); --check verifies a doc that embeds it')
170
238
  .option('--check <file>', 'exit 1 if the checklist block in this file has drifted from the pack')
171
- .action((pack, options) => {
239
+ .action(async (pack, options) => {
172
240
  const args = ['--checklist', pack || 'plugin'];
173
241
  if (options.check) args.push('--check', path.resolve(options.check));
174
- process.exitCode = runEngine(args);
242
+ process.exitCode = await runGuarded(args);
175
243
  });
176
244
 
177
245
  done
@@ -180,12 +248,12 @@ export function registerDoneCommands(program, ctx = {}) {
180
248
  .option('--json', 'machine-readable, includes raw deploy state')
181
249
  .option('--dod', 'per-surface gates, split by evidence class')
182
250
  .option('--no-deploy', 'skip docker/git; Deployed Ready becomes unknown, which is not a pass')
183
- .action((options) => {
251
+ .action(async (options) => {
184
252
  const args = [];
185
253
  if (options.json) args.push('--json');
186
254
  if (options.dod) args.push('--dod');
187
255
  if (options.deploy === false) args.push('--no-deploy');
188
- process.exitCode = runEngine(args);
256
+ process.exitCode = await runGuarded(args);
189
257
  });
190
258
 
191
259
  done
@@ -193,25 +261,27 @@ export function registerDoneCommands(program, ctx = {}) {
193
261
  .description('One rule pack against its subjects: BOTH finish lines, build and deployed')
194
262
  .option('--snapshot', 'append this reading to the snapshot history')
195
263
  .option('--org <id>', 'measure FOR this org: plan evidence reads as it, the snapshot is stamped with it (default: the pack\'s own org field)')
196
- .action((pack, options) => {
264
+ .action(async (pack, options) => {
197
265
  const args = pack ? ['--pack', pack] : ['--launch'];
198
266
  if (options.snapshot) args.push('--snapshot');
199
267
  if (options.org) args.push('--org', options.org);
200
- process.exitCode = runEngine(args);
268
+ process.exitCode = await runGuarded(args);
201
269
  });
202
270
 
203
271
  done
204
272
  .command('history [pack]')
205
273
  .description('Movement between snapshots, regressions listed first')
206
- .action((pack) => {
207
- process.exitCode = runEngine(pack ? ['--history', '--pack', pack] : ['--history']);
274
+ .action(async (pack) => {
275
+ process.exitCode = await runGuarded(pack ? ['--history', '--pack', pack] : ['--history']);
208
276
  });
209
277
 
210
278
  done
211
279
  .command('packs')
212
280
  .description('List available rule packs')
213
- .action(() => {
214
- const dir = path.join(path.dirname(ENGINE), 'packs');
281
+ .action(async () => {
282
+ const r = await ensureEngine();
283
+ if (r.code !== undefined) { process.exitCode = r.code; return; }
284
+ const dir = path.join(path.dirname(r.engine), 'packs');
215
285
  if (!fs.existsSync(dir)) {
216
286
  process.stderr.write(
217
287
  `No packs directory at ${dir}\n` +
@@ -246,13 +316,11 @@ export function registerDoneCommands(program, ctx = {}) {
246
316
  process.exitCode = 2;
247
317
  return;
248
318
  }
249
- const execFn = ctx.execFn || defaultExecFn;
250
- const isWritable = ctx.isWritable || defaultIsWritable;
251
- const fsOps = ctx.fsOps || defaultFsOps();
319
+ const { execFn, isWritable, fsOps } = fetchDeps();
252
320
 
253
321
  const { dest, reason, needsSudo } = resolveFetchDestination({
254
322
  destOption: options.dest,
255
- env: process.env,
323
+ env,
256
324
  isWritable,
257
325
  execFn,
258
326
  });
@@ -267,7 +335,9 @@ export function registerDoneCommands(program, ctx = {}) {
267
335
  }
268
336
  } catch (err) {
269
337
  process.stderr.write(`quox done fetch failed: ${err.message}\n`);
270
- process.exitCode = 1;
338
+ // A public install's 404 is "this install has no DoneEngine", not a
339
+ // transient failure: exit 2 like every other "cannot tell" here.
340
+ process.exitCode = err.code === 'DONEENGINE_NOT_SHIPPED' ? 2 : 1;
271
341
  }
272
342
  });
273
343
 
@@ -121,10 +121,13 @@ export function registerEngagementCommands(program, ctx) {
121
121
  const data = await client.post(ep.path, { service: ep.service, body });
122
122
  printSuccess(`Target "${value}" added to engagement ${id}.`);
123
123
  if (data.target.verification_token) {
124
+ // #1007: a url target is challenged on its hostname (dns_txt) or at its own origin (well_known_file)
125
+ const stored = data.target.value || value;
126
+ const url = data.target.type === 'url' ? (() => { try { return new URL(stored); } catch { return null; } })() : null;
124
127
  if (data.target.verification_method === 'dns_txt') {
125
- console.log(` Add a DNS TXT record on ${value}: quox-verify=${data.target.verification_token}`);
128
+ console.log(` Add a DNS TXT record on ${url ? url.hostname : stored}: quox-verify=${data.target.verification_token}`);
126
129
  } else if (data.target.verification_method === 'well_known_file') {
127
- console.log(` Serve this content at https://${value}/.well-known/quox-verification: ${data.target.verification_token}`);
130
+ console.log(` Serve this content at ${url ? url.origin : `https://${stored}`}/.well-known/quox-verification: ${data.target.verification_token}`);
128
131
  }
129
132
  console.log(` Then run: quox engagement verify-target ${id} ${data.target.id}`);
130
133
  }
@@ -172,6 +175,7 @@ export function registerEngagementCommands(program, ctx) {
172
175
  console.log(JSON.stringify(data.output, null, 2));
173
176
  } else {
174
177
  console.log(` Failed (allowed: ${data.allowed}): ${data.error}`);
178
+ process.exitCode = 1; // #1007: a refusal is not success, for scripts as much as people
175
179
  }
176
180
  });
177
181
 
@@ -185,6 +189,7 @@ export function registerEngagementCommands(program, ctx) {
185
189
  const data = await client.post(ep.path, { service: ep.service, body: { target: hostTarget, add: options.add === true } });
186
190
  if (!data.success) {
187
191
  console.log(` Failed (allowed: ${data.allowed}): ${data.error}`);
192
+ process.exitCode = 1; // #1007: a refusal is not success, for scripts as much as people
188
193
  return;
189
194
  }
190
195
  const discovered = data.discovered || [];
@@ -234,6 +239,7 @@ export function registerEngagementCommands(program, ctx) {
234
239
  console.log(` Poll: quox engagement job ${data.job_id}`);
235
240
  } else {
236
241
  console.log(` Failed (allowed: ${data.allowed}): ${data.error}`);
242
+ process.exitCode = 1; // #1007: a refusal is not success, for scripts as much as people
237
243
  }
238
244
  });
239
245
 
@@ -372,11 +378,16 @@ export function registerEngagementCommands(program, ctx) {
372
378
  }
373
379
  const buf = Buffer.from(await res.arrayBuffer());
374
380
  const rootOutput = program.opts().output;
375
- const outFile = options.output
376
- || (rootOutput && !KNOWN_OUTPUT_FORMATS.has(rootOutput) ? rootOutput : null)
377
- || `quox-security-${id}.pdf`;
381
+ // quox#919: the global --json reaches options.output as 'json' via
382
+ // mergeShadowedGlobals; an output-format name is never a file path.
383
+ const asPath = (v) => (v && !KNOWN_OUTPUT_FORMATS.has(v) ? v : null);
384
+ const outFile = asPath(options.output) || asPath(rootOutput) || `quox-security-${id}.pdf`;
378
385
  writeFileSync(outFile, buf);
379
- printSuccess(`Report saved to ${outFile} (${buf.length} bytes).`);
386
+ if (rootOutput === 'json') {
387
+ console.log(JSON.stringify({ success: true, file: outFile, bytes: buf.length }));
388
+ } else {
389
+ printSuccess(`Report saved to ${outFile} (${buf.length} bytes).`);
390
+ }
380
391
  });
381
392
 
382
393
  async function raiseIssue(findingId) {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `quox fetch allow` — the URL prefixes this org's agents may GET through the
3
+ * governed web_fetch_allowlisted tool (quox#1171). Default-deny: nothing is
4
+ * fetchable until an org admin adds a prefix. Every call goes through the
5
+ * collector; adding and removing need a human org admin (the server refuses
6
+ * agent tokens).
7
+ */
8
+
9
+ import { resolveEndpoint } from '../api/endpoints.js';
10
+
11
+ async function call(client, id, params = {}, body) {
12
+ const ep = resolveEndpoint(id, params);
13
+ const opts = { service: ep.service };
14
+ if (body) opts.body = body;
15
+ switch (ep.method) {
16
+ case 'POST': return client.post(ep.path, opts);
17
+ case 'DELETE': return client.delete(ep.path, opts);
18
+ default: return client.get(ep.path, opts);
19
+ }
20
+ }
21
+
22
+ export function registerFetchAllowlistCommands(program, ctx) {
23
+ const { client } = ctx;
24
+
25
+ const f = program
26
+ .command('fetch')
27
+ .description('Governed agent fetch: which URLs your agents may GET');
28
+
29
+ const al = f.command('allow')
30
+ .description('The URL prefixes this org allows agents to fetch (default-deny)');
31
+
32
+ al.command('list', { isDefault: true })
33
+ .description('Show the allowed URL prefixes for this org')
34
+ .action(async () => {
35
+ const d = await call(client, 'fetchAllowlist.list');
36
+ if (d.prefixes?.length) {
37
+ process.stdout.write('allowed prefixes:\n');
38
+ for (const e of d.prefixes) process.stdout.write(` ${e.url_prefix}${e.note ? ` (${e.note})` : ''}\n`);
39
+ } else {
40
+ process.stdout.write('none allowed yet: agents cannot fetch anything. Add one with `quox fetch allow add <url-prefix>`\n');
41
+ }
42
+ });
43
+
44
+ al.command('add <url-prefix>')
45
+ .description('Allow agents to GET URLs under a prefix, e.g. https://status.example.com/api/')
46
+ .option('--note <text>', 'why it is allowed, shown alongside it later')
47
+ .action(async (urlPrefix, opts) => {
48
+ try {
49
+ const d = await call(client, 'fetchAllowlist.add', {}, { url_prefix: urlPrefix, note: opts.note });
50
+ process.stdout.write(`allowed ${d.entry.url_prefix}\n`);
51
+ process.stdout.write(` ${d.prefixes.length} prefix(es) now allowed for this org\n`);
52
+ } catch (err) {
53
+ if (err?.status === 400 && err?.message) {
54
+ process.stderr.write(`Not added: ${err.message}\n`);
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+ throw err;
59
+ }
60
+ });
61
+
62
+ al.command('remove <url-prefix>')
63
+ .description('Stop allowing a URL prefix')
64
+ .action(async (urlPrefix) => {
65
+ const d = await call(client, 'fetchAllowlist.remove', { prefix: urlPrefix });
66
+ if (d.removed) process.stdout.write(`removed ${urlPrefix}\n`);
67
+ else process.stdout.write(`nothing to remove: ${urlPrefix} was not allowed for this org\n`);
68
+ });
69
+ }
70
+
71
+ export default registerFetchAllowlistCommands;
@@ -2,7 +2,7 @@
2
2
  * File commands: upload, download, list, delete
3
3
  */
4
4
 
5
- import { createWriteStream } from 'node:fs';
5
+ import { createWriteStream, readFileSync } from 'node:fs';
6
6
  import { basename } from 'node:path';
7
7
  import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
8
8
  import { resolveEndpoint } from '../api/endpoints.js';
@@ -125,4 +125,41 @@ export function registerFileCommands(program, ctx) {
125
125
  const data = await client.get(resolveEndpoint('files.stats').path, { service: resolveEndpoint('files.stats').service });
126
126
  formatDetail(data.stats || data, { format: program.opts().output, title: 'File Storage Stats' });
127
127
  });
128
+
129
+ // F01 A': legacy files with no proven owner are hidden from every org until a human instance admin assigns them.
130
+ file
131
+ .command('quarantine')
132
+ .description('List files with no proven owner org (instance admin only)')
133
+ .action(async () => {
134
+ auth.requireAuth();
135
+ const ep = resolveEndpoint('files.quarantine.list');
136
+ const data = await client.get(ep.path, { service: ep.service });
137
+ formatOutput({ columns: ['id', 'name', 'size'], headers: ['ID', 'Name', 'Size'], rows: data.files || [] }, { format: program.opts().output });
138
+ });
139
+
140
+ file
141
+ .command('assign')
142
+ .description('Assign a quarantined file to an org (instance admin only; witnessed): <fileId> --org <orgId>, or --from-report <dry-run.json>')
143
+ .argument('[fileId]', 'quarantined file id')
144
+ .option('--org <orgId>', 'org that owns the file')
145
+ .option('--from-report <path>', 'an owner-approved backfill dry-run report: assigns only rows with a derived owner')
146
+ .action(async (fileId, options) => {
147
+ auth.requireAuth();
148
+ let assignments;
149
+ if (options.fromReport) {
150
+ const report = JSON.parse(readFileSync(options.fromReport, 'utf8'));
151
+ assignments = (report.rows || []).filter((r) => r && r.id && r.orgId).map((r) => ({ file_id: r.id, org_id: r.orgId }));
152
+ if (!assignments.length) throw new Error('The report has no rows with a derived owner; nothing to assign.');
153
+ } else if (fileId) {
154
+ if (!options.org) throw new Error('Give the owning org: quox file assign <fileId> --org <orgId>');
155
+ assignments = [{ file_id: fileId, org_id: options.org }];
156
+ } else {
157
+ throw new Error('Give <fileId> --org <orgId>, or --from-report <dry-run.json>');
158
+ }
159
+ const ep = resolveEndpoint('files.quarantine.assign');
160
+ const data = await client.post(ep.path, { service: ep.service, body: { assignments } });
161
+ formatOutput({ columns: ['file_id', 'org_id', 'ok', 'error'], headers: ['File', 'Org', 'Assigned', 'Error'], rows: data.results || [] }, { format: program.opts().output });
162
+ const failed = (data.results || []).filter((r) => !r.ok).length;
163
+ if (failed) { process.exitCode = 1; } else { printSuccess(`Assigned ${assignments.length} file(s).`); }
164
+ });
128
165
  }