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
@@ -2,41 +2,143 @@
2
2
  * Agent commands: CRUD, activate/deactivate, tool assignments
3
3
  */
4
4
 
5
+ import { readFileSync } from 'node:fs';
5
6
  import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
7
  import { resolveOrgId } from './helpers.js';
7
8
  import { resolveEndpoint } from '../api/endpoints.js';
9
+ import { QuoxApiError } from '../core/errors.js';
10
+ import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
8
11
  import { registerAgentAttachCommands } from './agentAttach.js';
9
12
 
13
+ // quox#1169: the prompt, domains and parent a user gives their own agent. Auth reads
14
+ // system_prompt_additions, domains[] and parent_agent_id; it has never read manifest_id.
15
+ function agentPromptFields(options) {
16
+ if (options.prompt !== undefined && options.promptFile !== undefined) {
17
+ throw new Error('Give either --prompt or --prompt-file, not both. Nothing was sent.');
18
+ }
19
+ if (options.manifestId !== undefined) {
20
+ throw new Error('--manifest-id is not supported by the server (it was always ignored). Nothing was sent.');
21
+ }
22
+ const out = {};
23
+ if (options.prompt !== undefined) out.system_prompt_additions = String(options.prompt);
24
+ if (options.promptFile !== undefined) {
25
+ try {
26
+ out.system_prompt_additions = readFileSync(options.promptFile, 'utf8');
27
+ } catch (err) {
28
+ throw new Error(`cannot read --prompt-file ${options.promptFile}: ${err.code || err.message}. Nothing was sent.`);
29
+ }
30
+ }
31
+ if (options.domain !== undefined) out.domains = String(options.domain).split(',').map((d) => d.trim()).filter(Boolean);
32
+ if (options.parent !== undefined) out.parent_agent_id = options.parent;
33
+ return out;
34
+ }
35
+
10
36
  export function registerAgentCommands(program, ctx) {
11
37
  const { client, auth } = ctx;
12
38
 
39
+ // Registered specialists (no org row): resolved from the collector registry, quox#927.
40
+ async function registryAgent(agentId) {
41
+ try {
42
+ const ep = resolveEndpoint('v1.agents.registry');
43
+ const data = await client.get(ep.path, { service: ep.service });
44
+ return (data?.agents || []).find((r) => r.agent_id === agentId) || null;
45
+ } catch { return null; }
46
+ }
47
+ async function registryGrants(agentId) {
48
+ try {
49
+ const ep = resolveEndpoint('v1.tools.agents.list');
50
+ const data = await client.get(ep.path, { service: ep.service });
51
+ const list = data?.agentTools?.[agentId];
52
+ return Array.isArray(list) ? list : null;
53
+ } catch { return null; }
54
+ }
55
+
13
56
  const agent = program
14
57
  .command('agent')
15
58
  .description('Manage agents');
16
59
 
17
60
  agent
18
61
  .command('list')
19
- .description('List agents')
20
- .option('--status <status>', 'filter by status')
62
+ .description('List registered specialist agents and the org\'s own agents')
63
+ .option('--status <status>', 'filter by status (org agents only)')
21
64
  .option('--level <level>', 'filter by level (0-4)')
65
+ .option('--source <source>', 'registry | org | all', 'all')
66
+ .option('--plugin <id>', 'only agents that require this plugin')
67
+ .option('--search <text>', 'substring match on id, name or description')
22
68
  .action(async (options) => {
23
69
  auth.requireAuth();
24
- const orgId = resolveOrgId(auth, program);
25
- const query = {};
26
- if (options.status) query.status = options.status;
27
- if (options.level) query.level = options.level;
28
- // fleet #392: tool_count is only computed when includeTools=true, which
29
- // was never sent; and rows carry a `domains` ARRAY, not a `domain`.
30
- query.includeTools = 'true';
31
- const data = await client.get(resolveEndpoint('agents.list', { orgId }).path, { service: resolveEndpoint('agents.list', { orgId }).service, query });
70
+ const source = String(options.source || 'all').toLowerCase();
71
+ if (!['registry', 'org', 'all'].includes(source)) {
72
+ throw new Error(`--source must be registry, org or all (got "${options.source}")`);
73
+ }
74
+ const notes = [];
75
+
76
+ // Registered specialists (server roster; bundled roster if the server lacks the route).
77
+ let registry = [];
78
+ if (source !== 'org') {
79
+ try {
80
+ const ep = resolveEndpoint('v1.agents.registry');
81
+ const data = await client.get(ep.path, { service: ep.service });
82
+ if (!Array.isArray(data?.agents)) throw new Error('unexpected response');
83
+ registry = data.agents.map(r => ({ ...r, source: 'registry' }));
84
+ } catch (err) {
85
+ notes.push(`note: server registry unavailable (${err.message}); showing the bundled roster, which may be out of date.`);
86
+ registry = loadBundledAssistantsRoster().map(r => ({
87
+ agent_id: r.id, name: r.name || r.id, level: null, requires_plugin: null, tool_count: null, source: 'registry',
88
+ }));
89
+ }
90
+ }
91
+
92
+ // The org's own agent rows.
93
+ let orgRows = [];
94
+ if (source !== 'registry') {
95
+ try {
96
+ const orgId = resolveOrgId(auth, program);
97
+ const query = { includeTools: 'true' };
98
+ if (options.status) query.status = options.status;
99
+ if (options.level) query.level = options.level;
100
+ const ep = resolveEndpoint('agents.list', { orgId });
101
+ const data = await client.get(ep.path, { service: ep.service, query });
102
+ orgRows = (data.agents || data || []).map(r => ({ ...r, source: 'org' }));
103
+ } catch (err) {
104
+ if (source === 'org') throw err;
105
+ notes.push(`note: org agents unavailable (${err.message}); showing registered specialists only.`);
106
+ }
107
+ }
108
+
109
+ // Merge by agent_id: org row wins for status, registry supplies plugin/description.
110
+ const merged = new Map();
111
+ for (const r of registry) merged.set(r.agent_id, r);
112
+ for (const o of orgRows) {
113
+ const key = o.agent_id || o.id;
114
+ const reg = merged.get(key);
115
+ merged.set(key, reg
116
+ ? { ...reg, ...o, requires_plugin: reg.requires_plugin, description: reg.description ?? o.description, tool_count: o.tool_count ?? reg.tool_count, source: 'org+registry' }
117
+ : o);
118
+ }
119
+
120
+ let all = [...merged.values()];
121
+ if (options.level !== undefined) all = all.filter(r => String(r.level) === String(options.level));
122
+ if (options.status) all = all.filter(r => r.status === options.status);
123
+ if (options.plugin) all = all.filter(r => r.requires_plugin === options.plugin);
124
+ if (options.search) {
125
+ const q = options.search.toLowerCase();
126
+ all = all.filter(r => [r.agent_id, r.name, r.description].some(v => String(v ?? '').toLowerCase().includes(q)));
127
+ }
128
+ all.sort((x, y) => String(x.agent_id).localeCompare(String(y.agent_id)));
129
+
130
+ for (const n of notes) console.error(n);
131
+ const dash = (v) => (v === null || v === undefined || v === '' ? '-' : v);
32
132
  formatOutput({
33
- columns: ['id', 'agent_id', 'name', 'level', 'status', 'domains', 'tool_count'],
34
- headers: ['ID', 'Agent ID', 'Name', 'Level', 'Status', 'Domains', 'Tools'],
35
- rows: (data.agents || data || []).map(a => ({
36
- ...a,
37
- domains: Array.isArray(a.domains) ? a.domains.join(', ') : (a.domains ?? '-'),
133
+ columns: ['agent_id', 'name', 'source', 'level', 'state', 'tool_count'],
134
+ headers: ['Agent ID', 'Name', 'Source', 'Level', 'Status/Plugin', 'Tools'],
135
+ rows: all.map(r => ({
136
+ ...r,
137
+ level: dash(r.level),
138
+ state: r.status ? r.status : (r.requires_plugin ? `plugin:${r.requires_plugin}` : '-'),
139
+ tool_count: dash(r.tool_count),
38
140
  })),
39
- }, { format: program.opts().output });
141
+ }, { format: program.opts().output, raw: all, emptyMessage: 'No agents match.' });
40
142
  });
41
143
 
42
144
  agent
@@ -46,7 +148,16 @@ export function registerAgentCommands(program, ctx) {
46
148
  .action(async (agentId) => {
47
149
  auth.requireAuth();
48
150
  const orgId = resolveOrgId(auth, program);
49
- const data = await client.get(resolveEndpoint('agents.get', { orgId, agentId }).path, { service: resolveEndpoint('agents.get', { orgId, agentId }).service });
151
+ let data;
152
+ try {
153
+ data = await client.get(resolveEndpoint('agents.get', { orgId, agentId }).path, { service: resolveEndpoint('agents.get', { orgId, agentId }).service });
154
+ } catch (err) {
155
+ // quox#927: `agent list` also shows registered specialists that have no org row;
156
+ // resolve those from the registry instead of answering 404 for an id list printed.
157
+ const reg = err?.status === 404 ? await registryAgent(agentId) : null;
158
+ if (!reg) throw err;
159
+ data = { agent: { ...reg, source: 'registry' } };
160
+ }
50
161
  formatDetail(data.agent || data, {
51
162
  format: program.opts().output,
52
163
  title: data.agent?.name || data.name,
@@ -62,19 +173,22 @@ export function registerAgentCommands(program, ctx) {
62
173
  .option('--domain <domain>', 'agent domain')
63
174
  .option('--description <desc>', 'description')
64
175
  .option('--parent <parentId>', 'parent agent ID')
65
- .option('--manifest-id <id>', 'manifest ID')
176
+ .option('--manifest-id <id>', 'not supported: refused (the server never read it)')
177
+ .option('--prompt <text>', "the agent's instructions (its system prompt)")
178
+ .option('--prompt-file <path>', "read the agent's instructions from a file")
66
179
  .action(async (options) => {
67
180
  auth.requireAuth();
68
181
  const orgId = resolveOrgId(auth, program);
182
+ const fields = agentPromptFields(options);
69
183
  const body = {
70
184
  name: options.name,
71
185
  level: parseInt(options.level, 10),
186
+ ...fields,
72
187
  };
73
188
  if (options.agentId) body.agent_id = options.agentId;
189
+ // legacy key kept so older servers and tests see it; auth reads domains[] (set above)
74
190
  if (options.domain) body.domain = options.domain;
75
191
  if (options.description) body.description = options.description;
76
- if (options.parent) body.parent_id = options.parent;
77
- if (options.manifestId) body.manifest_id = options.manifestId;
78
192
  await client.post(resolveEndpoint('agents.create', { orgId }).path, { service: resolveEndpoint('agents.create', { orgId }).service, body });
79
193
  printSuccess(`Agent "${options.name}" created.`);
80
194
  });
@@ -87,10 +201,14 @@ export function registerAgentCommands(program, ctx) {
87
201
  .option('--description <desc>', 'new description')
88
202
  .option('--level <level>', 'new level')
89
203
  .option('--status <status>', 'new status')
204
+ .option('--prompt <text>', "new instructions (system prompt)")
205
+ .option('--prompt-file <path>', 'read new instructions from a file')
206
+ .option('--domain <domains>', 'comma-separated domains')
207
+ .option('--parent <parentId>', 'parent agent ID')
90
208
  .action(async (agentId, options) => {
91
209
  auth.requireAuth();
92
210
  const orgId = resolveOrgId(auth, program);
93
- const body = {};
211
+ const body = { ...agentPromptFields(options) };
94
212
  if (options.name) body.name = options.name;
95
213
  if (options.description) body.description = options.description;
96
214
  if (options.level) body.level = parseInt(options.level, 10);
@@ -133,7 +251,7 @@ export function registerAgentCommands(program, ctx) {
133
251
  const orgId = resolveOrgId(auth, program);
134
252
  await client.patch(resolveEndpoint('agents.update', { orgId, agentId }).path, {
135
253
  service: resolveEndpoint('agents.update', { orgId, agentId }).service,
136
- body: { status: 'inactive' },
254
+ body: { status: 'paused' }, // #985: auth accepts draft|active|paused|retired; 'inactive' 400'd
137
255
  });
138
256
  printSuccess('Agent deactivated.');
139
257
  });
@@ -150,7 +268,15 @@ export function registerAgentCommands(program, ctx) {
150
268
  .action(async (agentId) => {
151
269
  auth.requireAuth();
152
270
  const orgId = resolveOrgId(auth, program);
153
- const data = await client.get(resolveEndpoint('agents.tools.list', { orgId, agentId }).path, { service: resolveEndpoint('agents.tools.list', { orgId, agentId }).service });
271
+ let data;
272
+ try {
273
+ data = await client.get(resolveEndpoint('agents.tools.list', { orgId, agentId }).path, { service: resolveEndpoint('agents.tools.list', { orgId, agentId }).service });
274
+ } catch (err) {
275
+ // quox#927: a registered specialist's tools are its registry grants, not org assignments.
276
+ const grants = err?.status === 404 ? await registryGrants(agentId) : null;
277
+ if (!grants) throw err;
278
+ data = { tools: grants.map((name) => ({ tool_id: name, tool_name: name, category: '-', access_mode: 'registry grant', enabled: true })) };
279
+ }
154
280
  formatOutput({
155
281
  // fleet #392: assignment rows carry tool_name/category/enabled.
156
282
  columns: ['tool_id', 'tool_name', 'category', 'access_mode', 'enabled'],
@@ -168,10 +294,20 @@ export function registerAgentCommands(program, ctx) {
168
294
  .action(async (agentId, options) => {
169
295
  auth.requireAuth();
170
296
  const orgId = resolveOrgId(auth, program);
171
- await client.post(resolveEndpoint('agents.tools.create', { orgId, agentId }).path, {
172
- service: resolveEndpoint('agents.tools.create', { orgId, agentId }).service,
173
- body: { tool_id: options.tool, access_mode: options.accessMode },
174
- });
297
+ try {
298
+ await client.post(resolveEndpoint('agents.tools.create', { orgId, agentId }).path, {
299
+ service: resolveEndpoint('agents.tools.create', { orgId, agentId }).service,
300
+ body: { tool_id: options.tool, access_mode: options.accessMode },
301
+ });
302
+ } catch (err) {
303
+ // quox#1072: a built-in agent's tools come from its registry grant and
304
+ // hat. The server says so in prose; surface that sentence as the error
305
+ // with a stable code, so it does not read as a generic conflict.
306
+ if (err?.status === 409 && err.body?.code === 'BUILTIN_AGENT_TOOLS_FROM_GRANT') {
307
+ throw new QuoxApiError(409, err.body.code, err.body.message || err.body.error, undefined, err.body);
308
+ }
309
+ throw err;
310
+ }
175
311
  printSuccess(`Tool assigned to agent.`);
176
312
  });
177
313
 
@@ -699,6 +699,36 @@ export async function heartbeatAttachLease(client, agentId, { getOrgId } = {}) {
699
699
  return { leaseId, expires: newExpiry || 'unchanged' };
700
700
  }
701
701
 
702
+ /**
703
+ * #893: re-lease an attach that LAPSED (expired while heartbeats could not get
704
+ * through, e.g. an auth stall longer than the TTL). Never takes over: a 409
705
+ * here means another session holds the agent, and the keeper then exits as
706
+ * before. Rewrites only the token, lease id and expiry; the installed kit
707
+ * (prompt, tools, memory) is unchanged.
708
+ */
709
+ const leaseMetaPath = (agentId) => join(dirname(tokenPath(agentId)), 'lease-meta.json');
710
+
711
+ export async function reacquireAttachLease(client, agentId) {
712
+ const stored = readStoredKit(agentId);
713
+ if (!stored?.lease?.id) throw new QuoxApiError(404, 'no_local_lease', `No local attach state for "${agentId}"`);
714
+ let meta = {};
715
+ try { meta = JSON.parse(readFileSync(leaseMetaPath(agentId), 'utf-8')); } catch { /* defaults */ }
716
+ const ep = resolveEndpoint('agent-attach.lease.create', { agentId });
717
+ const data = await client.post(ep.path, {
718
+ service: ep.service,
719
+ body: {
720
+ session_label: meta.session_label || agentId,
721
+ ttl_seconds: meta.ttl_seconds || 900,
722
+ takeover: false,
723
+ },
724
+ });
725
+ if (!data?.token || !data?.lease?.id) throw new Error('re-lease returned no token or lease id');
726
+ writeFile0600(tokenPath(agentId), `${data.token}\n`);
727
+ stored.lease = { ...stored.lease, id: data.lease.id, expires_at: data.lease.expires_at || data.expires_at || null };
728
+ writeFile0600(kitPath(agentId), `${JSON.stringify(stored, null, 2)}\n`);
729
+ return { leaseId: data.lease.id };
730
+ }
731
+
702
732
  /**
703
733
  * P10: keep the bound slot (or this session's membership of it) alive with the
704
734
  * ATTACH token. Failures are logged, never fatal to the lease heartbeat.
@@ -897,15 +927,36 @@ export function registerAgentAttachCommands(agent, ctx, program) {
897
927
  });
898
928
  } catch (err) {
899
929
  const status = err instanceof QuoxApiError ? err.status : 'network error';
930
+ // quox#1026 (b): the kit cannot resolve this agent at all, so no retry can succeed. Release the lease
931
+ // just granted (never leave it held) and say plainly which agents are attachable.
932
+ if (err instanceof QuoxApiError && err.status === 404 && err.code === 'unknown_agent' && lease.id) {
933
+ let released = false;
934
+ try {
935
+ const relEp = resolveEndpoint('agent-attach.lease.release', { leaseId: lease.id });
936
+ await client.post(relEp.path, { service: relEp.service, body: {} });
937
+ released = true;
938
+ } catch { /* reported below */ }
939
+ throw new Error(
940
+ `"${agentId}" is not attachable: only core registry agents and custom assistants can be attached `
941
+ + '(an agent made with `quox agent create` has no attach kit yet, quox#1026). '
942
+ + (released ? `The lease ${lease.id} was released.` : `Releasing lease ${lease.id} failed: run \`quox agent detach ${agentId} --lease ${lease.id}\`.`)
943
+ );
944
+ }
900
945
  throw new Error(
901
946
  `GET ${kitEp.path} failed (${status}) after the lease was granted (lease ${lease.id || 'unknown'}). `
902
- + `The lease is still active: retry the kit fetch, or run \`quox agent detach ${agentId}\` to release it. `
947
+ + `The lease is still active: retry the kit fetch, or run \`quox agent detach ${agentId}${lease.id ? ` --lease ${lease.id}` : ''}\` to release it. `
903
948
  + `Original error: ${err.message}`
904
949
  );
905
950
  }
906
951
 
907
952
  const splinter = splinterInfoFrom(leaseData?.splinter);
908
953
  const paths = installAttachKit(agentId, { kit, token, splinter });
954
+ // #893: remember the label and TTL (beside the token, not in kit.json,
955
+ // which stays exactly what the server sent) so the keeper can re-lease a
956
+ // lapsed attach under the same identity.
957
+ try {
958
+ writeFile0600(leaseMetaPath(agentId), `${JSON.stringify({ session_label: attachLabel, ttl_seconds: options.ttl })}\n`);
959
+ } catch { /* the attach stands; a re-lease falls back to defaults */ }
909
960
  // ONE-BRAIN CX4: agents whose resolved tool list carries brain_context get
910
961
  // a primer from the brain. Best effort, bounded, never fails the attach.
911
962
  let primer = '';
@@ -1074,6 +1125,7 @@ export function registerAgentAttachCommands(agent, ctx, program) {
1074
1125
  .option('--handoff <file>', 'path to a file with a handoff note (state, open threads, next moves) to store before detaching')
1075
1126
  .option('--handoff-text <text>', 'handoff note text, provided inline instead of a file')
1076
1127
  .option('--force-detach', 'release the lease even if storing the handoff note fails')
1128
+ .option('--lease <leaseId>', 'release this lease when there is no local attach kit (an attach that failed after its lease was granted); the server allows only its holder or an org admin')
1077
1129
  .option('--no-brain-note', 'do not also save the handoff to the knowledge brain as a private note')
1078
1130
  .action(async (agentId, options) => {
1079
1131
  auth.requireAuth();
@@ -1081,7 +1133,20 @@ export function registerAgentAttachCommands(agent, ctx, program) {
1081
1133
  agentId = await resolveAgentId(client, agentId);
1082
1134
  const stored = readStoredKit(agentId);
1083
1135
  const leaseId = stored?.lease?.id;
1084
- if (!leaseId) throw noLocalLeaseError(agentId, 'attach-status');
1136
+ // #1003: no local kit (attach failed after the lease was granted). Release exactly the lease the caller
1137
+ // names, never a guessed one: a lease held by another session must not be released by inference.
1138
+ if (!leaseId && options.lease) {
1139
+ const relEp = resolveEndpoint('agent-attach.lease.release', { leaseId: options.lease });
1140
+ await client.post(relEp.path, { service: relEp.service, body: {} });
1141
+ removeAttachDir(agentId);
1142
+ printEvent(events.released('quox', `attach ${agentId}`, `lease ${String(options.lease).slice(0, 8)} (no local kit)`));
1143
+ return;
1144
+ }
1145
+ if (!leaseId) {
1146
+ throw new Error(`${noLocalLeaseError(agentId, 'attach-status').message} `
1147
+ + `If an attach failed after its lease was granted, release that lease with \`quox agent detach ${agentId} --lease <leaseId>\` `
1148
+ + `(the lease id is in the attach error and in \`quox agent attach-status ${agentId}\`).`);
1149
+ }
1085
1150
 
1086
1151
  if (options.handoff && options.handoffText) {
1087
1152
  throw new QuoxError('Pass only one of --handoff or --handoff-text.', 2);
@@ -1173,6 +1238,7 @@ export function registerAgentAttachCommands(agent, ctx, program) {
1173
1238
  sleep: ctx.sleep,
1174
1239
  dir: ctx.keeperDir,
1175
1240
  heartbeat: () => heartbeatAttachLease(client, agentId, { getOrgId: () => resolveOrgId(auth, program) }),
1241
+ reacquire: () => reacquireAttachLease(client, agentId),
1176
1242
  release: async () => {
1177
1243
  const leaseId = readStoredKit(agentId)?.lease?.id;
1178
1244
  if (!leaseId) return;
@@ -1213,10 +1279,10 @@ export function registerAgentAttachCommands(agent, ctx, program) {
1213
1279
  // the route deployed) degrades to silence, not an error.
1214
1280
  try {
1215
1281
  const bindingEp = resolveEndpoint('agent-attach.slot-binding.get', { agentId });
1216
- const binding = await client.get(bindingEp.path, { service: bindingEp.service });
1217
- if (binding?.name) {
1218
- printInfo(` slot: ${binding.name}${binding.holder_session ? ` (held by ${binding.holder_session})` : ' (free)'}`);
1219
- }
1282
+ // quox#1308: the route answers { binding: { slot_name, ... } }; this read .name off the body and never printed.
1283
+ const res = await client.get(bindingEp.path, { service: bindingEp.service });
1284
+ const slotName = res?.binding?.slot_name;
1285
+ if (slotName) printInfo(` slot: ${slotName}`);
1220
1286
  } catch {
1221
1287
  // No binding, or the route isn't deployed yet: not a status-command failure.
1222
1288
  }
@@ -1328,6 +1394,28 @@ export function registerAgentAttachCommands(agent, ctx, program) {
1328
1394
  if (program.opts().output === 'json') formatDetail(data, { format: 'json' });
1329
1395
  });
1330
1396
 
1397
+ // quox#1308: bind-slot was write-only. Reading an address is open to any org member (the GET route has no
1398
+ // admin gate). No binding is an answer, not a failure: exit 0, and --json prints { binding: null }.
1399
+ agent
1400
+ .command('slot')
1401
+ .description("Show which slot an agent is bound to (the only slot a user-made agent may post to)")
1402
+ .argument('<agentId>', 'registry agent id')
1403
+ .action(async (agentId) => {
1404
+ auth.requireAuth();
1405
+ const ep = resolveEndpoint('agent-attach.slot-binding.get', { agentId });
1406
+ let binding = null;
1407
+ try {
1408
+ const res = await client.get(ep.path, { service: ep.service });
1409
+ binding = res?.binding || null;
1410
+ } catch (err) {
1411
+ const status = err instanceof QuoxApiError ? err.status : 'network error';
1412
+ if (!(status === 404 && err.code === 'no_binding')) throw new Error(`GET ${ep.path} failed (${status}): ${err.message}`);
1413
+ }
1414
+ if (program.opts().output === 'json') { formatDetail({ binding }, { format: 'json' }); return; }
1415
+ if (!binding) { printInfo(`${agentId} has no slot binding. An org admin can bind one: quox agent bind-slot ${agentId} <slot>`); return; }
1416
+ printInfo(`${agentId} is bound to slot ${binding.slot_name}${binding.created_at ? ` (since ${binding.created_at})` : ''}`);
1417
+ });
1418
+
1331
1419
  agent
1332
1420
  .command('unbind-slot')
1333
1421
  .description("Remove an agent identity's slot binding (org admin only)")
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Approvals commands: result (quox#791)
2
+ * Approvals commands: result (quox#791), grants list|create|revoke (fleet standing grants, 2026-10-13)
3
3
  *
4
4
  * After a human approves a held tool call (e.g. a gated ssh_exec) the collector
5
5
  * runs it and stores the outcome. `quox approvals result <id>` reads it back.
@@ -7,12 +7,15 @@
7
7
 
8
8
  import { resolveEndpoint } from '../api/endpoints.js';
9
9
 
10
+ import { attachWalkthrough } from './walkthrough.js';
11
+
10
12
  export function registerApprovalsCommands(program, ctx) {
11
13
  const { client, auth } = ctx;
12
14
 
13
15
  const approvals = program
14
16
  .command('approvals')
15
17
  .description('Tool approvals (held actions awaiting or past a human decision)');
18
+ attachWalkthrough(approvals, ctx, program);
16
19
 
17
20
  approvals
18
21
  .command('result <approvalId>')
@@ -44,4 +47,73 @@ export function registerApprovalsCommands(program, ctx) {
44
47
  if (out !== undefined) console.log(typeof out === 'string' ? out : JSON.stringify(out, null, 2));
45
48
  else console.log(JSON.stringify(r, null, 2));
46
49
  });
50
+
51
+ // Fleet standing grants: an org admin pre-approves ONE host + an anchored command
52
+ // shape + a VMID range until an expiry, for ssh_exec. Every use still lands in
53
+ // approvals history with an AEE envelope; anything outside the grant files HITL.
54
+ const grants = approvals
55
+ .command('grants')
56
+ .description('Standing grants: org-admin pre-approved fleet command lanes (ssh_exec)');
57
+
58
+ grants
59
+ .command('list')
60
+ .description('List active standing grants (--all includes revoked and expired)')
61
+ .option('--all', 'include revoked and expired grants')
62
+ .action(async (options) => {
63
+ auth.requireAuth();
64
+ const ep = resolveEndpoint('v1.approvals.grants.list');
65
+ const data = await client.get(ep.path, { service: ep.service, ...(options.all ? { query: { all: '1' } } : {}) });
66
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
67
+ const rows = data.grants || [];
68
+ if (!rows.length) { console.log('No standing grants.'); return; }
69
+ for (const g of rows) {
70
+ const state = g.revoked_at ? `revoked ${g.revoked_at}` : `expires ${g.expires_at}`;
71
+ const range = g.vmid_min != null ? ` vmids ${g.vmid_min}-${g.vmid_max}` : '';
72
+ console.log(`${g.grant_id} ${g.tool}@${g.host}${range} ${g.preset || `${g.patterns.length} patterns`} uses ${g.use_count} ${state}`);
73
+ console.log(` reason: ${g.reason} (by ${g.created_by})`);
74
+ }
75
+ });
76
+
77
+ grants
78
+ .command('create')
79
+ .description('Create a standing grant (human org admin only)')
80
+ .requiredOption('--host <hostId>', 'the ONE host the grant covers')
81
+ .requiredOption('--reason <text>', 'why (shown in history and evidence)')
82
+ .option('--preset <name>', 'proxmox-test-vm-lane (clone/power/destroy/guest-exec in a VMID range)')
83
+ .option('--template <vmid>', 'template VMID the lane clones from (preset)')
84
+ .option('--vmid-range <min-max>', 'VMID range, e.g. 9100-9199')
85
+ .option('--name-prefix <prefix>', 'VM name prefix the lane may create (preset)', 'z2h')
86
+ .option('--pattern <regex>', 'custom anchored command regex (repeatable; instead of --preset)', (v, a) => (a || []).concat([v]))
87
+ .option('--days <n>', 'expiry in days (max 30)', '14')
88
+ .action(async (options) => {
89
+ auth.requireAuth();
90
+ const body = { tool: 'ssh_exec', host: options.host, reason: options.reason, expires_in_days: Number(options.days) };
91
+ if (options.vmidRange) {
92
+ const m = /^(\d+)-(\d+)$/.exec(options.vmidRange);
93
+ if (!m) { console.error('--vmid-range must look like 9100-9199'); process.exitCode = 1; return; }
94
+ body.vmid_min = Number(m[1]); body.vmid_max = Number(m[2]);
95
+ }
96
+ if (options.preset) {
97
+ body.preset = options.preset; body.template_vmid = Number(options.template); body.name_prefix = options.namePrefix;
98
+ } else {
99
+ body.patterns = options.pattern || [];
100
+ }
101
+ const ep = resolveEndpoint('v1.approvals.grants.create');
102
+ const data = await client.post(ep.path, { service: ep.service, body });
103
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
104
+ const g = data.grant || {};
105
+ console.log(`Created ${g.grant_id}: ${g.tool}@${g.host}${g.vmid_min != null ? ` vmids ${g.vmid_min}-${g.vmid_max}` : ''}, expires ${g.expires_at}`);
106
+ console.log(`Revoke any time: quox approvals grants revoke ${g.grant_id}`);
107
+ });
108
+
109
+ grants
110
+ .command('revoke <grantId>')
111
+ .description('Revoke a standing grant now (human org admin only)')
112
+ .action(async (grantId) => {
113
+ auth.requireAuth();
114
+ const ep = resolveEndpoint('v1.approvals.grants.revoke', { id: grantId });
115
+ const data = await client.post(ep.path, { service: ep.service, body: {} });
116
+ if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
117
+ console.log(`Revoked ${data.grant?.grant_id || grantId} at ${data.grant?.revoked_at || 'now'}.`);
118
+ });
47
119
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * quox aside "<question>"
3
+ *
4
+ * A read-only side question: the model gets no tools and no conversation, so nothing it says can change
5
+ * anything or land in a conversation's history. The TUI's /aside is the same call with the recent
6
+ * exchanges passed as read-only context.
7
+ */
8
+ import { askAside } from '../core/aside.js';
9
+
10
+ export function registerAsideCommand(program, ctx) {
11
+ const { client, auth } = ctx;
12
+ program
13
+ .command('aside')
14
+ .description('Ask a read-only side question: no tools, not saved to any conversation')
15
+ .argument('<question...>', 'the question')
16
+ .option('--agent <id>', 'who answers (default quox)')
17
+ .action(async (words, opts) => {
18
+ auth.requireAuth();
19
+ const r = await askAside(client, { question: words.join(' '), agent: opts.agent });
20
+ if (program.opts().output === 'json') { console.log(JSON.stringify({ answer: r.text, model: r.model, provider: r.provider, tools_used: r.tools, read_only: true }, null, 2)); return; }
21
+ console.log(r.text || '(no answer)');
22
+ console.log(`· aside, read only: no tools, not saved${r.model ? ` · ${r.provider ? `${r.provider}/` : ''}${r.model}` : ''}`);
23
+ });
24
+ }