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
@@ -102,6 +102,7 @@ export function registerLucidCommands(program, ctx) {
102
102
  if (o.json) { console.log(JSON.stringify(data, null, 2)); return; }
103
103
  if (o.agent) { console.log(data.text_agent ?? data.text ?? ''); return; }
104
104
  if (o.spoken) { console.log(data.spoken ?? data.text ?? ''); return; }
105
+ if (o.markdown) { const { renderSitrepMarkdown } = await import('../lucid/markdown.js'); console.log(renderSitrepMarkdown(data)); return; }
105
106
  const { renderSitrep } = await import('../lucid/sitrep.js');
106
107
  const tty = Boolean(process.stdout.isTTY);
107
108
  process.stdout.write(renderSitrep(data, {
@@ -118,14 +119,15 @@ export function registerLucidCommands(program, ctx) {
118
119
  const sitrepOpts = (c) => c
119
120
  .option('--agent', 'compact, id-rich form for agents')
120
121
  .option('--json', 'the full structured situation')
121
- .option('--spoken', 'sentences only, as read on the phone');
122
+ .option('--spoken', 'sentences only, as read on the phone')
123
+ .option('--markdown', 'chat-ready Markdown for Discord, Matrix and slots (fits one message)');
122
124
 
123
125
  sitrepOpts(lu
124
126
  .command('sitrep')
125
127
  .description('One-shot formatted snapshot (areas, what needs you, what changed, what I cannot see)'))
126
128
  .action(async (o) => showSitrep(o));
127
129
 
128
- // Top-level shortcut so any human or agent can ask for a sitrep: `quox sitrep [--agent|--json|--spoken]`.
130
+ // Top-level shortcut so any human or agent can ask for a sitrep: `quox sitrep [--agent|--json|--spoken|--markdown]`.
129
131
  sitrepOpts(program
130
132
  .command('sitrep')
131
133
  .description('LUCID situation report: what needs you, every area, what changed, what LUCID cannot see'))
@@ -393,9 +395,13 @@ export function registerLucidCommands(program, ctx) {
393
395
  if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
394
396
  if (data.status === 'filed') {
395
397
  printSuccess(`Filed${data.approval_id ? ` (approval ${data.approval_id})` : ''}. Approve it in Approvals.`);
396
- } else if (data.status === 'manual') {
398
+ } else if (data.status === 'manual' && data.command) {
397
399
  console.log('This one is manual. Run this yourself:');
398
- console.log(` ${data.command ?? ''}`);
400
+ console.log(` ${data.command}`);
401
+ } else if (data.status === 'manual') {
402
+ // #992: a 'say' measure has no command; print what it says and how to ask, never a blank instruction.
403
+ console.log(data.say || (m && m.label) || 'There is nothing to run for this one.');
404
+ console.log(` quox lucid ask "${((m && m.label) || 'Look into it').replace(/"/g, "'")} (${ref})"`);
399
405
  } else if (data.status === 'opened') {
400
406
  console.log(`Opened: ${data.url ?? ''}`);
401
407
  } else {
@@ -26,6 +26,7 @@ import { resolveEndpoint } from '../api/endpoints.js';
26
26
  import { QuoxError } from '../core/errors.js';
27
27
  import { resolveOrgId } from './helpers.js';
28
28
  import { resolveMyAttach } from './agentAttach.js';
29
+ import { hasExplicitProfile } from '../core/actingAs.js';
29
30
 
30
31
  // QUOXIDENTITY P9: which agent this session acts as is resolved session-scoped
31
32
  // (QUOX_T subtree / QUOX_ATTACH_AGENT override / single-attach fallback), so
@@ -46,7 +47,12 @@ async function callMemoryTool({ client, auth, program }, toolName, input) {
46
47
  const body = { tool_name: toolName, input };
47
48
  const requestOptions = { service: ep.service, body };
48
49
 
49
- const lease = detectSoleAttachLease();
50
+ // An explicit -p/--profile (or QUOX_PROFILE) is the caller saying "as me":
51
+ // it wins over the session's attach lease, same precedence as
52
+ // core/actingAs. Before this, `quox -p moo memory search` inside an attached
53
+ // session went out on the lease token and 401'd (proof engine 2026-10-14).
54
+ const argv = (program.rawArgs || process.argv).slice(2);
55
+ const lease = hasExplicitProfile(argv) ? null : detectSoleAttachLease();
50
56
  if (lease) {
51
57
  requestOptions.noAuth = true;
52
58
  requestOptions.headers = { Authorization: `Bearer ${lease.token}` };
@@ -260,16 +266,87 @@ export function registerMemoryCommands(program, ctx) {
260
266
  }, { format: program.opts().output });
261
267
  });
262
268
 
269
+ // QUOXMIND #1111: the old view counted only rows under YOUR user id, so an
270
+ // org holding hundreds of agent-written memories looked nearly empty. Default
271
+ // is now the org view (per agent, per writer), falling back honestly.
263
272
  memory
264
273
  .command('stats')
265
- .description('Show memory statistics')
266
- .action(async () => {
274
+ .description('Show memory statistics: the whole org by agent and writer (org admins), else your own')
275
+ .option('--mine', 'only memories written under your own login')
276
+ .action(async (options) => {
277
+ auth.requireAuth();
278
+ const ep = resolveEndpoint('memory.stats.list');
279
+ let data;
280
+ let note = '';
281
+ if (options.mine) {
282
+ data = await client.get(ep.path, { service: ep.service, query: {} });
283
+ } else {
284
+ try {
285
+ data = await client.get(ep.path, { service: ep.service, query: { scope: 'org' } });
286
+ } catch (err) {
287
+ if (err?.status !== 403) throw err;
288
+ data = await client.get(ep.path, { service: ep.service, query: {} });
289
+ note = 'Org-wide counts need an org admin or owner. Showing only memories written under your own login; agents write under their own identities, so the org holds more.';
290
+ }
291
+ }
292
+ const format = program.opts().output;
293
+ if (format === 'json') { console.log(JSON.stringify(note ? { ...data, note } : data, null, 2)); return; }
294
+ if (data?.scope !== 'org') {
295
+ formatDetail(data.stats || data, { format, title: 'Memory Stats (your login only)' });
296
+ console.log(note || 'Counts only memories written under your own login. `quox memory stats` without --mine shows the whole org (org admins).');
297
+ return;
298
+ }
299
+ const m = data.memories || {};
300
+ console.log(`Memory Stats (org): ${m.total ?? 0} valid memories, ${m.not_valid ?? 0} superseded or deleted (kept for history), ${data.pinned ?? 0} pinned, last write ${data.last_write_at || 'never'}`);
301
+ if (m.by_type) console.log(`By type: ${Object.entries(m.by_type).map(([k, v]) => `${k} ${v}`).join(', ')}`);
302
+ formatOutput({
303
+ columns: ['agent', 'count'], headers: ['Agent', 'Memories'],
304
+ rows: Object.entries(data.by_agent || {}).map(([agent, count]) => ({ agent, count })),
305
+ }, { format });
306
+ formatOutput({
307
+ columns: ['user_id', 'kind', 'count', 'agents'], headers: ['Writer', 'Kind', 'Memories', 'Agents'],
308
+ rows: (data.by_user || []).map((u) => ({ ...u, agents: (u.agents || []).join(',') })),
309
+ }, { format });
310
+ console.log('Valid = not deleted and not superseded. A writer is the login a memory was saved under: your own, an agent identity, service/system (internal agents), or org-shared beliefs.');
311
+ });
312
+
313
+ memory
314
+ .command('core')
315
+ .description('Show the core set: memories loaded into every chat turn (pinned, plus auto-promoted decisions and facts rated high or critical)')
316
+ .option('--limit <n>', 'max memories', '20')
317
+ .action(async (options) => {
267
318
  auth.requireAuth();
268
- const data = await client.get(resolveEndpoint('memory.stats.list').path, { service: resolveEndpoint('memory.stats.list').service });
269
- formatDetail(data.stats || data, {
270
- format: program.opts().output,
271
- title: 'Memory Stats',
272
- });
319
+ const ep = resolveEndpoint('memory.core-set');
320
+ const data = await client.get(ep.path, { service: ep.service, query: { limit: options.limit } });
321
+ formatOutput({
322
+ columns: ['id', 'core_reason', 'agent_id', 'memory_type', 'content'],
323
+ headers: ['ID', 'Why', 'Agent', 'Type', 'Content'],
324
+ rows: (data.memories || []).map((m) => ({ ...m, content: (m.content || '').slice(0, 80) })),
325
+ }, { format: program.opts().output, raw: data });
326
+ });
327
+
328
+ memory
329
+ .command('pin')
330
+ .description('Pin a memory into the core set so every chat turn carries it')
331
+ .argument('<id>', 'memory ID')
332
+ .action(async (id) => {
333
+ auth.requireAuth();
334
+ const ep = resolveEndpoint('memory.pin', { id });
335
+ const data = await client.post(ep.path, { service: ep.service, body: {} });
336
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
337
+ printSuccess(`Pinned ${id} into the core set.`);
338
+ });
339
+
340
+ memory
341
+ .command('unpin')
342
+ .description('Take a memory out of the core set (also stops it being auto-promoted)')
343
+ .argument('<id>', 'memory ID')
344
+ .action(async (id) => {
345
+ auth.requireAuth();
346
+ const ep = resolveEndpoint('memory.unpin', { id });
347
+ const data = await client.delete(ep.path, { service: ep.service });
348
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
349
+ printSuccess(`Unpinned ${id}: it stays a memory, but is no longer in the core set.`);
273
350
  });
274
351
 
275
352
  memory
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `quox mind implants|implant <id>|health`: QuoxMind cognitive implants (read-only).
3
+ * Collector routes under /api/v1/mind (org-scoped). Implants with no recorded
4
+ * activity say so; nothing is invented.
5
+ */
6
+ import { formatOutput } from '../core/output.js';
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { resolveOrgId } from './helpers.js';
9
+
10
+ export function registerMindCommands(program, ctx) {
11
+ const { client, auth } = ctx;
12
+ const get = async (name, params) => {
13
+ auth.requireAuth();
14
+ const ep = resolveEndpoint(name, params);
15
+ return client.get(ep.path, { service: ep.service, query: { org_id: resolveOrgId(auth, program) } });
16
+ };
17
+ const isJson = () => program.opts().output === 'json';
18
+
19
+ const mind = program.command('mind').description('QuoxMind: the cognitive implants (read-only)');
20
+
21
+ mind
22
+ .command('implants')
23
+ .description('List the cognitive implants: purpose, status, last activity')
24
+ .action(async () => {
25
+ const data = await get('mind.implants');
26
+ const rows = data.implants || [];
27
+ formatOutput(rows.map((i) => ({
28
+ ID: i.short,
29
+ NAME: i.name,
30
+ PURPOSE: i.purpose,
31
+ STATUS: i.status,
32
+ 'LAST ACTIVITY': i.last_activity || 'no activity recorded',
33
+ // QUOXMIND-PROOF: firing evidence from the implant's own table (null = not tracked).
34
+ 'LAST FIRED': i.activity_error ? 'unknown' : (typeof i.fired_7d === 'number' ? (i.last_fired || 'never') : 'not tracked'),
35
+ '7D': typeof i.fired_7d === 'number' && !i.activity_error ? i.fired_7d : '-',
36
+ })), {
37
+ format: program.opts().output,
38
+ columns: ['ID', 'NAME', 'STATUS', 'LAST FIRED', '7D', 'PURPOSE'],
39
+ emptyMessage: 'No implants registered.',
40
+ raw: rows,
41
+ });
42
+ });
43
+
44
+ mind
45
+ .command('implant <id>')
46
+ .description('One implant in detail (e.g. predict, reflect, repobrain, qortex)')
47
+ .action(async (id) => {
48
+ const d = await get('mind.implant', { id });
49
+ if (isJson()) { console.log(JSON.stringify(d, null, 2)); return; }
50
+ console.log(`${d.name} (${d.short}) · ${d.status}${d.tier ? ` · ${d.tier}` : ''}`);
51
+ console.log(d.description || d.purpose);
52
+ if (d.inspired_by) console.log(`Inspired by: ${d.inspired_by}`);
53
+ if (d.summary) console.log(`Now: ${d.summary}`);
54
+ console.log(d.activity);
55
+ if (typeof d.fired_7d === 'number' && !d.activity_error) console.log(`Fired: ${d.last_fired || 'never'} · ${d.fired_7d} in the last 7 days${d.activity_source ? ` (from ${d.activity_source})` : ''}`);
56
+ else if (d.activity_error) console.log(`Fired: unknown (${d.activity_error})`);
57
+ for (const [k, v] of Object.entries(d.metrics || {})) if (v !== null && typeof v !== 'object') console.log(` ${k}: ${v}`);
58
+ });
59
+
60
+ mind
61
+ .command('health')
62
+ .description('Summary of QuoxMind health across all implants')
63
+ .action(async () => {
64
+ const d = await get('mind.health');
65
+ if (isJson()) { console.log(JSON.stringify(d, null, 2)); return; }
66
+ console.log(`${d.total} implants: ${Object.entries(d.by_status).map(([k, v]) => `${v} ${k}`).join(', ')}`);
67
+ console.log(`${d.with_activity} with recorded activity, ${d.no_activity} with no activity recorded`);
68
+ if (typeof d.fired_7d_total === 'number') console.log(`Last 7 days: ${d.fired_7d_implants} implants fired (${d.fired_7d_total} firings), ${d.silent_7d} silent, ${d.activity_unknown} not tracked`);
69
+ console.log(d.average_health === null ? 'Average health: not enough data' : `Average health: ${d.average_health} (over ${d.scored} scored)`);
70
+ });
71
+ }
@@ -2,7 +2,7 @@
2
2
  * Monitoring commands: Docker fleet, Wazuh security monitoring, AGH-chain
3
3
  * agent honesty leaderboard, Prometheus metrics, and monitoring config.
4
4
  *
5
- * quox mon docker fleet|containers|container|logs|stats|stacks|df|action
5
+ * quox mon docker fleet|containers|container|inspect|logs|stats|stacks|df|action
6
6
  * quox mon wazuh status|agents|alerts|vulns|deploy-history|deploy-agent|deploy-bulk|active-response
7
7
  * quox mon agh-chain leaderboard|agent|events
8
8
  * quox mon metrics
@@ -36,6 +36,47 @@ async function confirmOrSkip(program, options, message) {
36
36
  return ans.confirm;
37
37
  }
38
38
 
39
+
40
+ /** One readable line per fact from the inspect health block (null when the container has no healthcheck). */
41
+ function healthRows(h) {
42
+ if (!h) return null;
43
+ const rows = [];
44
+ const streak = h.failing_streak ? ` (failing ${h.failing_streak} times)` : '';
45
+ rows.push(`health: ${h.status || 'unknown'}${streak}`);
46
+ if (h.test) {
47
+ const parts = [h.interval && `every ${h.interval}`, h.timeout && `timeout ${h.timeout}`, h.retries != null && `retries ${h.retries}`].filter(Boolean);
48
+ rows.push(`probe: ${h.test}${parts.length ? ' ' + parts.join(', ') : ''}`);
49
+ }
50
+ for (const p of h.last_probes || []) {
51
+ const o = String(p.output || '').replace(/\s+/g, ' ').trim();
52
+ rows.push(`last: exit ${p.exit_code}${o ? ` '${o}'` : ''}${p.end ? ` at ${p.end}` : ''}`);
53
+ }
54
+ return rows;
55
+ }
56
+
57
+ /**
58
+ * DEVFLEET-DOCKER: an error body is not data. If the collector reports a refused/failed Docker
59
+ * read, say so (stderr) and set a non-zero exit instead of printing an empty list. Partial fleets
60
+ * print the failed hosts as warnings. Returns true when the whole read failed (caller stops).
61
+ */
62
+ function reportDockerReadFailures(data) {
63
+ if (!data) return false;
64
+ const failedHosts = Array.isArray(data.hosts_failed) ? data.hosts_failed
65
+ : (data.success === false && Array.isArray(data.hosts) ? data.hosts.filter((h) => h && h.ok === false) : []);
66
+ if (data.success === false) {
67
+ const tag = data.refused ? ' (refused, error_code ' + (data.error_code || 'approval_required') + ')' : '';
68
+ console.error('Docker read failed' + tag + ': ' + (data.error || 'unknown error'));
69
+ for (const h of failedHosts) console.error(' ' + h.host + ': ' + (h.error || 'failed'));
70
+ process.exitCode = 1;
71
+ return true;
72
+ }
73
+ if (data.partial) {
74
+ console.error('Partial result: ' + failedHosts.length + ' host(s) could not be read; the list below is incomplete.');
75
+ for (const h of failedHosts) console.error(' ' + h.host + ': ' + (h.error || 'failed'));
76
+ }
77
+ return false;
78
+ }
79
+
39
80
  export function registerMonCommands(program, ctx) {
40
81
  const { client, auth } = ctx;
41
82
 
@@ -59,6 +100,7 @@ export function registerMonCommands(program, ctx) {
59
100
  auth.requireAuth();
60
101
  const ep = resolveEndpoint('mon.docker.fleet.list');
61
102
  const data = await client.get(ep.path, { service: ep.service, query: options.refresh ? { refresh: 'true' } : undefined });
103
+ if (reportDockerReadFailures(data)) return;
62
104
  const hosts = data.hosts || [];
63
105
  formatOutput({
64
106
  // fleet #392: docker fleet rows are id/containers_total/docker_version; ip and status don't exist.
@@ -82,6 +124,7 @@ export function registerMonCommands(program, ctx) {
82
124
  if (options.all === false) query.all = 'false';
83
125
  if (options.refresh) query.refresh = 'true';
84
126
  const data = await client.get(ep.path, { service: ep.service, query });
127
+ if (reportDockerReadFailures(data)) return;
85
128
  formatOutput({
86
129
  columns: ['host_id', 'id', 'name', 'image', 'state', 'status'],
87
130
  headers: ['Host', 'ID', 'Name', 'Image', 'State', 'Status'],
@@ -98,6 +141,9 @@ export function registerMonCommands(program, ctx) {
98
141
  auth.requireAuth();
99
142
  const ep = resolveEndpoint('mon.docker.containers.get', { id });
100
143
  const data = await client.get(ep.path, { service: ep.service, query: { host: options.host } });
144
+ // quox#1001: a 200 with success:false (e.g. "Bastion not configured") is a failure, not an
145
+ // empty container. Surface the error on stderr + exit non-zero instead of rendering a blank card.
146
+ if (reportDockerReadFailures(data)) return;
101
147
  formatDetail(data, {
102
148
  format: program.opts().output,
103
149
  title: `Container: ${id} (${options.host})`,
@@ -106,6 +152,24 @@ export function registerMonCommands(program, ctx) {
106
152
  });
107
153
  });
108
154
 
155
+ docker
156
+ .command('inspect')
157
+ .description('Show container state and health (failing probe command and last results)')
158
+ .argument('<id>', 'container ID')
159
+ .requiredOption('--host <hostId>', 'host the container lives on')
160
+ .action(async (id, options) => {
161
+ auth.requireAuth();
162
+ const ep = resolveEndpoint('mon.docker.containers.inspect', { id });
163
+ const data = await client.get(ep.path, { service: ep.service, query: { host: options.host } });
164
+ if (reportDockerReadFailures(data)) return;
165
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
166
+ const d = data.details || {};
167
+ console.log(`Container: ${id} (${options.host})`);
168
+ console.log(`state: ${d.status || 'unknown'} · image: ${d.image || 'unknown'}`);
169
+ const rows = healthRows(d.health);
170
+ console.log(rows ? rows.join('\n') : 'health: no healthcheck configured');
171
+ });
172
+
109
173
  docker
110
174
  .command('logs')
111
175
  .description('Show container logs')
@@ -143,6 +207,7 @@ export function registerMonCommands(program, ctx) {
143
207
  auth.requireAuth();
144
208
  const ep = resolveEndpoint('mon.docker.stacks.list');
145
209
  const data = await client.get(ep.path, { service: ep.service, query: options.host ? { host: options.host } : undefined });
210
+ if (reportDockerReadFailures(data)) return;
146
211
  formatOutput({
147
212
  columns: ['host_id', 'name', 'status', 'services'],
148
213
  headers: ['Host', 'Stack', 'Status', 'Services'],
@@ -181,7 +246,9 @@ export function registerMonCommands(program, ctx) {
181
246
  const ep = resolveEndpoint('mon.docker.containers.action', { id, action });
182
247
  const data = await client.post(ep.path, { service: ep.service, body: { host: options.host } });
183
248
  if (data.success === false) {
184
- printWarning(`Action failed: ${data.error || 'unknown error'}`);
249
+ // quox#1001: surface the real server error and exit non-zero (was exit 0 = success on failure).
250
+ printWarning(`Action failed: ${data.error || data.message || 'unknown error'}`);
251
+ process.exitCode = 1;
185
252
  return;
186
253
  }
187
254
  printSuccess(`Container ${id}: ${action} succeeded.`);
@@ -35,7 +35,7 @@ export function registerMonitoringCommands(program, ctx) {
35
35
  // nothing is stored); logged-in now actually sends the token.
36
36
  const data = await client.get(resolveEndpoint('v1.health.aggregate.list').path, {
37
37
  service: resolveEndpoint('v1.health.aggregate.list').service,
38
- timeout: 5000,
38
+ timeout: 15000, // #930: aggregate fan-out, 5s too tight under load
39
39
  });
40
40
  formatDetail(data, { format: program.opts().output, title: 'Platform Health' });
41
41
  });
@@ -5,8 +5,8 @@
5
5
  export function registerMotionCommand(program) {
6
6
  program
7
7
  .command('motion')
8
- .description('Play the Quox motion gallery: the Q intro, portal, particle, the Dragon\'s Eye, the status Q, ring and reveals (no server needed)')
9
- .argument('[scenes...]', 'only these: orbit portal particle eye hello perpetual ring fx')
8
+ .description('Play the Quox motion gallery: the Q intro, portal, particle, the Dragon\'s Eye, the status Q, ring, reveals and the living dragon (no server needed)')
9
+ .argument('[scenes...]', 'only these: orbit portal particle eye hello perpetual ring fx dragon dragon-fire dragon-moods')
10
10
  .option('--narrow', 'use the 40-column layout (the compact Q)')
11
11
  .option('--slow', 'play three times slower')
12
12
  .option('--mono', 'no colour (also honoured: NO_COLOR)')
@@ -23,6 +23,36 @@ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
23
23
  import { resolveEndpoint } from '../api/endpoints.js';
24
24
  import { resolveOrgId } from './helpers.js';
25
25
 
26
+ // codebase_query's schema (collector agentTools.js): question (required),
27
+ // target, max_snippets. quox#836: the CLI used to send query/repo/limit,
28
+ // every call failed "Missing required parameter: question", and the failure
29
+ // printed as "0 hits".
30
+ export function buildNucleusInput(question, options = {}) {
31
+ const input = { question };
32
+ if (options.target) input.target = options.target;
33
+ const n = parseInt(options.limit, 10);
34
+ if (n > 0) input.max_snippets = n;
35
+ return input;
36
+ }
37
+
38
+ // The tool result shape varies by engine version, so normalise rather than
39
+ // assuming: an unexpected shape must still print SOMETHING true. An error
40
+ // body is an error, never an empty result.
41
+ export function readNucleusResult(data) {
42
+ if (data && data.success === false) return { error: data.error || data.message || 'tool call failed' };
43
+ // The collector's codebase_query returns agent-friendly text with
44
+ // file:line citations ({ success, output, resultCount }), not an array.
45
+ if (typeof data?.output === 'string') return { text: data.output, count: data.resultCount ?? null, result: data };
46
+ const result = data && (data.result || data.output || data);
47
+ if (result && result.success === false) return { error: result.error || result.message || 'tool call failed' };
48
+ if (typeof result?.output === 'string') return { text: result.output, count: result.resultCount ?? null, result };
49
+ const results = Array.isArray(result?.results) ? result.results
50
+ : Array.isArray(result?.snippets) ? result.snippets
51
+ : Array.isArray(result) ? result
52
+ : null;
53
+ return { results, result };
54
+ }
55
+
26
56
  export function registerNucleusCommands(program, { client, auth }) {
27
57
  const nucleus = program
28
58
  .command('nucleus')
@@ -43,33 +73,34 @@ export function registerNucleusCommands(program, { client, auth }) {
43
73
  const started = Date.now();
44
74
 
45
75
  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
- };
76
+ const body = { tool_name: 'codebase_query', input: buildNucleusInput(question, options) };
54
77
  const orgId = resolveOrgId(auth, program);
55
78
  if (orgId) body.org_id = orgId;
56
79
 
57
80
  const data = await client.post(ep.path, { service: ep.service, body, timeout: 60000 });
58
81
  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;
82
+ const { error, results, result, text, count } = readNucleusResult(data);
67
83
 
68
84
  if (program.opts().output === 'json') {
69
85
  formatDetail(data, { format: 'json' });
70
86
  return;
71
87
  }
72
88
 
89
+ if (error) {
90
+ printEvent(glyphLine('repobrain', `query "${question}"`, { result: 'failed', ms }));
91
+ printWarning(`The query failed: ${error}`);
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+
96
+ if (typeof text === 'string') {
97
+ const hits = count === null ? 'answered' : `${count} hits`;
98
+ printEvent(glyphLine('repobrain', `query "${question}"`, { result: count === 0 ? '0 hits' : hits, ms }));
99
+ console.log('');
100
+ console.log(text.split('\n').map((l) => ` ${l}`).join('\n'));
101
+ return;
102
+ }
103
+
73
104
  if (!results || results.length === 0) {
74
105
  printEvent(glyphLine('repobrain', `query "${question}"`, { result: '0 hits', ms }));
75
106
  printWarning('Nothing matched. Try naming a concrete symbol, filename or behaviour rather than a general description.');
@@ -85,7 +116,7 @@ export function registerNucleusCommands(program, { client, auth }) {
85
116
  const line = r.line || r.start_line || null;
86
117
  console.log('');
87
118
  console.log(` ${loc}${line ? `:${line}` : ''}`);
88
- const snippet = r.snippet || r.content || r.text || '';
119
+ const snippet = r.snippet || r.code || r.content || r.text || '';
89
120
  for (const l of String(snippet).split('\n').slice(0, 20)) console.log(` ${l}`);
90
121
  }
91
122
  });
@@ -209,6 +209,17 @@ export function registerObjectiveCommands(program, ctx) {
209
209
  printSuccess(`Checkpoint ${checkpointId}: ${data.status}${data.objective_status ? ` (objective ${data.objective_status})` : ''}`);
210
210
  });
211
211
 
212
+ objective
213
+ .command('cancel')
214
+ .description('Cancel an objective (pending ones included, e.g. proof leftovers that never started); its open tasks are cancelled too. For a RUNNING loop prefer `halt`, which also kills the live session.')
215
+ .argument('<objectiveId>', 'objective ID')
216
+ .action(async (objectiveId) => {
217
+ auth.requireAuth();
218
+ const ep = resolveEndpoint('objectives.cancel', { id: objectiveId });
219
+ const data = await client.delete(ep.path, { service: ep.service });
220
+ printSuccess(`Objective ${objectiveId}: ${data?.status || 'cancelled'}`);
221
+ });
222
+
212
223
  objective
213
224
  .command('halt')
214
225
  .description('Halt a running QLoop loop ON DEMAND (unconditional human stop; no pending gate needed). Fires the same signed-kill as a gate deny.')
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `quox offload` : run a heavy command on a pool worker VM and behave like running it locally.
3
+ *
4
+ * quox offload [--dir <path>] [--fresh] [--keep] [--cores N --memory-gb N --disk-gb N] -- <command...>
5
+ * quox offload status
6
+ * quox offload release
7
+ *
8
+ * Engine: lib/core/offload.js. The worker is leased through the human pool REST API
9
+ * (same as `quox vm pool lease`) with a dedicated key at ~/.config/quox/offload_ed25519.
10
+ */
11
+
12
+ import path from 'node:path';
13
+ import { printInfo } from '../core/output.js';
14
+ import { makePoolRest } from '../core/vm.js';
15
+ import { OffloadError, realDeps, runOffload, releaseWorker, workerStatus, gitToplevel, WORKER_NAME_RE } from '../core/offload.js';
16
+
17
+ export function registerOffloadCommands(program, ctx, { deps: injected } = {}) {
18
+ const { client, auth } = ctx;
19
+ const json = () => program.opts().output === 'json';
20
+ const base = () => injected || realDeps({ rest: makePoolRest({ client }) });
21
+ // --worker <name> / QUOX_OFFLOAD_WORKER: your own worker (state, lease, size), not the box's shared one.
22
+ const mk = (options = {}) => {
23
+ const d = base();
24
+ const name = options.worker ?? d.env?.QUOX_OFFLOAD_WORKER;
25
+ if (name !== undefined && name !== '' && !WORKER_NAME_RE.test(String(name))) {
26
+ throw new OffloadError(`worker name must be lowercase letters, digits and dashes (up to 32), got ${JSON.stringify(name)}`);
27
+ }
28
+ return name ? Object.assign(Object.create(Object.getPrototypeOf(d)), d, { worker: String(name) }) : d;
29
+ };
30
+ const WORKER_HELP = 'use your own named worker (its own lease and size) instead of the shared one; or set QUOX_OFFLOAD_WORKER';
31
+
32
+ const off = program
33
+ .command('offload')
34
+ .description('Run a heavy command on a pool worker VM (synced dir, cached npm ci, live output, same exit code)')
35
+ .argument('[command...]', 'command to run remotely (put it after --)')
36
+ .option('--dir <path>', 'directory to sync (default: git toplevel of the current directory)')
37
+ .option('--fresh', 'release the current worker, lease a new one and reinstall dependencies')
38
+ .option('--keep', 'leave the worker leased for reuse (the default; kept for compatibility)')
39
+ .option('--cores <n>', 'vCPU cores for a newly leased worker')
40
+ .option('--memory-gb <n>', 'memory in GB for a newly leased worker')
41
+ .option('--disk-gb <n>', 'disk in GB for a newly leased worker')
42
+ .option('--worker <name>', WORKER_HELP)
43
+ .option('--docker', 'also give the worker Docker (user hyp in the docker group) and build the quox-sandbox images from the synced repo when they are missing or changed')
44
+ .allowExcessArguments(true)
45
+ .action(async (command, options) => {
46
+ auth.requireAuth();
47
+ const cwd = process.cwd();
48
+ const dir = path.resolve(options.dir || gitToplevel(cwd));
49
+ try {
50
+ const code = await runOffload(mk(options), {
51
+ dir, cwd, command, fresh: !!options.fresh, docker: !!options.docker,
52
+ cores: options.cores, memoryGb: options.memoryGb, diskGb: options.diskGb,
53
+ });
54
+ process.exitCode = code;
55
+ } catch (e) {
56
+ if (!(e instanceof OffloadError)) throw e;
57
+ process.stderr.write(`offload: ${e.message}\n`);
58
+ process.exitCode = 1;
59
+ }
60
+ });
61
+
62
+ off.command('status')
63
+ .description('Show your offload worker: vmid, ip, lease expiry, synced dirs')
64
+ .option('--worker <name>', WORKER_HELP)
65
+ .action(async (options) => {
66
+ auth.requireAuth();
67
+ const s = await workerStatus(mk(options));
68
+ if (json()) { console.log(JSON.stringify(s, null, 2)); return; }
69
+ if (!s.worker) { printInfo('no offload worker leased (it is leased on the next `quox offload -- <command>`)'); return; }
70
+ const w = s.worker;
71
+ console.log(`worker ${w.vmid} ${w.ip || '?'} ${w.state} lessee ${w.lessee} lease expires ${w.lease_expires_at || '?'}`);
72
+ for (const d of s.dirs) console.log(` synced ${d.local} -> ${d.remote} ${d.synced_at || ''}`);
73
+ });
74
+
75
+ off.command('release')
76
+ .description('Release (destroy) your offload worker via the pool')
77
+ .option('--worker <name>', WORKER_HELP)
78
+ .action(async (options) => {
79
+ auth.requireAuth();
80
+ try {
81
+ const vmid = await releaseWorker(mk(options));
82
+ if (json()) console.log(JSON.stringify({ released: vmid }));
83
+ else printInfo(vmid ? `released worker ${vmid}` : 'no offload worker to release');
84
+ } catch (e) {
85
+ process.stderr.write(`offload: release failed: ${e.message}\n`);
86
+ process.exitCode = 1;
87
+ }
88
+ });
89
+ }
@@ -64,7 +64,8 @@ export function inferTargetType(value) {
64
64
  /** curl install command for a host that is not yet a fleet-enrolled dev server. */
65
65
  export function buildOnboardInstallCommand(collectorBase, host) {
66
66
  const base = String(collectorBase || '').replace(/\/+$/, '');
67
- return `curl -sSL ${base}/install | sudo bash -s -- --host-id ${host}`;
67
+ // REL-423: fail loud, never `curl | sudo bash` (that pipe reports success when the download fails).
68
+ return `( f=$(mktemp) && trap 'rm -f "$f"' EXIT && curl -fsSL ${base}/install -o "$f" && sudo bash "$f" --host-id ${host} )`;
68
69
  }
69
70
 
70
71
  // ── the wizard ──────────────────────────────────────────────────────────────
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import { QuoxError } from '../core/errors.js';
6
- import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
+ import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
7
+ import { otherProfileUsers, listProcs } from '../core/profileUsers.js';
7
8
  import { resolveOrgId } from './helpers.js';
8
9
  import { resolveEndpoint } from '../api/endpoints.js';
9
10
 
@@ -120,6 +121,15 @@ export function registerOrgCommands(program, ctx) {
120
121
  config.setAuth(authData);
121
122
 
122
123
  printSuccess(`Switched to ${data.org?.name || orgId}`);
124
+ // #1034: this changed the SHARED profile. Name the other live sessions that will follow it.
125
+ try {
126
+ const name = config.getActiveProfileName?.();
127
+ const others = name ? otherProfileUsers({ profile: name, defaultProfile: config._data?.activeProfile, procs: listProcs(), selfPids: [process.pid, process.ppid] }) : [];
128
+ if (others.length) {
129
+ printWarning(`${others.length} other quox session${others.length === 1 ? '' : 's'} use profile "${name}" (pid ${others.map(p => p.pid).join(', ')}). `
130
+ + 'They follow this org on their next token refresh or restart. To move one window only, use /org switch inside it.');
131
+ }
132
+ } catch { /* best effort: never fail the switch over the warning */ }
123
133
  });
124
134
 
125
135
  // Members subcommands
@@ -0,0 +1,28 @@
1
+ /**
2
+ * quox peek <id>
3
+ *
4
+ * Look up an id without knowing what it is: an AEE envelope, inbox (approval) item, objective, workflow
5
+ * run, WARD receipt or agent. The id's shape picks what to ask; every lookup is an existing read
6
+ * endpoint; a miss names each kind tried and what it answered. The TUI's /peek prints the same card.
7
+ */
8
+ import { createPeekFetchers, peekId, peekLines } from '../core/peek.js';
9
+ import { resolveOrgId } from './helpers.js';
10
+
11
+ export function registerPeekCommand(program, ctx) {
12
+ const { client, auth } = ctx;
13
+ program
14
+ .command('peek')
15
+ .description('Look up an envelope, inbox item, objective, run, WARD receipt or agent by id (read only)')
16
+ .argument('<id>', 'the id, e.g. a ULID, a WARD entry UUID or an agent name')
17
+ .action(async (id) => {
18
+ auth.requireAuth();
19
+ const orgId = resolveOrgId(auth, program);
20
+ const r = await peekId(id, createPeekFetchers({ client, orgId: () => orgId }));
21
+ if (program.opts().output === 'json') {
22
+ console.log(JSON.stringify({ id: r.id, shape: r.shape, kind: r.hit?.kind || null, data: r.hit?.data || null, also: r.also, tried: r.tried }, null, 2));
23
+ } else {
24
+ for (const l of peekLines(r)) console.log(l.text);
25
+ }
26
+ if (!r.hit) process.exitCode = 5; // not found (the inbox verbs use 5 for not_found too)
27
+ });
28
+ }