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,296 @@
1
+ /**
2
+ * Powers commands: MCP endpoint + hosted-repo tool deploys (LIFECYCLE-P1..P3)
3
+ *
4
+ * quox powers detect|deploy|deploy-repo|status|finalize|update|archive|restore|config|delete
5
+ *
6
+ * Backend: services/auth/routes/powers.js, mounted org-scoped at
7
+ * `/api/orgs/:orgId/powers/...`. Two deploy flows:
8
+ * - `deploy` (mcp_endpoint): probes a live MCP URL and registers it as a
9
+ * governed agent in one call. Requires `persona` (agent_id + name) upfront.
10
+ * - `deploy-repo` (mcp_hosted): kicks off an async container build from a
11
+ * GitHub repo (202 Accepted). Poll `status` until the build reaches a
12
+ * terminal state (`built`/`failed`), then call `finalize` (persona
13
+ * required there, NOT at deploy-repo time) to activate the agent.
14
+ */
15
+
16
+ import { formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
17
+ import { resolveOrgId } from './helpers.js';
18
+ import { resolveEndpoint } from '../api/endpoints.js';
19
+
20
+ const TERMINAL_BUILD_STATES = new Set(['built', 'failed']);
21
+
22
+ function sleep(ms) {
23
+ return new Promise(resolve => setTimeout(resolve, ms));
24
+ }
25
+
26
+ /** Commander collector for repeatable --env key=value flags -> [{ key, value }]. */
27
+ function collectEnvVar(input, previous) {
28
+ const idx = input.indexOf('=');
29
+ if (idx === -1) {
30
+ throw new Error(`Invalid --env value "${input}". Expected key=value.`);
31
+ }
32
+ const key = input.slice(0, idx).trim();
33
+ const value = input.slice(idx + 1);
34
+ if (!key) {
35
+ throw new Error(`Invalid --env value "${input}". Key cannot be empty.`);
36
+ }
37
+ return [...previous, { key, value }];
38
+ }
39
+
40
+ function buildPersona(options, { requireCore = false } = {}) {
41
+ const persona = {};
42
+ if (options.agentId) persona.agent_id = options.agentId;
43
+ if (options.name) persona.name = options.name;
44
+ if (options.description !== undefined) persona.description = options.description;
45
+ if (options.icon) persona.icon = options.icon;
46
+ if (options.systemPromptAdditions) persona.system_prompt_additions = options.systemPromptAdditions;
47
+ if (requireCore && (!persona.agent_id || !persona.name)) {
48
+ throw new Error('--agent-id and --name are required.');
49
+ }
50
+ return persona;
51
+ }
52
+
53
+ async function confirmOrSkip(message) {
54
+ const { default: inquirer } = await import('inquirer');
55
+ const ans = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message, default: false }]);
56
+ return ans.confirm;
57
+ }
58
+
59
+ export function registerPowersCommands(program, ctx) {
60
+ const { client, auth } = ctx;
61
+
62
+ const powers = program
63
+ .command('powers')
64
+ .description('Deploy and manage POWERS (MCP endpoint / hosted-repo tools)');
65
+
66
+ // ── powers detect ───────────────────────────────────────────────────────────
67
+
68
+ powers
69
+ .command('detect')
70
+ .description('Probe an MCP URL to see what kind of server it is and what tools it exposes')
71
+ .requiredOption('--url <url>', 'MCP server URL')
72
+ .option('--auth-kind <kind>', 'none|bearer|header', 'none')
73
+ .option('--auth-value <value>', 'auth token/header value (required if --auth-kind is bearer or header)')
74
+ .action(async (options) => {
75
+ auth.requireAuth();
76
+ const orgId = resolveOrgId(auth, program);
77
+ const body = { url: options.url, authKind: options.authKind };
78
+ if (options.authValue) body.authValue = options.authValue;
79
+ const ep = resolveEndpoint('powers.detect', { orgId });
80
+ const data = await client.post(ep.path, { service: ep.service, body });
81
+ formatDetail(data, { format: program.opts().output, title: 'Detection Result' });
82
+ });
83
+
84
+ // ── powers deploy (mcp_endpoint) ────────────────────────────────────────────
85
+
86
+ powers
87
+ .command('deploy')
88
+ .description('Deploy a live MCP endpoint as a governed agent (one-shot, no build)')
89
+ .requiredOption('--url <url>', 'MCP server URL')
90
+ .option('--auth-kind <kind>', 'none|bearer|header', 'none')
91
+ .option('--auth-value <value>', 'auth token/header value')
92
+ .requiredOption('--agent-id <id>', 'new agent id (lowercase letters/numbers/underscores, starts with a letter)')
93
+ .requiredOption('--name <name>', 'agent display name')
94
+ .option('--description <text>', 'agent description')
95
+ .option('--icon <icon>', 'agent icon')
96
+ .option('--system-prompt-additions <text>', 'extra system prompt text for the agent')
97
+ .action(async (options) => {
98
+ auth.requireAuth();
99
+ const orgId = resolveOrgId(auth, program);
100
+ const body = {
101
+ url: options.url,
102
+ authKind: options.authKind,
103
+ persona: buildPersona(options, { requireCore: true }),
104
+ };
105
+ if (options.authValue) body.authValue = options.authValue;
106
+ const ep = resolveEndpoint('powers.deploy', { orgId });
107
+ const data = await client.post(ep.path, { service: ep.service, body });
108
+ printSuccess(`Power deployed: agent "${data.agent_id}" (tool ${data.tool_id}).`);
109
+ formatDetail(data, { format: program.opts().output });
110
+ });
111
+
112
+ // ── powers deploy-repo (mcp_hosted) ─────────────────────────────────────────
113
+
114
+ powers
115
+ .command('deploy-repo')
116
+ .description('Deploy a public GitHub repo as a hosted MCP power (builds a container; async)')
117
+ .requiredOption('--repo-url <url>', 'public GitHub repo URL, e.g. https://github.com/owner/repo')
118
+ .option('--env <key=value>', 'env var to inject into the build (repeatable)', collectEnvVar, [])
119
+ .option('--agent-id <id>', 'reserve an agent id upfront (optional — can also be set at finalize)')
120
+ .option('--force-suffix <suffix>', 'suffix to allow deploying the same repo again as a duplicate')
121
+ .option('--no-poll', 'return immediately after kicking off the build instead of polling status')
122
+ .option('--poll-interval <ms>', 'polling interval in ms', '3000')
123
+ .option('--poll-timeout <ms>', 'max time to poll before giving up, in ms', '300000')
124
+ .action(async (options) => {
125
+ auth.requireAuth();
126
+ const orgId = resolveOrgId(auth, program);
127
+
128
+ const body = { repo_url: options.repoUrl, env_vars: options.env };
129
+ if (options.agentId) body.persona = { agent_id: options.agentId };
130
+ if (options.forceSuffix) body.force_suffix = options.forceSuffix;
131
+
132
+ const deployEp = resolveEndpoint('powers.deploy-repo', { orgId });
133
+ const data = await client.post(deployEp.path, { service: deployEp.service, body });
134
+ printSuccess(`Build started: tool_def_id=${data.tool_def_id} (${data.tool_id}), build_state=${data.build_state}.`);
135
+
136
+ if (options.poll === false) {
137
+ formatDetail(data, { format: program.opts().output });
138
+ printInfo(`Poll with: quox powers status ${data.tool_def_id}`);
139
+ return;
140
+ }
141
+
142
+ const toolDefId = data.tool_def_id;
143
+ const pollInterval = Number(options.pollInterval);
144
+ const pollTimeout = Number(options.pollTimeout);
145
+ const statusEp = resolveEndpoint('powers.deploy-repo.status', { orgId, toolDefId });
146
+ const start = Date.now();
147
+ let status = { build_state: data.build_state };
148
+
149
+ while (!TERMINAL_BUILD_STATES.has(status.build_state) && (Date.now() - start) < pollTimeout) {
150
+ await sleep(pollInterval);
151
+ status = await client.get(statusEp.path, { service: statusEp.service });
152
+ printInfo(`build_state=${status.build_state}`);
153
+ }
154
+
155
+ formatDetail(status, { format: program.opts().output, title: 'Build Status' });
156
+
157
+ if (status.build_state === 'built') {
158
+ printSuccess(`Build complete. Run: quox powers finalize ${toolDefId} --agent-id <id> --name <name>`);
159
+ } else if (status.build_state === 'failed') {
160
+ printWarning(`Build failed: ${status.build_failed_reason || 'unknown reason'}`);
161
+ } else {
162
+ printWarning(`Polling timed out before the build reached a terminal state. Check later: quox powers status ${toolDefId}`);
163
+ }
164
+ });
165
+
166
+ // ── powers status ───────────────────────────────────────────────────────────
167
+
168
+ powers
169
+ .command('status')
170
+ .description('Check a hosted-repo power\'s build status')
171
+ .argument('<toolDefId>', 'tool definition ID')
172
+ .action(async (toolDefId) => {
173
+ auth.requireAuth();
174
+ const orgId = resolveOrgId(auth, program);
175
+ const ep = resolveEndpoint('powers.deploy-repo.status', { orgId, toolDefId });
176
+ const data = await client.get(ep.path, { service: ep.service });
177
+ formatDetail(data, { format: program.opts().output, title: 'Build Status' });
178
+ });
179
+
180
+ // ── powers finalize ─────────────────────────────────────────────────────────
181
+
182
+ powers
183
+ .command('finalize')
184
+ .description('Activate the agent for a completed hosted-repo build')
185
+ .argument('<toolDefId>', 'tool definition ID')
186
+ .requiredOption('--agent-id <id>', 'new agent id (lowercase letters/numbers/underscores, starts with a letter)')
187
+ .requiredOption('--name <name>', 'agent display name')
188
+ .option('--description <text>', 'agent description')
189
+ .option('--icon <icon>', 'agent icon')
190
+ .option('--system-prompt-additions <text>', 'extra system prompt text for the agent')
191
+ .action(async (toolDefId, options) => {
192
+ auth.requireAuth();
193
+ const orgId = resolveOrgId(auth, program);
194
+ const body = { persona: buildPersona(options, { requireCore: true }) };
195
+ const ep = resolveEndpoint('powers.deploy-repo.finalize', { orgId, toolDefId });
196
+ const data = await client.post(ep.path, { service: ep.service, body });
197
+ printSuccess(`Power finalized: agent "${data.agent_id}" (tool ${data.tool_id}).`);
198
+ formatDetail(data, { format: program.opts().output });
199
+ });
200
+
201
+ // ── powers update (redeploy at a new ref) ──────────────────────────────────
202
+
203
+ powers
204
+ .command('update')
205
+ .description('Redeploy a hosted-repo power at a new git ref (branch/tag/commit)')
206
+ .argument('<toolDefId>', 'tool definition ID')
207
+ .requiredOption('--repo-ref <ref>', 'git ref to build, e.g. HEAD or a commit SHA')
208
+ .action(async (toolDefId, options) => {
209
+ auth.requireAuth();
210
+ const orgId = resolveOrgId(auth, program);
211
+ const ep = resolveEndpoint('powers.deploy-repo.update', { orgId, toolDefId });
212
+ const data = await client.post(ep.path, { service: ep.service, body: { repo_ref: options.repoRef } });
213
+ printSuccess(`Update build started for ${toolDefId} at ref "${data.repo_ref}" (build_state=${data.build_state}).`);
214
+ formatDetail(data, { format: program.opts().output });
215
+ });
216
+
217
+ // ── powers archive / restore ────────────────────────────────────────────────
218
+
219
+ powers
220
+ .command('archive')
221
+ .description('Archive a hosted-repo power (disables it without deleting any rows)')
222
+ .argument('<toolDefId>', 'tool definition ID')
223
+ .action(async (toolDefId) => {
224
+ auth.requireAuth();
225
+ const orgId = resolveOrgId(auth, program);
226
+ const ep = resolveEndpoint('powers.deploy-repo.archive', { orgId, toolDefId });
227
+ const data = await client.post(ep.path, { service: ep.service });
228
+ printSuccess('Power archived.');
229
+ formatDetail(data, { format: program.opts().output });
230
+ });
231
+
232
+ powers
233
+ .command('restore')
234
+ .description('Restore a previously archived hosted-repo power')
235
+ .argument('<toolDefId>', 'tool definition ID')
236
+ .action(async (toolDefId) => {
237
+ auth.requireAuth();
238
+ const orgId = resolveOrgId(auth, program);
239
+ const ep = resolveEndpoint('powers.deploy-repo.restore', { orgId, toolDefId });
240
+ const data = await client.post(ep.path, { service: ep.service });
241
+ printSuccess('Power restored.');
242
+ formatDetail(data, { format: program.opts().output });
243
+ });
244
+
245
+ // ── powers config ───────────────────────────────────────────────────────────
246
+
247
+ powers
248
+ .command('config')
249
+ .description('Update env vars and/or agent persona for a hosted-repo power without rebuilding')
250
+ .argument('<toolDefId>', 'tool definition ID')
251
+ .option('--env <key=value>', 'env var to set/rotate (repeatable)', collectEnvVar, [])
252
+ .option('--name <name>', 'new agent display name')
253
+ .option('--description <text>', 'new agent description')
254
+ .option('--icon <icon>', 'new agent icon')
255
+ .option('--system-prompt-additions <text>', 'new system prompt additions')
256
+ .action(async (toolDefId, options) => {
257
+ auth.requireAuth();
258
+ const orgId = resolveOrgId(auth, program);
259
+
260
+ const body = {};
261
+ if (options.env && options.env.length > 0) body.env_vars = options.env;
262
+ const persona = buildPersona(options);
263
+ if (Object.keys(persona).length > 0) body.persona = persona;
264
+
265
+ if (!body.env_vars && !body.persona) {
266
+ throw new Error('Specify at least one of --env or a persona field (--name/--description/--icon/--system-prompt-additions).');
267
+ }
268
+
269
+ const ep = resolveEndpoint('powers.deploy-repo.config', { orgId, toolDefId });
270
+ const data = await client.patch(ep.path, { service: ep.service, body });
271
+ printSuccess('Power config updated.');
272
+ formatDetail(data, { format: program.opts().output });
273
+ });
274
+
275
+ // ── powers delete ───────────────────────────────────────────────────────────
276
+
277
+ powers
278
+ .command('delete')
279
+ .description('Permanently destroy a hosted-repo power (revokes agent, deletes credentials)')
280
+ .argument('<toolDefId>', 'tool definition ID')
281
+ .option('--force', 'skip confirmation')
282
+ .action(async (toolDefId, options) => {
283
+ auth.requireAuth();
284
+ const orgId = resolveOrgId(auth, program);
285
+
286
+ if (!options.force && !program.opts().yes) {
287
+ const ok = await confirmOrSkip(`Permanently destroy power ${toolDefId}? This retires its agent and deletes its credentials. This cannot be undone.`);
288
+ if (!ok) { printInfo('Cancelled.'); return; }
289
+ }
290
+
291
+ const ep = resolveEndpoint('powers.deploy-repo.delete', { orgId, toolDefId });
292
+ const data = await client.delete(ep.path, { service: ep.service, query: { confirm: 'true' } });
293
+ printSuccess('Power deleted.');
294
+ formatDetail(data, { format: program.opts().output });
295
+ });
296
+ }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Prompt commands: user-owned prompts library (CRUD + versions + import/export)
3
+ *
4
+ * quox prompt list|get|create|update|delete|versions|restore|export|import
5
+ *
6
+ * Backend: services/auth/routes/prompts.js, mounted at plain `/api/prompts`
7
+ * (NOT org-scoped in the URL — server.js:726). Every operation still requires
8
+ * an org_id: GETs take it as a query param, writes take it as a body field.
9
+ */
10
+
11
+ import { readFileSync } from 'node:fs';
12
+ import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
13
+ import { resolveOrgId } from './helpers.js';
14
+ import { resolveEndpoint } from '../api/endpoints.js';
15
+ import { confirmDestructive } from '../core/confirm.js';
16
+
17
+ /** Parse a stored JSON-string field (tags/variables/deploy_targets) back to an array/object. */
18
+ function parseJsonField(value) {
19
+ if (Array.isArray(value) || (value !== null && typeof value === 'object')) return value;
20
+ if (typeof value !== 'string' || value.trim() === '') return [];
21
+ try {
22
+ return JSON.parse(value);
23
+ } catch {
24
+ return value;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Format a created_at/updated_at value for display. The value may be an
30
+ * epoch-millis number, an all-digits epoch-millis string, or an ISO string,
31
+ * depending on the path it came through. Falsy values pass through as-is
32
+ * rather than rendering "Invalid Date".
33
+ */
34
+ function formatTimestamp(value) {
35
+ if (!value) return value;
36
+ const isEpochMillis = (typeof value === 'number' && Number.isFinite(value))
37
+ || (typeof value === 'string' && /^\d+$/.test(value));
38
+ const date = isEpochMillis ? new Date(Number(value)) : new Date(value);
39
+ if (Number.isNaN(date.getTime())) return value;
40
+ return date.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
41
+ }
42
+
43
+ /**
44
+ * Extract `{{identifier}}` template variables from prompt body text, in
45
+ * order of first appearance, deduped. Conservative identifier pattern
46
+ * (`[A-Za-z_][A-Za-z0-9_]*`) so arbitrary template expressions (helpers,
47
+ * filters, nested paths) aren't misparsed as variable names.
48
+ */
49
+ function extractVariables(body) {
50
+ if (typeof body !== 'string' || body === '') return [];
51
+ const seen = new Set();
52
+ const variables = [];
53
+ const re = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
54
+ let match;
55
+ while ((match = re.exec(body)) !== null) {
56
+ if (!seen.has(match[1])) {
57
+ seen.add(match[1]);
58
+ variables.push(match[1]);
59
+ }
60
+ }
61
+ return variables;
62
+ }
63
+
64
+ function normalizePrompt(p) {
65
+ if (!p) return p;
66
+ return {
67
+ ...p,
68
+ tags: parseJsonField(p.tags),
69
+ variables: parseJsonField(p.variables),
70
+ deploy_targets: parseJsonField(p.deploy_targets),
71
+ created_at: formatTimestamp(p.created_at),
72
+ updated_at: formatTimestamp(p.updated_at),
73
+ };
74
+ }
75
+
76
+ /** Resolve prompt content from --content or --file. Throws if neither given. */
77
+ function resolveContent(options) {
78
+ if (options.file) {
79
+ return readFileSync(options.file, 'utf-8');
80
+ }
81
+ if (options.content !== undefined) {
82
+ return options.content;
83
+ }
84
+ return undefined;
85
+ }
86
+
87
+ export function registerPromptCommands(program, ctx) {
88
+ const { client, auth } = ctx;
89
+
90
+ const prompt = program
91
+ .command('prompt')
92
+ .description('Manage the user-owned prompts library');
93
+
94
+ // ── prompt list ─────────────────────────────────────────────────────────────
95
+
96
+ prompt
97
+ .command('list')
98
+ .description('List prompts (own + org-visible)')
99
+ .option('--search <text>', 'substring search on name/body')
100
+ .option('--tags <tags>', 'comma-separated tag filter (match any)')
101
+ .action(async (options) => {
102
+ auth.requireAuth();
103
+ const orgId = resolveOrgId(auth, program);
104
+ const query = { org_id: orgId };
105
+ if (options.search) query.q = options.search;
106
+ if (options.tags) query.tags = options.tags;
107
+
108
+ const data = await client.get(resolveEndpoint('prompts.list').path, {
109
+ service: resolveEndpoint('prompts.list').service,
110
+ query,
111
+ });
112
+ const prompts = (data.prompts || data || []).map(normalizePrompt);
113
+
114
+ formatOutput({
115
+ columns: ['id', 'name', 'category', 'visibility', 'tags', 'updated_at'],
116
+ headers: ['ID', 'Name', 'Category', 'Visibility', 'Tags', 'Updated'],
117
+ rows: prompts.map(p => ({
118
+ ...p,
119
+ tags: Array.isArray(p.tags) ? p.tags.join(', ') : p.tags,
120
+ })),
121
+ }, { format: program.opts().output, emptyMessage: 'No prompts found.' });
122
+ });
123
+
124
+ // ── prompt get ──────────────────────────────────────────────────────────────
125
+
126
+ prompt
127
+ .command('get')
128
+ .description('Show prompt details')
129
+ .argument('<id>', 'prompt ID')
130
+ .action(async (id) => {
131
+ auth.requireAuth();
132
+ const orgId = resolveOrgId(auth, program);
133
+ const data = await client.get(resolveEndpoint('prompts.get', { id }).path, {
134
+ service: resolveEndpoint('prompts.get', { id }).service,
135
+ query: { org_id: orgId },
136
+ });
137
+ const p = normalizePrompt(data.prompt || data);
138
+
139
+ formatDetail(p, {
140
+ format: program.opts().output,
141
+ title: `Prompt: ${p.name || id}`,
142
+ fields: ['id', 'name', 'description', 'category', 'visibility', 'tags', 'body', 'variables', 'deploy_targets', 'created_at', 'updated_at'],
143
+ labels: {
144
+ id: 'ID', name: 'Name', description: 'Description', category: 'Category',
145
+ visibility: 'Visibility', tags: 'Tags', body: 'Body', variables: 'Variables',
146
+ deploy_targets: 'Deploy Targets', created_at: 'Created', updated_at: 'Updated',
147
+ },
148
+ });
149
+ });
150
+
151
+ // ── prompt create ───────────────────────────────────────────────────────────
152
+
153
+ prompt
154
+ .command('create')
155
+ .description('Create a prompt')
156
+ .requiredOption('--name <name>', 'prompt name')
157
+ .option('--content <text>', 'prompt body text')
158
+ .option('--file <path>', 'read prompt body from a file (overrides --content)')
159
+ .option('--description <text>', 'description')
160
+ .option('--tags <tags>', 'comma-separated tags')
161
+ .option('--category <category>', 'category')
162
+ .option('--visibility <vis>', 'private|org', 'private')
163
+ .action(async (options) => {
164
+ auth.requireAuth();
165
+ const orgId = resolveOrgId(auth, program);
166
+
167
+ const body = resolveContent(options);
168
+ if (body === undefined || body.trim() === '') {
169
+ throw new Error('Prompt content is required. Use --content <text> or --file <path>.');
170
+ }
171
+
172
+ const payload = { org_id: orgId, name: options.name, body };
173
+ if (options.description) payload.description = options.description;
174
+ if (options.tags) payload.tags = options.tags.split(',').map(t => t.trim()).filter(Boolean);
175
+ if (options.category) payload.category = options.category;
176
+ if (options.visibility) payload.visibility = options.visibility;
177
+ // fleet #398: nothing extracted {{var}} tokens from the body, so
178
+ // `variables` was always stored as []. Only fill it in when the
179
+ // caller didn't explicitly pass one, and only when the body actually
180
+ // contains variables (keeps the payload identical to before for
181
+ // variable-less prompts).
182
+ const createdVariables = extractVariables(body);
183
+ if (payload.variables === undefined && createdVariables.length > 0) payload.variables = createdVariables;
184
+
185
+ const data = await client.post(resolveEndpoint('prompts.create').path, {
186
+ service: resolveEndpoint('prompts.create').service,
187
+ body: payload,
188
+ });
189
+ printSuccess(`Prompt "${options.name}" created.`);
190
+ formatDetail(normalizePrompt(data.prompt || data), { format: program.opts().output });
191
+ });
192
+
193
+ // ── prompt update ───────────────────────────────────────────────────────────
194
+
195
+ prompt
196
+ .command('update')
197
+ .description('Update a prompt (bumps version)')
198
+ .argument('<id>', 'prompt ID')
199
+ .option('--name <name>', 'new name')
200
+ .option('--content <text>', 'new body text')
201
+ .option('--file <path>', 'read new body from a file (overrides --content)')
202
+ .option('--description <text>', 'new description')
203
+ .option('--tags <tags>', 'new tags (comma-separated)')
204
+ .option('--category <category>', 'new category')
205
+ .option('--visibility <vis>', 'private|org')
206
+ .action(async (id, options) => {
207
+ auth.requireAuth();
208
+ const orgId = resolveOrgId(auth, program);
209
+
210
+ const newBody = resolveContent(options);
211
+ const payload = { org_id: orgId };
212
+ if (options.name) payload.name = options.name;
213
+ if (newBody !== undefined) payload.body = newBody;
214
+ if (options.description !== undefined) payload.description = options.description;
215
+ if (options.tags) payload.tags = options.tags.split(',').map(t => t.trim()).filter(Boolean);
216
+ if (options.category) payload.category = options.category;
217
+ if (options.visibility) payload.visibility = options.visibility;
218
+ // Re-extract {{var}} tokens whenever the body changes, so stale
219
+ // variables from the old body don't linger. Only when the caller
220
+ // didn't explicitly pass one.
221
+ if (newBody !== undefined && payload.variables === undefined) {
222
+ payload.variables = extractVariables(newBody);
223
+ }
224
+
225
+ if (Object.keys(payload).length === 1) {
226
+ throw new Error('Specify at least one field to update (--name, --content/--file, --description, --tags, --category, --visibility).');
227
+ }
228
+
229
+ const data = await client.patch(resolveEndpoint('prompts.update', { id }).path, {
230
+ service: resolveEndpoint('prompts.update', { id }).service,
231
+ body: payload,
232
+ });
233
+ printSuccess('Prompt updated.');
234
+ formatDetail(normalizePrompt(data.prompt || data), { format: program.opts().output });
235
+ });
236
+
237
+ // ── prompt delete ───────────────────────────────────────────────────────────
238
+
239
+ prompt
240
+ .command('delete')
241
+ .description('Delete a prompt (soft-delete)')
242
+ .argument('<id>', 'prompt ID')
243
+ .option('--force', 'skip confirmation')
244
+ .action(async (id, options) => {
245
+ auth.requireAuth();
246
+ const orgId = resolveOrgId(auth, program);
247
+
248
+ const confirmed = await confirmDestructive(program, options, `Delete prompt ${id}? This cannot be undone.`);
249
+ if (!confirmed) {
250
+ printInfo('Cancelled.');
251
+ return;
252
+ }
253
+
254
+ // NOTE: org_id MUST be sent in the body, not the query string. The
255
+ // handler's `req.body || req.query` fallback (prompts.js:431) never
256
+ // reaches req.query because express.json() leaves req.body as a
257
+ // truthy `{}` even on a request with no JSON body.
258
+ await client.delete(resolveEndpoint('prompts.delete', { id }).path, {
259
+ service: resolveEndpoint('prompts.delete', { id }).service,
260
+ body: { org_id: orgId },
261
+ });
262
+ printSuccess('Prompt deleted.');
263
+ });
264
+
265
+ // ── prompt versions ─────────────────────────────────────────────────────────
266
+
267
+ prompt
268
+ .command('versions')
269
+ .description('Show version history for a prompt')
270
+ .argument('<id>', 'prompt ID')
271
+ .action(async (id) => {
272
+ auth.requireAuth();
273
+ const orgId = resolveOrgId(auth, program);
274
+ const data = await client.get(resolveEndpoint('prompts.versions', { id }).path, {
275
+ service: resolveEndpoint('prompts.versions', { id }).service,
276
+ query: { org_id: orgId },
277
+ });
278
+ const versions = data.versions || data || [];
279
+
280
+ formatOutput({
281
+ columns: ['version', 'name', 'created_at', 'created_by'],
282
+ headers: ['Version', 'Name', 'Created', 'By'],
283
+ rows: versions,
284
+ }, { format: program.opts().output, emptyMessage: 'No version history found.' });
285
+ });
286
+
287
+ // ── prompt restore ──────────────────────────────────────────────────────────
288
+
289
+ prompt
290
+ .command('restore')
291
+ .description('Restore a prompt to a previous version')
292
+ .argument('<id>', 'prompt ID')
293
+ .argument('<version>', 'version number to restore')
294
+ .action(async (id, version) => {
295
+ auth.requireAuth();
296
+ const orgId = resolveOrgId(auth, program);
297
+ const data = await client.post(resolveEndpoint('prompts.restore', { id, version }).path, {
298
+ service: resolveEndpoint('prompts.restore', { id, version }).service,
299
+ body: { org_id: orgId },
300
+ });
301
+ printSuccess(`Prompt restored from version ${version}.`);
302
+ formatDetail(normalizePrompt(data.prompt || data), { format: program.opts().output });
303
+ });
304
+
305
+ // ── prompt export ───────────────────────────────────────────────────────────
306
+
307
+ prompt
308
+ .command('export')
309
+ .description('Export all of the caller\'s prompts as JSON')
310
+ .action(async () => {
311
+ auth.requireAuth();
312
+ const orgId = resolveOrgId(auth, program);
313
+ const data = await client.get(resolveEndpoint('prompts.export').path, {
314
+ service: resolveEndpoint('prompts.export').service,
315
+ query: { org_id: orgId },
316
+ });
317
+ console.log(JSON.stringify(data, null, 2));
318
+ });
319
+
320
+ // ── prompt import ───────────────────────────────────────────────────────────
321
+
322
+ prompt
323
+ .command('import')
324
+ .description('Bulk-import prompts from a JSON file (as produced by `quox prompt export`)')
325
+ .argument('<file>', 'path to a JSON file with an array of prompts or { prompts: [...] }')
326
+ .action(async (file) => {
327
+ auth.requireAuth();
328
+ const orgId = resolveOrgId(auth, program);
329
+
330
+ let parsed;
331
+ try {
332
+ parsed = JSON.parse(readFileSync(file, 'utf-8'));
333
+ } catch (err) {
334
+ throw new Error(`Could not read/parse ${file}: ${err.message}`);
335
+ }
336
+ const prompts = Array.isArray(parsed) ? parsed : (parsed.prompts || []);
337
+ if (!Array.isArray(prompts) || prompts.length === 0) {
338
+ throw new Error(`No prompts found in ${file}. Expected an array or { "prompts": [...] }.`);
339
+ }
340
+
341
+ const data = await client.post(resolveEndpoint('prompts.import').path, {
342
+ service: resolveEndpoint('prompts.import').service,
343
+ body: { org_id: orgId, prompts },
344
+ });
345
+ printSuccess(`Imported ${data.imported ?? 0} prompt(s).`);
346
+ if ((data.imported ?? 0) < prompts.length) {
347
+ printWarning(`${prompts.length - (data.imported ?? 0)} prompt(s) were skipped (missing name/body).`);
348
+ }
349
+ });
350
+ }