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.
- package/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- 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
|
+
}
|