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
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `quox router` : read-only MikroTik RouterOS views via the NETWATCH tools (ROUTEROS-WIRE).
3
+ *
4
+ * quox router health CPU, memory, temperature, uptime
5
+ * quox router leases DHCP leases
6
+ * quox router pools DHCP pool usage
7
+ * quox router conflicts IP address conflicts (ARP vs DHCP leases)
8
+ * quox router free-ip [--count n] [--pool name] spare addresses for static use (reserves nothing)
9
+ * quox router vpn VPN tunnel health (WireGuard + IPsec)
10
+ * quox router wireguard WireGuard peers and whether they are connected
11
+ * quox router inventory routers and the devices connected to them
12
+ * quox router trust --show the router's current TLS fingerprint (sends NO credentials; pins nothing)
13
+ *
14
+ * Every verb is one tool call through POST /api/v1/tools/execute. No verb writes to the router.
15
+ * Engine: lib/core/router.js.
16
+ */
17
+ import { resolveOrgId } from './helpers.js';
18
+ import { ROUTER_TOOLS, ROUTER_FORMATTERS, makeRouterExec, formatTrust } from '../core/router.js';
19
+ import { resolveEndpoint } from '../api/endpoints.js';
20
+
21
+ const DESCRIPTIONS = {
22
+ health: 'Router CPU, memory, temperature and uptime (read only)',
23
+ leases: 'DHCP leases: which devices hold an address (read only)',
24
+ pools: 'DHCP pool usage: size, used, free (read only)',
25
+ conflicts: 'IP address conflicts seen by the router, ARP vs DHCP leases (read only)',
26
+ 'free-ip': 'Spare pool addresses for static assignment; reserves nothing (read only)',
27
+ vpn: 'VPN tunnel health, WireGuard and IPsec (read only)',
28
+ wireguard: 'WireGuard peers and whether each is connected (read only)',
29
+ inventory: 'MikroTik routers and the devices connected to them (read only)',
30
+ };
31
+
32
+ export function registerRouterCommands(program, ctx) {
33
+ const { client, auth } = ctx;
34
+ const json = () => program.opts().output === 'json';
35
+
36
+ const router = program
37
+ .command('router')
38
+ .description('MikroTik RouterOS read-only views via NETWATCH (health, DHCP, VPN, inventory)');
39
+
40
+ for (const verb of Object.keys(ROUTER_TOOLS)) {
41
+ const cmd = router.command(verb).description(DESCRIPTIONS[verb]);
42
+ if (verb === 'free-ip') {
43
+ cmd.option('--count <n>', 'how many addresses (1-20)', '3').option('--pool <name>', 'only this DHCP pool');
44
+ }
45
+ cmd.action(async (opts = {}) => {
46
+ auth.requireAuth();
47
+ const exec = makeRouterExec({ client, orgId: resolveOrgId(auth, program) });
48
+ const input = {};
49
+ if (verb === 'free-ip') {
50
+ const n = Number.parseInt(opts.count, 10);
51
+ if (!Number.isInteger(n) || n < 1 || n > 20) throw new Error('--count must be a whole number from 1 to 20');
52
+ input.count = n;
53
+ if (opts.pool) input.pool = String(opts.pool);
54
+ }
55
+ const p = await exec(ROUTER_TOOLS[verb], input);
56
+ if (json()) console.log(JSON.stringify(p, null, 2));
57
+ else console.log(ROUTER_FORMATTERS[verb](p).join('\n'));
58
+ });
59
+ }
60
+
61
+ // quoxai/quox#1236: the operator reads the endpoint's fingerprint here (no credentials sent),
62
+ // compares it out of band, then sets MIKROTIK_TLS_PIN themselves. This verb never pins.
63
+ router
64
+ .command('trust')
65
+ .description("Show the router's current TLS fingerprint, sending no credentials (never pins; you set MIKROTIK_TLS_PIN yourself)")
66
+ .option('--show', "print the fingerprint the router's HTTPS endpoint presents now")
67
+ .action(async (opts = {}) => {
68
+ auth.requireAuth();
69
+ if (!opts.show) {
70
+ throw new Error('quox router trust only has --show. It never pins anything: compare the fingerprint out of band, then set MIKROTIK_TLS_PIN yourself.');
71
+ }
72
+ const ep = resolveEndpoint('router.tlsFingerprint');
73
+ const data = await client.get(ep.path, { service: ep.service, query: { org_id: resolveOrgId(auth, program) } });
74
+ if (json()) { console.log(JSON.stringify(data, null, 2)); return; }
75
+ console.log(formatTrust(data).join('\n'));
76
+ });
77
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * ROWS (owner 2026-10-17): `quox rows`, the CLI for the live lines above the quox window's status bar.
3
+ * CLI first: everything /row does in the window works here, from a shell or a script.
4
+ *
5
+ * quox rows list [--now] what you have, where each one lives, what it shows now
6
+ * quox rows add <id> --url|--quox|--plugin|--cmd one row (no --template = the status-row contract)
7
+ * quox rows edit <id> [...] change any field; --contract drops the template
8
+ * quox rows remove <id> (alias rm)
9
+ * quox rows test [id] | test --url ... fetch once and draw it here, nothing saved for an ad hoc test
10
+ * quox rows share [id...] a bundle others can import (command rows never leave)
11
+ * quox rows import <file|-> url, quox and plugin rows; command rows need --trust-commands
12
+ * AND a person answering y to each exact command
13
+ * quox rows contract the JSON shape a row endpoint returns
14
+ *
15
+ * Storage is the same as the window's (quoxon/rowsStore.js): synced rows in the user's server-side preferences,
16
+ * command rows in a 0600 local file. An open window picks changes up within a minute, or at once with /row reload.
17
+ */
18
+ import chalk from 'chalk';
19
+ import { readFileSync } from 'node:fs';
20
+ import { createInterface } from 'node:readline/promises';
21
+ import { resolveEndpoint } from '../api/endpoints.js';
22
+ import { resolveOrgId } from './helpers.js';
23
+ import {
24
+ checkRow, secretInRow, syncable, createRowEngine, runShell, rowParts, clipParts, labelWidth, MAX_ROWS,
25
+ shareBundle, readBundle, cleanText, STATUS_ROW_TONES, themeRole, ROLES, fieldHints, dataFrom
26
+ } from '../quoxon/rows.js';
27
+ import { createRowsStore } from '../quoxon/rowsStore.js';
28
+ import { c as colourOf } from '../quoxon/theme.js';
29
+
30
+ export const CONTRACT = {
31
+ kind: 'quox.status-row',
32
+ version: 1,
33
+ fields: {
34
+ text: 'required (unless value is given). One line, 160 characters at most.',
35
+ label: 'optional. Shown first, 16 characters. The row\'s own label wins.',
36
+ value: 'optional. A number (shown compactly: 1.2k) or a short string (24), drawn bold before the text.',
37
+ tone: `optional. One of ${STATUS_ROW_TONES.join(', ')}. Colours the text and adds ✔ ⚠ ✖ ◆.`,
38
+ trend: 'optional. up, down, flat, or a number (the change: 3 shows ▲3, -2 shows ▼2).',
39
+ link: 'optional. An http(s) URL to look at for more; shown by quox rows test and /row test.'
40
+ },
41
+ example: { label: 'Approvals', value: 3, text: 'waiting for you', tone: 'warn', trend: 1, link: 'https://quox.example/approvals' },
42
+ rules: [
43
+ 'Plain text output counts as {"text": "<the text>"}.',
44
+ 'Terminal escapes and control characters are removed before anything is drawn.',
45
+ 'A row definition never carries a secret: use a quox or plugin row (your session is used) or a local command that reads its own secret.',
46
+ 'Command rows run on the machine that holds them only. They are never synced, shared or imported without a person saying yes to the exact command.'
47
+ ]
48
+ };
49
+
50
+ /** A theme role -> a chalk painter in the window's own colours (hex, ansi256, or plain under NO_COLOR). No grey. */
51
+ export function tint(name) {
52
+ const v = colourOf(name);
53
+ if (!v) return chalk.reset;
54
+ const m = /^ansi256\((\d+)\)$/.exec(v);
55
+ if (m) return chalk.ansi256(Number(m[1]));
56
+ return /^#[0-9a-f]{6}$/i.test(v) ? chalk.hex(v) : chalk.reset;
57
+ }
58
+ const paint = (role, bold, t) => { const f = tint(themeRole(role)); return bold ? f.bold(t) : f(t); };
59
+ export const drawRow = (state, width = process.stdout.columns || 100, labelW = 0) =>
60
+ ' ' + clipParts(rowParts(state, { labelW }), Math.max(10, width - 2)).map(([t, role, bold]) => paint(role, bold, t)).join('');
61
+
62
+ const where = (r) => {
63
+ const s = r.source;
64
+ if (s.type === 'url') return s.url;
65
+ if (s.type === 'quox') return `${s.path}${s.service === 'collector' ? ' (collector)' : ''}`;
66
+ if (s.type === 'plugin') return `${s.plugin} ${s.path}`;
67
+ return s.cmd;
68
+ };
69
+
70
+ /** Options -> a source (or null when none of --url/--quox/--plugin/--cmd was given). */
71
+ export function sourceFromOptions(o) {
72
+ const given = ['url', 'quox', 'plugin', 'cmd'].filter((k) => o[k] !== undefined);
73
+ if (given.length > 1) return { error: `Give one of --url, --quox, --plugin or --cmd (you gave ${given.map((g) => `--${g}`).join(' and ')}).` };
74
+ if (!given.length) return { source: null };
75
+ const k = given[0];
76
+ if (k === 'url') return { source: { type: 'url', url: String(o.url) } };
77
+ if (k === 'cmd') return { source: { type: 'cmd', cmd: String(o.cmd) } };
78
+ if (k === 'quox') return { source: { type: 'quox', service: o.service || (String(o.quox).startsWith('/api/v1/') ? 'collector' : 'auth'), path: String(o.quox) } };
79
+ return { source: { type: 'plugin', plugin: String(o.plugin), path: o.path ? String(o.path) : '' } };
80
+ }
81
+
82
+ /**
83
+ * A plugin source with no --path -> the path its manifest's statusRows names. -> { source, label?, every? } | { error }.
84
+ * manifest.statusRows: [{ id, label, path, every?, description? }] (quox-plugin-sdk docs/status-rows.md).
85
+ */
86
+ export async function resolvePluginRow(client, orgId, source, rowId) {
87
+ if (source.path) return { source };
88
+ let manifest;
89
+ try {
90
+ const ep = resolveEndpoint('plugins.get', { orgId, pluginId: source.plugin });
91
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
92
+ manifest = d?.plugin?.manifest ?? d?.manifest;
93
+ if (typeof manifest === 'string') manifest = JSON.parse(manifest);
94
+ } catch (e) { return { error: `Could not read the ${source.plugin} plugin (${e?.message || 'failed'}). Is it installed in this org?` }; }
95
+ const rows = Array.isArray(manifest?.statusRows) ? manifest.statusRows : [];
96
+ if (!rows.length) return { error: `The ${source.plugin} plugin ships no status rows (its manifest has no statusRows). Give --path to read one of its routes.` };
97
+ const pick = rowId ? rows.find((r) => r?.id === rowId) : rows[0];
98
+ if (!pick) return { error: `${source.plugin} has no status row "${rowId}". It has: ${rows.map((r) => r?.id).filter(Boolean).join(', ')}.` };
99
+ return { source: { ...source, path: String(pick.path || '') }, label: typeof pick.label === 'string' ? pick.label : undefined, every: typeof pick.every === 'string' ? pick.every : undefined };
100
+ }
101
+
102
+ export function registerRowsCommands(program, ctx) {
103
+ const { client, auth, config } = ctx;
104
+ const store = createRowsStore({ client, config, orgId: () => { try { return auth.getOrgId(program.opts().org); } catch { return null; } } });
105
+ const json = () => program.opts().output === 'json';
106
+ const fail = (msg, code = 1) => { console.error(tint('error')(msg)); process.exitCode = code; };
107
+ const say = (msg) => console.log(msg);
108
+
109
+ async function loadAll() {
110
+ const r = await store.rowPrefs();
111
+ return { synced: r.rows, cmd: store.localCmdRows(), offline: r.source !== 'server' ? (r.error || 'server unreachable') : null };
112
+ }
113
+ /** Write the synced list; -> true when the server took it. */
114
+ async function saveSynced(rows) {
115
+ const r = await store.setRowPrefs(rows);
116
+ if (!r.synced) fail(`Saved on this machine only: ${r.error || 'the server did not take it'}. Run the command again to retry.`);
117
+ return r.synced;
118
+ }
119
+ const engine = () => createRowEngine({ fetchUrl: (u) => store.rowFetch(u), fetchQuox: (s) => store.rowFetchQuox(s), runCmd: runShell });
120
+
121
+ /** Fetch once and print raw value, notes and the drawn row. -> the probe result. */
122
+ async function showOnce(row) {
123
+ const p = await engine().probe(row);
124
+ const raw = String(p.raw || '');
125
+ if (raw) {
126
+ say(tint('label')('Raw value:'));
127
+ const lines = raw.split('\n').filter((l) => l.trim()).slice(0, 6);
128
+ for (const l of lines) say(' ' + cleanText(l, (process.stdout.columns || 100) - 4));
129
+ if (raw.length > 600 || lines.length >= 6) say(' …');
130
+ }
131
+ if (!p.ok) {
132
+ fail(`⚠ ${row.label || row.id}: ${p.error}`);
133
+ const hints = raw ? fieldHints(dataFrom(raw)) : [];
134
+ if (hints.length && !row.template) {
135
+ say(tint('label')('It is JSON but not a status row. A template can read it; fields you can use:'));
136
+ say(' ' + hints.map((h) => tint('command')(`{{${h}}}`)).join(' '));
137
+ say(tint('label')(` e.g. --template '{{${hints.find((h) => !h.startsWith('count(')) || hints[0]}}} …'`));
138
+ }
139
+ return p;
140
+ }
141
+ for (const w of p.warnings || []) say(tint('warn')(`Note: ${w}`));
142
+ say(tint('label')(`Shape: ${row.template ? 'template' : 'status-row contract'}${p.seg?.link ? ` · link ${p.seg.link}` : ''}`));
143
+ say(tint('label')('The row:'));
144
+ say(drawRow({ row, status: 'ok', text: p.text, seg: p.seg, at: Date.now() }));
145
+ return p;
146
+ }
147
+
148
+ /** Build a row from add/edit options on top of `base`. -> { row } | { error }. */
149
+ async function buildRow(id, o, base = null) {
150
+ const s = sourceFromOptions(o);
151
+ if (s.error) return { error: s.error };
152
+ let source = s.source || (base ? { ...base.source } : null);
153
+ if (!source) return { error: 'Say where the row reads from: --url <url>, --quox <path>, --plugin <id>, or --cmd "<command>".' };
154
+ let label = o.label ?? base?.label;
155
+ let every = o.every ?? base?.source?.every; // a new source keeps the row's interval
156
+ if (source.type === 'plugin' && !source.path) {
157
+ auth.requireAuth();
158
+ const r = await resolvePluginRow(client, resolveOrgId(auth, program), source, o.row);
159
+ if (r.error) return { error: r.error };
160
+ source = r.source; label = label ?? r.label; every = every ?? r.every;
161
+ }
162
+ if (every) source.every = every;
163
+ const draft = { id, source };
164
+ if (label) draft.label = label;
165
+ const colour = o.colour ?? o.color ?? base?.colour;
166
+ if (colour) draft.colour = colour;
167
+ const template = o.contract ? '' : (o.template ?? base?.template ?? '');
168
+ if (template) draft.template = template;
169
+ const c = checkRow(draft);
170
+ if (c.error) return { error: c.error };
171
+ const secret = secretInRow(c.row);
172
+ if (secret) return { error: secret };
173
+ return { row: c.row };
174
+ }
175
+
176
+ async function put(row, { replace = null } = {}) {
177
+ const { synced, cmd } = await loadAll();
178
+ const all = [...synced, ...cmd];
179
+ const prev = replace ? all.find((r) => r.id === replace) : all.find((r) => r.id.toLowerCase() === row.id.toLowerCase());
180
+ if (!prev && all.length >= MAX_ROWS) return fail(`You have ${MAX_ROWS} rows already (the most the window has room for). quox rows remove <id> makes space.`);
181
+ const syncedNext = synced.filter((r) => r !== prev);
182
+ const cmdNext = cmd.filter((r) => r !== prev);
183
+ if (syncable(row)) syncedNext.push(row); else cmdNext.push(row);
184
+ if (prev && !syncable(prev) || !syncable(row)) if (!store.setLocalCmdRows(cmdNext)) return fail('Could not write the local rows file.');
185
+ if (prev && syncable(prev) || syncable(row)) if (!(await saveSynced(syncedNext))) return undefined;
186
+ if (json()) return say(JSON.stringify({ ok: true, row, where: syncable(row) ? 'synced' : 'this machine' }, null, 2));
187
+ say(tint('ok')(`✔ Row ${row.id} ${prev ? 'saved' : 'added'}. ${syncable(row) ? 'It follows your account to your other machines.' : 'It runs on THIS machine only and is never synced.'}`));
188
+ say(tint('label')(` Open windows show it within a minute (/row reload for now). quox rows test ${row.id} draws it here.`));
189
+ return undefined;
190
+ }
191
+
192
+ const rows = program.command('rows').description('Your live lines above the quox window\'s status bar: add, edit, remove, test, share, import (url, quox, plugin or local command rows)');
193
+
194
+ const sourceOpts = (cmd) => cmd
195
+ .option('--url <url>', 'read JSON or text from a URL (no credentials are sent to other hosts)')
196
+ .option('--quox <path>', 'read a path on your own Quox with your session, e.g. /api/orgs/{org}/inbox/stats')
197
+ .option('--service <name>', 'for --quox: auth (default) or collector')
198
+ .option('--plugin <id>', 'read a status row a plugin ships (its manifest statusRows)')
199
+ .option('--row <rowId>', 'for --plugin: which of its status rows (default: the first)')
200
+ .option('--path <path>', 'for --plugin: a route of the plugin backend instead of a declared row')
201
+ .option('--cmd <command>', 'run a local command (this machine only, never synced)')
202
+ .option('--template <template>', 'fill {{path}} from the data; leave out to read the status-row contract')
203
+ .option('--every <interval>', 'refresh interval: 30s, 5m, 1h (5 s to 1 day)')
204
+ .option('--label <text>', 'a short name drawn first')
205
+ .option('--colour <role>', `text colour: ${ROLES.join(', ')}`);
206
+
207
+ rows.command('list').alias('ls')
208
+ .description('List your rows: where each reads from, where it is stored, and (--now) what it shows now')
209
+ .option('--now', 'fetch each row once and draw it')
210
+ .action(async (o) => {
211
+ auth.requireAuth();
212
+ const { synced, cmd, offline } = await loadAll();
213
+ const all = [...synced, ...cmd];
214
+ if (offline) console.error(tint('warn')(`⚠ Server not reachable (${offline}); showing this machine's copy.`));
215
+ const states = [];
216
+ if (o.now) {
217
+ const e = engine();
218
+ for (const r of all) {
219
+ const p = await e.probe(r);
220
+ states.push(p.ok ? { row: r, status: 'ok', text: p.text, seg: p.seg, at: Date.now() } : { row: r, status: 'error', error: p.error, text: '', at: null });
221
+ }
222
+ }
223
+ if (json()) return say(JSON.stringify({ rows: all.map((r, i) => ({ ...r, stored: syncable(r) ? 'synced' : 'this machine', shape: r.template ? 'template' : 'contract', ...(states[i] ? { now: states[i].status === 'ok' ? states[i].text : null, error: states[i].error || undefined } : {}) })), max: MAX_ROWS }, null, 2));
224
+ if (!all.length) return say(`No rows yet. Try: quox rows add approvals --quox '/api/orgs/{org}/inbox/stats' --template '{{pending_approvals}} waiting' --label Approvals\n(or /row new in the quox window).`);
225
+ say(tint('heading').bold(`Rows (${all.length} of ${MAX_ROWS})`));
226
+ const lw = labelWidth(all.map((row) => ({ row })));
227
+ for (const [i, r] of all.entries()) {
228
+ say(` ${tint('id').bold(r.id.padEnd(12))} ${tint('label')(r.source.type.padEnd(6))} ${tint('text')(where(r))}`);
229
+ say(tint('label')(` ${''.padEnd(12)} every ${r.source.every} · ${syncable(r) ? 'synced to your account' : 'this machine only'} · ${r.template ? `template ${r.template}` : 'status-row contract'}${r.label ? ` · label ${r.label}` : ''}`));
230
+ if (states[i]) say(' ' + drawRow(states[i], (process.stdout.columns || 100) - 3, lw));
231
+ }
232
+ return undefined;
233
+ });
234
+
235
+ sourceOpts(rows.command('add <id>').description('Add a row. No --template = it reads the status-row contract (quox rows contract)')).option('--test', 'fetch it once and draw it after saving')
236
+ .action(async (id, o) => {
237
+ auth.requireAuth();
238
+ const b = await buildRow(id, o);
239
+ if (b.error) return fail(b.error);
240
+ const { synced, cmd } = await loadAll();
241
+ if ([...synced, ...cmd].some((r) => r.id.toLowerCase() === b.row.id.toLowerCase())) return fail(`A row called ${b.row.id} exists. quox rows edit ${b.row.id} changes it.`);
242
+ await put(b.row);
243
+ if (o.test && !process.exitCode) await showOnce(b.row);
244
+ return undefined;
245
+ });
246
+
247
+ sourceOpts(rows.command('edit <id>').description('Change a row: any of the add options; --contract drops the template')).option('--contract', 'read the status-row contract instead of a template')
248
+ .action(async (id, o) => {
249
+ auth.requireAuth();
250
+ const { synced, cmd } = await loadAll();
251
+ const cur = [...synced, ...cmd].find((r) => r.id.toLowerCase() === String(id).toLowerCase());
252
+ if (!cur) return fail(`No row "${id}". quox rows list shows them.`);
253
+ const b = await buildRow(cur.id, o, cur);
254
+ if (b.error) return fail(b.error);
255
+ if (JSON.stringify(b.row) === JSON.stringify(cur)) return say(`Nothing changed on ${cur.id}.`);
256
+ return put(b.row, { replace: cur.id });
257
+ });
258
+
259
+ rows.command('remove <id>').alias('rm').description('Remove a row')
260
+ .action(async (id) => {
261
+ auth.requireAuth();
262
+ const { synced, cmd } = await loadAll();
263
+ const cur = [...synced, ...cmd].find((r) => r.id.toLowerCase() === String(id).toLowerCase());
264
+ if (!cur) return fail(`No row "${id}". quox rows list shows them.`);
265
+ if (syncable(cur)) { if (!(await saveSynced(synced.filter((r) => r !== cur)))) return undefined; }
266
+ else if (!store.setLocalCmdRows(cmd.filter((r) => r !== cur))) return fail('Could not write the local rows file.');
267
+ return json() ? say(JSON.stringify({ ok: true, removed: cur.id })) : say(tint('ok')(`✔ Removed row ${cur.id}.`));
268
+ });
269
+
270
+ sourceOpts(rows.command('test [id]').description('Fetch a row once and draw it here. With --url/--quox/--plugin/--cmd instead of an id, try one before adding it (nothing is saved)'))
271
+ .action(async (id, o) => {
272
+ auth.requireAuth();
273
+ let row;
274
+ if (id) {
275
+ const { synced, cmd } = await loadAll();
276
+ row = [...synced, ...cmd].find((r) => r.id.toLowerCase() === String(id).toLowerCase());
277
+ if (!row) return fail(`No row "${id}". quox rows list shows them.`);
278
+ } else {
279
+ const b = await buildRow('test', o);
280
+ if (b.error) return fail(b.error);
281
+ row = b.row;
282
+ }
283
+ const p = await showOnce(row);
284
+ if (json()) say(JSON.stringify({ ok: p.ok, text: p.text ?? null, seg: p.seg ?? null, warnings: p.warnings ?? [], error: p.error ?? null }, null, 2));
285
+ return undefined;
286
+ });
287
+
288
+ rows.command('share [ids...]').description('Print a bundle of your rows for someone else to import. Command rows never leave this machine')
289
+ .action(async (ids) => {
290
+ auth.requireAuth();
291
+ const { synced, cmd } = await loadAll();
292
+ const all = [...synced, ...cmd];
293
+ const want = ids?.length ? ids.map((x) => String(x).toLowerCase()) : null;
294
+ const missing = want ? want.filter((x) => !all.some((r) => r.id.toLowerCase() === x)) : [];
295
+ if (missing.length) return fail(`No row ${missing.join(', ')}. quox rows list shows them.`);
296
+ const picked = want ? all.filter((r) => want.includes(r.id.toLowerCase())) : all;
297
+ const { bundle, skipped, refused } = shareBundle(picked);
298
+ for (const s of skipped) console.error(tint('warn')(`Not shared: ${s} is a command row (it runs a shell on this machine, so it never leaves it).`));
299
+ for (const r of refused) console.error(tint('warn')(`Not shared: ${r.id}: ${r.why}`));
300
+ if (!bundle.rows.length) return fail('Nothing to share.');
301
+ return say(JSON.stringify(bundle, null, 2));
302
+ });
303
+
304
+ rows.command('import <file>').description('Add rows from a share bundle (a file, or - for stdin). Command rows are never added without --trust-commands and a y for each exact command')
305
+ .option('--trust-commands', 'offer each command row for a yes/no, one by one (needs a person at the terminal)')
306
+ .option('--replace', 'replace rows that have the same id')
307
+ .action(async (file, o) => {
308
+ auth.requireAuth();
309
+ let text;
310
+ try { text = file === '-' ? readFileSync(0, 'utf8') : readFileSync(file, 'utf8'); } catch (e) { return fail(`Could not read ${file}: ${e.message}`); }
311
+ const b = readBundle(text);
312
+ for (const e of b.errors) console.error(tint('warn')(`Skipped ${e}`));
313
+ const take = [...b.rows];
314
+ if (b.commands.length) {
315
+ if (!o.trustCommands) {
316
+ for (const r of b.commands) console.error(tint('warn')(`Not added: ${r.id} would run \`${cleanText(r.source.cmd, 200)}\` on this machine. Command rows from a bundle need --trust-commands and your yes.`));
317
+ } else if (!process.stdin.isTTY || file === '-') {
318
+ return fail('A command row from a bundle needs a person at the terminal to say yes to it. Nothing was imported.');
319
+ } else {
320
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
321
+ try {
322
+ for (const r of b.commands) {
323
+ say(tint('warn').bold(`⚠ ${r.id} runs this on THIS machine every ${r.source.every}:`));
324
+ say(' ' + tint('command')(cleanText(r.source.cmd, 500)));
325
+ const a = (await rl.question(' Add it? y/N ')).trim();
326
+ if (/^y(es)?$/i.test(a)) take.push(r); else say(' Skipped.');
327
+ }
328
+ } finally { rl.close(); }
329
+ }
330
+ }
331
+ if (!take.length) return fail('Nothing to import.');
332
+ const { synced, cmd } = await loadAll();
333
+ const has = (id) => [...synced, ...cmd].find((r) => r.id.toLowerCase() === id.toLowerCase());
334
+ const clash = take.filter((r) => has(r.id));
335
+ if (clash.length && !o.replace) return fail(`You already have ${clash.map((r) => r.id).join(', ')}. --replace swaps them; nothing was imported.`);
336
+ const syncedNext = synced.filter((r) => !take.some((t) => t.id.toLowerCase() === r.id.toLowerCase()));
337
+ const cmdNext = cmd.filter((r) => !take.some((t) => t.id.toLowerCase() === r.id.toLowerCase()));
338
+ for (const r of take) (syncable(r) ? syncedNext : cmdNext).push(r);
339
+ if (syncedNext.length + cmdNext.length > MAX_ROWS) return fail(`That would make ${syncedNext.length + cmdNext.length} rows; the window has room for ${MAX_ROWS}. Remove some first. Nothing was imported.`);
340
+ if (take.some((r) => !syncable(r)) || cmdNext.length !== cmd.length) if (!store.setLocalCmdRows(cmdNext)) return fail('Could not write the local rows file.');
341
+ if (!(await saveSynced(syncedNext))) return undefined;
342
+ return say(tint('ok')(`✔ Imported ${take.map((r) => r.id).join(', ')}.`));
343
+ });
344
+
345
+ rows.command('contract').description('The JSON a row endpoint returns (the status-row contract), with an example')
346
+ .action(() => {
347
+ if (json()) return say(JSON.stringify(CONTRACT, null, 2));
348
+ say(tint('heading').bold('Status-row contract v1'));
349
+ for (const [k, v] of Object.entries(CONTRACT.fields)) say(` ${tint('id').bold(k.padEnd(6))} ${v}`);
350
+ say(tint('label')('Example:'));
351
+ say(' ' + JSON.stringify(CONTRACT.example));
352
+ say(tint('label')('Draws as:'));
353
+ const ex = { row: { id: 'approvals', source: {} }, status: 'ok', at: Date.now(), seg: { label: 'Approvals', value: '3', text: 'waiting for you', tone: 'warn', trend: '▲1' }, text: '3 waiting for you ▲1' };
354
+ say(' ' + drawRow(ex));
355
+ for (const r of CONTRACT.rules) say(tint('label')(` · ${r}`));
356
+ return undefined;
357
+ });
358
+ }
@@ -31,9 +31,14 @@ export function registerRunCommands(program, ctx) {
31
31
 
32
32
  let rows, source;
33
33
  try {
34
+ // Journey #3 (2026-10-19): the engine path sent no query, so --workflow/--status/--limit were ignored and a
35
+ // filter for one workflow listed every workflow's runs. quoxflow's /api/executions takes all three.
34
36
  const ep = resolveEndpoint('executions.list');
35
- const data = await client.get(ep.path, { service: ep.service });
36
- const items = data.data || data.items || (Array.isArray(data) ? data : []);
37
+ const engineQuery = { ...(options.workflow ? { workflowId: options.workflow } : {}), ...(options.status ? { status: options.status } : {}), limit: options.limit };
38
+ const data = await client.get(ep.path, { service: ep.service, query: engineQuery });
39
+ let items = data.data || data.items || (Array.isArray(data) ? data : []);
40
+ // an older engine that ignores the filter must still never show another workflow's runs
41
+ if (options.workflow) items = items.filter((it) => (it.workflowId ?? it.workflow_id) === options.workflow);
37
42
  rows = items.map((item) => ({ ...item, source: 'engine' }));
38
43
  source = 'engine';
39
44
  } catch (err) {
@@ -93,12 +98,35 @@ export function registerRunCommands(program, ctx) {
93
98
  .description('Start a new workflow run')
94
99
  .requiredOption('--workflow <id>', 'workflow ID')
95
100
  .option('--input <json>', 'input data as JSON')
101
+ .option('--wait', 'engine workflows: wait for the run to finish (up to 10 minutes) instead of returning its id at once')
96
102
  .action(async (options) => {
97
103
  auth.requireAuth();
98
104
  const orgId = resolveOrgId(auth, program);
99
105
  const body = { workflow_id: options.workflow };
100
106
  if (options.input) body.context = parseJSON(options.input, '--input');
101
- const data = await client.post(resolveEndpoint('runs.create', { orgId }).path, { service: resolveEndpoint('runs.create', { orgId }).service, body });
107
+ let data;
108
+ try {
109
+ data = await client.post(resolveEndpoint('runs.create', { orgId }).path, { service: resolveEndpoint('runs.create', { orgId }).service, body });
110
+ } catch (err) {
111
+ if (err.status !== 404) throw err;
112
+ // #997: the auth runs model only knows auth-store workflows; an engine workflow runs on the engine
113
+ // (same fallback as `workflow run`, quoxflow#7).
114
+ const getEp = resolveEndpoint('workflows.applyGet', { workflowId: options.workflow });
115
+ try { await client.get(getEp.path, { service: getEp.service }); } catch (engineErr) { if (engineErr.status === 404) throw err; throw engineErr; }
116
+ const exEp = resolveEndpoint('workflows.applyExecute', { workflowId: options.workflow });
117
+ // #1029: start async by default and return the id at once; the engine ran synchronously before, so any
118
+ // run past the 30 s client timeout printed 'timed out' while it kept going. --wait keeps the old shape.
119
+ const ran = await client.post(exEp.path, {
120
+ service: exEp.service,
121
+ body: { triggerData: body.context || {}, async: !options.wait },
122
+ ...(options.wait ? { timeout: 600000 } : {}),
123
+ });
124
+ const runId = ran.executionId || ran.execution_id || ran.id || ran.execution?.id || ran.data?.executionId || 'ok';
125
+ const st = ran.status || ran.execution?.status || ran.data?.status;
126
+ printSuccess(`Run started (engine): ${runId}${st ? ` [${st}]` : ''}`);
127
+ if (!options.wait && runId !== 'ok') console.log(` Follow it: quox run get ${runId}`);
128
+ return;
129
+ }
102
130
  printSuccess(`Run started: ${data.run?.id || data.id || 'ok'}`);
103
131
  });
104
132
 
@@ -112,8 +140,12 @@ export function registerRunCommands(program, ctx) {
112
140
  const orgId = resolveOrgId(auth, program);
113
141
  const body = {};
114
142
  if (options.reason) body.reason = options.reason;
115
- await client.post(resolveEndpoint('runs.cancel', { orgId, runId }).path, { service: resolveEndpoint('runs.cancel', { orgId, runId }).service, body });
116
- printSuccess('Run cancelled.');
143
+ // #997: engine first (the runs `run list` shows), auth-store fallback on a 404 only
144
+ const source = await engineFirst(
145
+ () => { const ep = resolveEndpoint('executions.cancel', { executionId: runId }); return client.post(ep.path, { service: ep.service, body }); },
146
+ () => client.post(resolveEndpoint('runs.cancel', { orgId, runId }).path, { service: resolveEndpoint('runs.cancel', { orgId, runId }).service, body }),
147
+ );
148
+ printSuccess(`Run cancelled (${source}).`);
117
149
  });
118
150
 
119
151
  run
@@ -121,12 +153,35 @@ export function registerRunCommands(program, ctx) {
121
153
  .description('Resume a paused run')
122
154
  .argument('<runId>', 'run ID')
123
155
  .option('--response <json>', 'human response payload as JSON')
156
+ .option('--wait', 'engine runs: wait for the resumed run to finish (up to 10 minutes) instead of answering at once')
124
157
  .action(async (runId, options) => {
125
158
  auth.requireAuth();
126
159
  const orgId = resolveOrgId(auth, program);
127
- const body = {};
128
- if (options.response) body.response = parseJSON(options.response, '--response');
129
- await client.post(resolveEndpoint('runs.resume', { orgId, runId }).path, { service: resolveEndpoint('runs.resume', { orgId, runId }).service, body });
130
- printSuccess('Run resumed.');
160
+ const response = options.response ? parseJSON(options.response, '--response') : undefined;
161
+ const body = response === undefined ? {} : { response };
162
+ // #997: the engine's resume body IS the decision ({approved, approver, reason?}; approver must be you);
163
+ // the auth model wraps it as { response }.
164
+ const source = await engineFirst(
165
+ // #1037: async by default; the engine checks the approver synchronously, then runs the rest in the background
166
+ () => {
167
+ const ep = resolveEndpoint('executions.resume', { executionId: runId });
168
+ return client.post(ep.path, {
169
+ service: ep.service,
170
+ body: { ...(response || {}), async: !options.wait },
171
+ ...(options.wait ? { timeout: 600000 } : {}),
172
+ });
173
+ },
174
+ () => client.post(resolveEndpoint('runs.resume', { orgId, runId }).path, { service: resolveEndpoint('runs.resume', { orgId, runId }).service, body }),
175
+ );
176
+ printSuccess(`Run resumed (${source}).`);
177
+ if (source === 'engine' && !options.wait) console.log(` Follow it: quox run get ${runId}`);
131
178
  });
132
179
  }
180
+
181
+ // #997: try the engine (quoxflow) store, fall back to the auth-service store only when the engine says 404.
182
+ // Any other engine answer (409 already terminal, 400 not paused) is the real outcome and is thrown.
183
+ async function engineFirst(engineCall, authCall) {
184
+ try { await engineCall(); return 'engine'; } catch (err) { if (err.status !== 404) throw err; }
185
+ await authCall();
186
+ return 'auth';
187
+ }