quoxcli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Approvals commands: result (quox#791)
3
+ *
4
+ * After a human approves a held tool call (e.g. a gated ssh_exec) the collector
5
+ * runs it and stores the outcome. `quox approvals result <id>` reads it back.
6
+ */
7
+
8
+ import { resolveEndpoint } from '../api/endpoints.js';
9
+
10
+ export function registerApprovalsCommands(program, ctx) {
11
+ const { client, auth } = ctx;
12
+
13
+ const approvals = program
14
+ .command('approvals')
15
+ .description('Tool approvals (held actions awaiting or past a human decision)');
16
+
17
+ approvals
18
+ .command('result <approvalId>')
19
+ .description('Show the status and stored output of an approved tool call')
20
+ .option('--org <id>', 'org ID (service-key automation callers; defaults to active org / QUOX_ORG_ID)')
21
+ .action(async (approvalId, options) => {
22
+ auth.requireAuth();
23
+ const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
24
+ const orgId = auth.getOrgId(options.org || process.env.QUOX_ORG_ID);
25
+ const query = orgId ? { org_id: orgId } : undefined;
26
+ const data = await client.get(ep.path, { service: ep.service, ...(query ? { query } : {}) });
27
+
28
+ if (program.opts().output === 'json') {
29
+ console.log(JSON.stringify(data, null, 2));
30
+ return;
31
+ }
32
+ console.log(`Approval ${data.approval_id} tool: ${data.tool} status: ${data.status}`
33
+ + (data.executed_at ? ` executed: ${data.executed_at}` : ''));
34
+ if (data.result === null || data.result === undefined) {
35
+ if (data.status === 'pending') console.log('Still pending: a human has not decided yet.');
36
+ else if (data.status === 'resuming') console.log('Approved, executing. Re-run shortly.');
37
+ else console.log('No stored output.');
38
+ return;
39
+ }
40
+ const r = data.result;
41
+ if (typeof r === 'string') { console.log(r); return; }
42
+ if (r.success === false) console.log(`Failed: ${r.error || 'unknown error'}`);
43
+ const out = r.output ?? r.stdout ?? r.result;
44
+ if (out !== undefined) console.log(typeof out === 'string' ? out : JSON.stringify(out, null, 2));
45
+ else console.log(JSON.stringify(r, null, 2));
46
+ });
47
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Assistant commands: CRUD, deploy, fork
3
+ */
4
+
5
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
+ import { resolveOrgId } from './helpers.js';
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+
9
+ export function registerAssistantCommands(program, ctx) {
10
+ const { client, auth } = ctx;
11
+
12
+ const assistant = program
13
+ .command('assistant')
14
+ .description('Manage assistants');
15
+
16
+ assistant
17
+ .command('list')
18
+ .description('List assistants')
19
+ .action(async () => {
20
+ auth.requireAuth();
21
+ const orgId = resolveOrgId(auth, program);
22
+ const data = await client.get(resolveEndpoint('assistants.list', { orgId }).path, { service: resolveEndpoint('assistants.list', { orgId }).service });
23
+ formatOutput({
24
+ // fleet #392: route returns active_deployments, not deployed.
25
+ columns: ['id', 'name', 'status', 'active_deployments', 'created_at'],
26
+ headers: ['ID', 'Name', 'Status', 'Deployed', 'Created'],
27
+ rows: data.items || data.assistants || data,
28
+ }, { format: program.opts().output });
29
+ });
30
+
31
+ assistant
32
+ .command('get')
33
+ .description('Get assistant details')
34
+ .argument('<assistantId>', 'assistant ID')
35
+ .action(async (assistantId) => {
36
+ auth.requireAuth();
37
+ const orgId = resolveOrgId(auth, program);
38
+ const data = await client.get(resolveEndpoint('assistants.get', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.get', { orgId, assistantId }).service });
39
+ formatDetail(data.assistant || data, { format: program.opts().output, title: data.assistant?.name || data.name });
40
+ });
41
+
42
+ assistant
43
+ .command('create')
44
+ .description('Create an assistant')
45
+ .requiredOption('--name <name>', 'assistant name')
46
+ .option('--persona <text>', 'persona/system prompt')
47
+ .option('--description <desc>', 'description')
48
+ .action(async (options) => {
49
+ auth.requireAuth();
50
+ const orgId = resolveOrgId(auth, program);
51
+ const body = { name: options.name };
52
+ if (options.persona) body.persona = options.persona;
53
+ if (options.description) body.description = options.description;
54
+ await client.post(resolveEndpoint('assistants.create', { orgId }).path, { service: resolveEndpoint('assistants.create', { orgId }).service, body });
55
+ printSuccess(`Assistant "${options.name}" created.`);
56
+ });
57
+
58
+ assistant
59
+ .command('update')
60
+ .description('Update an assistant')
61
+ .argument('<assistantId>', 'assistant ID')
62
+ .option('--name <name>', 'new name')
63
+ .option('--persona <text>', 'new persona')
64
+ .option('--description <desc>', 'new description')
65
+ .action(async (assistantId, options) => {
66
+ auth.requireAuth();
67
+ const orgId = resolveOrgId(auth, program);
68
+ const body = {};
69
+ if (options.name) body.name = options.name;
70
+ if (options.persona) body.persona = options.persona;
71
+ if (options.description) body.description = options.description;
72
+ await client.patch(resolveEndpoint('assistants.update', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.update', { orgId, assistantId }).service, body });
73
+ printSuccess('Assistant updated.');
74
+ });
75
+
76
+ assistant
77
+ .command('delete')
78
+ .description('Delete an assistant')
79
+ .argument('<assistantId>', 'assistant ID')
80
+ .action(async (assistantId) => {
81
+ auth.requireAuth();
82
+ const orgId = resolveOrgId(auth, program);
83
+ await client.delete(resolveEndpoint('assistants.delete', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.delete', { orgId, assistantId }).service });
84
+ printSuccess('Assistant deleted.');
85
+ });
86
+
87
+ assistant
88
+ .command('deploy')
89
+ .description('Deploy an assistant')
90
+ .argument('<assistantId>', 'assistant ID')
91
+ .option('--target <target>', 'deployment target')
92
+ .action(async (assistantId, options) => {
93
+ auth.requireAuth();
94
+ const orgId = resolveOrgId(auth, program);
95
+ const body = {};
96
+ if (options.target) body.target = options.target;
97
+ await client.post(resolveEndpoint('assistants.deploy', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.deploy', { orgId, assistantId }).service, body });
98
+ printSuccess('Assistant deployed.');
99
+ });
100
+
101
+ assistant
102
+ .command('fork')
103
+ .description('Fork an assistant')
104
+ .argument('<assistantId>', 'assistant ID to fork')
105
+ .option('--name <name>', 'name for the fork')
106
+ .action(async (assistantId, options) => {
107
+ auth.requireAuth();
108
+ const orgId = resolveOrgId(auth, program);
109
+ const body = {};
110
+ if (options.name) body.name = options.name;
111
+ await client.post(resolveEndpoint('assistants.fork', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.fork', { orgId, assistantId }).service, body });
112
+ printSuccess('Assistant forked.');
113
+ });
114
+ }
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Auth commands: login, logout, whoami, signup, mfa
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import { formatDetail, printSuccess, printInfo } from '../core/output.js';
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { loadResidentProfile, residentLogin, resolveResidentBaseUrl } from '../core/residentAuth.js';
9
+ import { readStoredKit, resolveMyAttach, sessionKey, myAttachState } from './agentAttach.js';
10
+ import { resolveActing } from '../core/actingAs.js';
11
+
12
+ export function registerAuthCommands(program, ctx) {
13
+ const { config, auth, client } = ctx;
14
+
15
+ // quox login
16
+ program
17
+ .command('login')
18
+ .description('Authenticate with QuoxCORE')
19
+ .option('--email <email>', 'email address')
20
+ .option('--password <password>', 'password (prefer --password-stdin: argv is visible in process lists)')
21
+ .option('--password-stdin', 'read the password from stdin (one line) — no secret in argv or shell history')
22
+ .option('--mfa-code <code>', 'TOTP code for MFA')
23
+ .option('--org <orgId>', 'organization to log into')
24
+ .option('--resident', 'authenticate as a resident agent using the on-disk Ed25519 identity, no password, no human')
25
+ .option('--resident-profile <path>', 'path to the resident bootstrap profile (default: ~/.config/quox/resident-profile.json)')
26
+ .action(async (options) => {
27
+ // Resident agent path: signed-challenge exchange with an Ed25519 key
28
+ // on disk, entirely separate from the email/password flow below.
29
+ if (options.resident) {
30
+ const ora = (await import('ora')).default;
31
+ const spinner = ora('Authenticating (resident identity)...').start();
32
+ try {
33
+ const profile = loadResidentProfile(options.residentProfile);
34
+ const baseUrl = resolveResidentBaseUrl(config, profile);
35
+ const tokenData = await residentLogin({ baseUrl, profile });
36
+
37
+ const authData = {
38
+ access_token: tokenData.access_token,
39
+ refresh_token: null,
40
+ user_id: tokenData.user?.id || null,
41
+ email: tokenData.user?.email || null,
42
+ display_name: tokenData.user?.display_name || null,
43
+ org_id: profile.org_id,
44
+ org_name: null,
45
+ org_slug: null,
46
+ org_role: tokenData.user?.role || null,
47
+ actor_type: tokenData.user?.actor_type || null,
48
+ host_id: tokenData.user?.host_id || profile.host_id,
49
+ kind: 'resident',
50
+ resident_profile_path: profile._path,
51
+ expires_at: Date.now() + (tokenData.expires_in || 900) * 1000,
52
+ };
53
+ config.setAuth(authData);
54
+
55
+ // quox#455: persist the bootstrap's reach-back URLs onto this
56
+ // profile so every later command (collector, slots, dev) resolves
57
+ // services from what this host actually reaches, never a
58
+ // port/scheme guess. Only fill the profile URL when it is still
59
+ // the local default; an explicit URL a human set always wins.
60
+ const active = typeof config.getActiveProfile === 'function' ? config.getActiveProfile() : null;
61
+ // Any loopback URL counts as "not deliberately set". An exact-string
62
+ // check against http://localhost:3000 missed https://localhost:3000
63
+ // and left the profile base pointing at the host's own empty
64
+ // instance, which only stayed invisible because the per-service
65
+ // overrides below happen to cover the services in use today.
66
+ const urlIsDefault = (() => {
67
+ if (!active?.url) return true;
68
+ try {
69
+ return ['localhost', '127.0.0.1', '::1'].includes(new URL(active.url).hostname);
70
+ } catch {
71
+ return true;
72
+ }
73
+ })();
74
+ // quox#455 follow-up / quox#469: stamp AUTH as well as collector.
75
+ // Stamping only collector left every auth-service call (whoami, and
76
+ // the token refresh itself) resolving through getServiceUrl('auth'),
77
+ // which port-swaps the edge to loopback :3101 and is unreachable
78
+ // from any remote host. The acceptance owner hit exactly that:
79
+ // login worked zero-override while whoami and slot ops still needed
80
+ // manual QUOX_AUTH_URL/QUOX_COLLECTOR_URL.
81
+ const services = {};
82
+ if (profile.collector_url) services.collector = profile.collector_url;
83
+ if (profile.instance_url) services.auth = profile.instance_url;
84
+ config.setProfileConnection({
85
+ ...(urlIsDefault && profile.instance_url ? { url: profile.instance_url } : {}),
86
+ ...(Object.keys(services).length ? { services } : {}),
87
+ });
88
+
89
+ spinner.stop();
90
+ printSuccess(`Logged in as resident agent ${authData.email || authData.user_id || authData.host_id} (host ${authData.host_id})`);
91
+ } catch (err) {
92
+ spinner.fail('Resident login failed');
93
+ throw err;
94
+ }
95
+ return;
96
+ }
97
+
98
+ let { email, password, mfaCode } = options;
99
+ const orgId = options.org || program.opts().org;
100
+
101
+ // SVC-AUTH #447 bridge: non-interactive automation reads the secret
102
+ // from stdin (or QUOX_LOGIN_PASSWORD / QUOX_LOGIN_EMAIL env), never
103
+ // argv, so no credential lands in process lists or shell history.
104
+ if (!email && process.env.QUOX_LOGIN_EMAIL) email = process.env.QUOX_LOGIN_EMAIL;
105
+ if (!password && options.passwordStdin) {
106
+ const chunks = [];
107
+ for await (const chunk of process.stdin) chunks.push(chunk);
108
+ password = Buffer.concat(chunks).toString('utf8').split('\n')[0].trim();
109
+ if (!password) {
110
+ console.error('Error: --password-stdin given but stdin was empty');
111
+ process.exit(1);
112
+ }
113
+ }
114
+ if (!password && process.env.QUOX_LOGIN_PASSWORD) password = process.env.QUOX_LOGIN_PASSWORD;
115
+
116
+ // Interactive prompts if not provided
117
+ if (!email || !password) {
118
+ const { default: inquirer } = await import('inquirer');
119
+
120
+ if (!email) {
121
+ const ans = await inquirer.prompt([{
122
+ type: 'input',
123
+ name: 'email',
124
+ message: 'Email:',
125
+ validate: v => v.includes('@') || 'Please enter a valid email',
126
+ }]);
127
+ email = ans.email;
128
+ }
129
+
130
+ if (!password) {
131
+ const ans = await inquirer.prompt([{
132
+ type: 'password',
133
+ name: 'password',
134
+ message: 'Password:',
135
+ mask: '*',
136
+ }]);
137
+ password = ans.password;
138
+ }
139
+ }
140
+
141
+ const baseUrl = config.getServiceUrl('auth');
142
+ const ora = (await import('ora')).default;
143
+ const spinner = ora('Authenticating...').start();
144
+
145
+ try {
146
+ const result = await auth.login(baseUrl, email, password, orgId);
147
+ spinner.stop();
148
+
149
+ // Handle MFA
150
+ if (result.mfa_required) {
151
+ if (!mfaCode) {
152
+ printInfo('MFA required. Check your authenticator app.');
153
+ const { default: inquirer } = await import('inquirer');
154
+ const ans = await inquirer.prompt([{
155
+ type: 'input',
156
+ name: 'code',
157
+ message: 'TOTP code:',
158
+ }]);
159
+ mfaCode = ans.code;
160
+ }
161
+
162
+ const mfaSpinner = ora('Verifying MFA...').start();
163
+ await auth.verifyMfa(baseUrl, result.access_token, mfaCode);
164
+ mfaSpinner.succeed('MFA verified');
165
+ }
166
+
167
+ const authData = config.getAuth();
168
+ // quox#647: login resolves and SETS the active org, silently overriding
169
+ // any prior `quox org switch` (the root cause of wrong-org actions).
170
+ // Always surface the resulting active org so the reset is visible
171
+ // (owner: "at least print 'active org set to X'"). Fall back
172
+ // org_name -> org_id like whoami, since resident logins carry org_id but
173
+ // no org_name (quox#463) and this line used to blank out entirely.
174
+ const orgLabel = authData.org_name
175
+ ? `${authData.org_name} (${authData.org_slug})`
176
+ : (authData.org_id || 'unknown');
177
+ printSuccess(`Logged in as ${authData.email}`);
178
+ printInfo(`Active org set to ${orgLabel}`);
179
+ } catch (err) {
180
+ spinner.fail('Login failed');
181
+ throw err;
182
+ }
183
+ });
184
+
185
+ // quox logout
186
+ program
187
+ .command('logout')
188
+ .description('End current session')
189
+ .action(async () => {
190
+ const baseUrl = config.getServiceUrl('auth');
191
+ await auth.logout(baseUrl);
192
+ printSuccess('Logged out. Tokens cleared.');
193
+ });
194
+
195
+ // quox whoami
196
+ program
197
+ .command('whoami')
198
+ .description('Show current user and session info')
199
+ .action(async () => {
200
+ auth.requireAuth();
201
+ const data = await client.get(resolveEndpoint('auth.me.list').path, { service: resolveEndpoint('auth.me.list').service });
202
+ const authData = config.getAuth();
203
+
204
+ // quox#463: resident logins carry org_id but no org_name (the
205
+ // bootstrap has only the id), and this rendered a real membership as
206
+ // 'none'. Fall back to the id rather than claiming no org.
207
+ //
208
+ // quox#463 follow-up: some resident sessions have no active org_id in
209
+ // the stored session/profile at all, even though `quox org list` on
210
+ // the same session shows a real membership. Whoami is a read verb, so
211
+ // when the active org is unset we resolve it by fetching the caller's
212
+ // org memberships through the exact same endpoint `quox org list`
213
+ // uses, and display the single membership (or the first, marked as
214
+ // resolved from membership) instead of the literal 'none'. Only an
215
+ // empty membership list still says 'none'. This never writes back to
216
+ // stored auth state; it only changes what is displayed.
217
+ let orgDisplay;
218
+ if (authData.org_name) {
219
+ orgDisplay = `${authData.org_name} (${authData.org_slug})`;
220
+ } else if (authData.org_id) {
221
+ orgDisplay = authData.org_id;
222
+ } else {
223
+ let memberships = [];
224
+ try {
225
+ const orgsEp = resolveEndpoint('orgs.list');
226
+ const orgsData = await client.get(orgsEp.path, { service: orgsEp.service });
227
+ const list = orgsData.organizations || orgsData.orgs || orgsData;
228
+ memberships = Array.isArray(list) ? list : [];
229
+ } catch {
230
+ memberships = [];
231
+ }
232
+ if (memberships.length > 0) {
233
+ const m = memberships[0];
234
+ const label = m.name || m.id;
235
+ orgDisplay = `${label}${m.slug ? ` (${m.slug})` : ''} (resolved from membership)`;
236
+ } else {
237
+ orgDisplay = 'none';
238
+ }
239
+ }
240
+
241
+ const info = {
242
+ user: data.user?.display_name || data.user?.email || authData.email,
243
+ email: data.user?.email || authData.email,
244
+ user_id: data.user?.id || authData.user_id,
245
+ org: orgDisplay,
246
+ role: authData.org_role || 'n/a',
247
+ mfa: data.user?.mfa_enabled ? 'enabled' : 'disabled',
248
+ session: authData.expires_at
249
+ ? `expires in ${Math.max(0, Math.round((authData.expires_at - Date.now()) / 60000))}m`
250
+ : 'unknown',
251
+ profile: config.getActiveProfileName(),
252
+ };
253
+ const fields = ['user', 'email', 'user_id', 'org', 'role', 'mfa', 'session', 'profile'];
254
+ const labels = {
255
+ user: 'User',
256
+ email: 'Email',
257
+ user_id: 'User ID',
258
+ org: 'Org',
259
+ role: 'Role',
260
+ mfa: 'MFA',
261
+ session: 'Session',
262
+ profile: 'Profile',
263
+ };
264
+
265
+ // Resident agent identity (quox#453 phase 15): only shown when the
266
+ // stored identity actually carries it, so a normal human login is
267
+ // unaffected.
268
+ const actorType = data.user?.actor_type || authData.actor_type;
269
+ const hostId = data.user?.host_id || authData.host_id;
270
+ if (actorType) {
271
+ info.actor_type = actorType;
272
+ labels.actor_type = 'Actor Type';
273
+ fields.splice(fields.indexOf('role') + 1, 0, 'actor_type');
274
+ }
275
+ if (hostId) {
276
+ info.host_id = hostId;
277
+ labels.host_id = 'Host ID';
278
+ fields.splice(fields.indexOf('mfa'), 0, 'host_id');
279
+ }
280
+
281
+ formatDetail(info, {
282
+ format: program.opts().output,
283
+ fields,
284
+ labels,
285
+ });
286
+
287
+ // F4 (QuoxIdentity CLI finding-fix): whoami answered "who is this
288
+ // token" but not "is this session also attached as an agent identity".
289
+ // QUOXIDENTITY P9: report only THIS session's agent (resolved
290
+ // session-scoped), not every attach on the box — the old scan listed
291
+ // every session's lease, which is exactly the "single resident sees all"
292
+ // flaw. A scan failure must never fail whoami itself.
293
+ try {
294
+ // Show what PLAIN verbs would act as (whoami itself is exempt from the default).
295
+ const acting = await resolveActing({ argv: process.argv.slice(2).filter((x) => x !== 'whoami') .concat(['_probe']) });
296
+ if (acting.source === 'flag' || acting.source === 'env' || acting.source === 'default') {
297
+ const askit = readStoredKit(acting.name);
298
+ const why = acting.source === 'flag' ? '--as' : acting.source === 'env' ? 'QUOX_AS' : 'default: attached session';
299
+ printInfo(
300
+ ` Acting as ${acting.name} (${why})`
301
+ + (askit?.lease?.id ? ` (attach lease ${askit.lease.id}, expires ${askit.lease.expires_at || 'unknown'})` : '')
302
+ );
303
+ } else if (acting.source === 'self') {
304
+ printInfo(' Acting as yourself (--as self)');
305
+ } else if (acting.source === 'profile') {
306
+ printInfo(' Acting as yourself (-p/--profile given)');
307
+ } else if (myAttachState().state === 'ambiguous') {
308
+ printInfo(' Acting as yourself: this session has more than one attach (ambiguous), so no agent default applies. Pick one with --as <agent>.');
309
+ }
310
+ } catch {
311
+ // Best effort only.
312
+ }
313
+ try {
314
+ const mine = resolveMyAttach();
315
+ if (mine) {
316
+ const kit = readStoredKit(mine.agentId);
317
+ const key = sessionKey();
318
+ printInfo(
319
+ ` attached as ${mine.agentId}`
320
+ + (kit?.lease?.id ? ` (lease ${String(kit.lease.id).slice(0, 8)}, expires ${kit.lease.expires_at || 'unknown'})` : '')
321
+ + (key ? ` · session ${key}` : '')
322
+ );
323
+ }
324
+ } catch {
325
+ // Best effort only: never fail whoami on an attach-state scan error.
326
+ }
327
+ });
328
+
329
+ // quox signup
330
+ program
331
+ .command('signup')
332
+ .description('Create a new user account')
333
+ .requiredOption('--email <email>', 'email address')
334
+ .requiredOption('--password <password>', 'password')
335
+ .requiredOption('--name <name>', 'display name')
336
+ .action(async (options) => {
337
+ const data = await client.post(resolveEndpoint('auth.signup').path, {
338
+ service: resolveEndpoint('auth.signup').service,
339
+ body: {
340
+ email: options.email,
341
+ password: options.password,
342
+ display_name: options.name,
343
+ },
344
+ noAuth: true,
345
+ });
346
+ printSuccess(`Account created for ${options.email}. Check your email for verification.`);
347
+ });
348
+
349
+ // quox mfa
350
+ const mfa = program
351
+ .command('mfa')
352
+ .description('Manage multi-factor authentication');
353
+
354
+ mfa
355
+ .command('setup')
356
+ .description('Enable MFA for your account')
357
+ .action(async () => {
358
+ auth.requireAuth();
359
+ const data = await client.post(resolveEndpoint('mfa.setup').path, { service: resolveEndpoint('mfa.setup').service });
360
+ if (data.qr_code || data.secret) {
361
+ console.log(chalk.bold('MFA Setup'));
362
+ if (data.secret) console.log(`Secret: ${data.secret}`);
363
+ if (data.qr_code) console.log(`QR URL: ${data.qr_code}`);
364
+ printInfo('Enter the code from your authenticator app to verify.');
365
+ } else {
366
+ printSuccess('MFA setup initiated. Check the response for setup instructions.');
367
+ }
368
+ });
369
+
370
+ mfa
371
+ .command('verify')
372
+ .description('Verify MFA setup with a code')
373
+ .requiredOption('--code <code>', 'TOTP verification code')
374
+ .action(async (options) => {
375
+ auth.requireAuth();
376
+ await client.post(resolveEndpoint('mfa.verify-setup').path, {
377
+ service: resolveEndpoint('mfa.verify-setup').service,
378
+ body: { code: options.code },
379
+ });
380
+ printSuccess('MFA enabled successfully.');
381
+ });
382
+
383
+ mfa
384
+ .command('disable')
385
+ .description('Disable MFA for your account')
386
+ .requiredOption('--code <code>', 'TOTP code to confirm')
387
+ .action(async (options) => {
388
+ auth.requireAuth();
389
+ await client.post(resolveEndpoint('mfa.disable').path, {
390
+ service: resolveEndpoint('mfa.disable').service,
391
+ body: { code: options.code },
392
+ });
393
+ printSuccess('MFA disabled.');
394
+ });
395
+
396
+ mfa
397
+ .command('backup-codes')
398
+ .description('View MFA backup codes')
399
+ .action(async () => {
400
+ auth.requireAuth();
401
+ const data = await client.get(resolveEndpoint('mfa.backup-codes.list').path, { service: resolveEndpoint('mfa.backup-codes.list').service });
402
+ if (program.opts().output === 'json') {
403
+ console.log(JSON.stringify(data, null, 2));
404
+ } else {
405
+ console.log(chalk.bold('Backup Codes'));
406
+ console.log(chalk.dim('Save these somewhere safe:\n'));
407
+ for (const code of (data.codes || data.backup_codes || [])) {
408
+ console.log(` ${code}`);
409
+ }
410
+ }
411
+ });
412
+
413
+ // ── quox account ────────────────────────────────────────────────────────
414
+ // fleet #390: PATCH /api/auth/me/password existed, worked and audited, but
415
+ // no command exposed it — so rotating your own password on a headless box
416
+ // meant hand-crafting a curl with a bearer token. Credential rotation is
417
+ // basic hygiene; making the correct action harder than doing nothing is
418
+ // the wrong incentive for a governance product.
419
+ const account = program
420
+ .command('account')
421
+ .description('Your own account (password, session)');
422
+
423
+ account
424
+ .command('password')
425
+ .description('Change your password')
426
+ // Values are prompted by default. --current/--new exist for automation,
427
+ // but they land in argv and shell history, so the help says so plainly
428
+ // rather than presenting them as equivalent.
429
+ .option('--current <password>', 'current password (WARNING: visible in argv/shell history — prefer the prompt)')
430
+ .option('--new <password>', 'new password (same warning)')
431
+ .action(async (options) => {
432
+ auth.requireAuth();
433
+
434
+ let current = options.current;
435
+ let next = options.new;
436
+
437
+ if (!current || !next) {
438
+ const { default: inquirer } = await import('inquirer');
439
+ if (!current) {
440
+ const a = await inquirer.prompt([{ type: 'password', name: 'v', message: 'Current password:', mask: '*' }]);
441
+ current = a.v;
442
+ }
443
+ if (!next) {
444
+ const a = await inquirer.prompt([{ type: 'password', name: 'v', message: 'New password:', mask: '*' }]);
445
+ const b = await inquirer.prompt([{ type: 'password', name: 'v', message: 'Confirm new password:', mask: '*' }]);
446
+ if (a.v !== b.v) throw new Error('New passwords do not match.');
447
+ next = a.v;
448
+ }
449
+ }
450
+
451
+ const ep = resolveEndpoint('auth.me.password');
452
+ await client.patch(ep.path, {
453
+ service: ep.service,
454
+ body: { current_password: current, new_password: next },
455
+ });
456
+ printSuccess('Password changed. Existing sessions elsewhere may need re-authentication.');
457
+ });
458
+ }