quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/commands/hat.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QuoxHat commands (QUOXHAT #721 b
|
|
2
|
+
* QuoxHat commands (QUOXHAT #721 b; CUSTOM-HATS 2026-10-17).
|
|
3
3
|
*
|
|
4
4
|
* quox hat list
|
|
5
5
|
* quox hat show <id|key> [--rev <n>]
|
|
6
|
-
* quox hat reset <id|key> --yes (audited reset-to-template)
|
|
6
|
+
* quox hat reset <id|key> --yes (audited reset-to-template, built-ins)
|
|
7
|
+
* quox hat create <key> --name "<Name>" [--from <hat>] [--agents a,b] [--tools x,y] [--mode m] [--offer o]
|
|
8
|
+
* quox hat edit <hat> [--name N] [--add-agents|--remove-agents|--agents] [--add-tools|--remove-tools|--tools] [--mode m]
|
|
9
|
+
* quox hat delete <hat> --yes (custom hats only)
|
|
10
|
+
* quox hat use <hat> --conversation <id> [--yes]
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Custom hats (owner 2026-10-17): users who build their own agents organise them under their own
|
|
13
|
+
* hats, so a turn under "security" carries only the security agents, not every agent in the org.
|
|
14
|
+
* create/edit/delete act on CUSTOM hats only; a built-in is reset to its template, or copied with --from.
|
|
15
|
+
* Everything is server-side, org-scoped, revisioned and audited (services/auth/routes/quoxhat.js);
|
|
16
|
+
* who may create or change an org hat is the existing hat-edit permission (org manage-settings),
|
|
17
|
+
* a private hat (--private) only its owner. This file never decides permission itself.
|
|
14
18
|
*
|
|
15
|
-
* Backend: services/auth/routes/quoxhat.js (see cli/lib/api/endpoints/
|
|
19
|
+
* Backend: services/auth/routes/quoxhat.js (see cli/lib/api/endpoints/hat.js).
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import chalk from 'chalk';
|
|
@@ -59,6 +63,32 @@ export function findHat(hats, ref) {
|
|
|
59
63
|
return (hats || []).find(h => h.id === r) || (hats || []).find(h => h.key === r) || null;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
|
|
67
|
+
export { DELEGATION_TOOLS, HAT_MODES, HAT_OFFERS, parseList, memberTools, applyMode, buildCustomDefinition, applyHatEdits } from '../core/customHats.js';
|
|
68
|
+
import { parseList, memberTools, buildCustomDefinition, applyHatEdits } from '../core/customHats.js';
|
|
69
|
+
|
|
70
|
+
/** Which of `ids` no registered or org agent matches. checked=false when neither list could be read. */
|
|
71
|
+
export async function findUnknownAgents(client, orgId, ids) {
|
|
72
|
+
if (!ids.length) return { unknown: [], checked: true };
|
|
73
|
+
const known = new Set();
|
|
74
|
+
let read = 0;
|
|
75
|
+
try {
|
|
76
|
+
const ep = resolveEndpoint('v1.agents.registry');
|
|
77
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
78
|
+
if (Array.isArray(d?.agents)) { read++; for (const a of d.agents) if (a?.agent_id) known.add(String(a.agent_id).toLowerCase()); }
|
|
79
|
+
} catch { /* named below */ }
|
|
80
|
+
try {
|
|
81
|
+
const ep = resolveEndpoint('agents.list', { orgId });
|
|
82
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
83
|
+
const rows = Array.isArray(d?.agents) ? d.agents : (Array.isArray(d) ? d : null);
|
|
84
|
+
if (rows) { read++; for (const a of rows) { const id = a?.agent_id || a?.agentId; if (id) known.add(String(id).toLowerCase()); } }
|
|
85
|
+
} catch { /* named below */ }
|
|
86
|
+
if (!read) return { unknown: [], checked: false };
|
|
87
|
+
return { unknown: ids.filter(id => !known.has(id)), checked: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isBuiltin(h) { return !!h?.template_key; }
|
|
91
|
+
|
|
62
92
|
export function registerHatCommands(program, ctx) {
|
|
63
93
|
const { client, auth } = ctx;
|
|
64
94
|
|
|
@@ -154,6 +184,8 @@ export function registerHatCommands(program, ctx) {
|
|
|
154
184
|
out.push(`Enforcement: ${enforcement} (org)`);
|
|
155
185
|
out.push(`Transitions: confirm=${def.transitions?.confirm || 'none'} privileged=${def.transitions?.privileged ? 'yes' : 'no'}`);
|
|
156
186
|
out.push(`Agents: orchestrator=${def.agents?.orchestrator || '-'} specialists=${(def.agents?.specialists || []).join(', ') || 'none'}`);
|
|
187
|
+
out.push(`Offer: ${def.tools?.offer === 'members' ? 'members (an orchestrator turn is offered only this hat\'s tools)' : 'authorized (hat tools first, then the rest of the grant)'}`);
|
|
188
|
+
out.push(`Kind: ${h.template_key ? 'built-in' : 'custom'}${h.visibility === 'private' ? ' (private)' : ''}`);
|
|
157
189
|
out.push(`Budgets: initial_tool_target=${def.budgets?.initial_tool_target ?? '-'} max_manifest=${def.budgets?.max_manifest ?? '-'}`);
|
|
158
190
|
out.push('');
|
|
159
191
|
const explicit = def.tools?.explicit || [];
|
|
@@ -202,4 +234,141 @@ export function registerHatCommands(program, ctx) {
|
|
|
202
234
|
if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h }, null, 2)); return; }
|
|
203
235
|
console.log(` Ⓠ QUOX │ hat ${h.key} reset to template │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
|
|
204
236
|
});
|
|
237
|
+
// ── hat create (custom) ───────────────────────────────────────────────────────
|
|
238
|
+
hat
|
|
239
|
+
.command('create <key>')
|
|
240
|
+
.description('Create a custom hat: the agents and tools it lists are the only ones a turn under it carries')
|
|
241
|
+
.requiredOption('--name <name>', 'display name, e.g. "Security"')
|
|
242
|
+
.option('--from <hat>', 'start from a copy of this hat (key or id)')
|
|
243
|
+
.option('--agents <ids>', 'comma-separated agent ids this hat puts in scope (registered or your own agents)')
|
|
244
|
+
.option('--tools <names>', 'comma-separated tool names the orchestrator is offered under this hat')
|
|
245
|
+
.option('--mode <mode>', 'permissive (default: each agent keeps its own grant) or restrictive (only the listed tools are authorized, for every agent under the hat)')
|
|
246
|
+
.option('--offer <offer>', 'members (default: offer only this hat\'s tools) or authorized')
|
|
247
|
+
.option('--purpose <text>', 'one line: what this hat is for')
|
|
248
|
+
.option('--private', 'a private hat only you can see and wear (needs no org settings permission)')
|
|
249
|
+
.option('--reason <text>', 'why (kept in the revision history)')
|
|
250
|
+
.action(async (key, options) => {
|
|
251
|
+
auth.requireAuth();
|
|
252
|
+
const orgId = resolveOrgId(auth, program);
|
|
253
|
+
const k = String(key || '').trim().toLowerCase();
|
|
254
|
+
if (!/^[a-z0-9][a-z0-9-]{1,40}$/.test(k)) throw new Error(`Hat key '${key}' must be a lowercase slug (a-z, 0-9, hyphen), 2-41 characters.`);
|
|
255
|
+
const list = await fetchList(client, orgId);
|
|
256
|
+
const hats = list?.hats || [];
|
|
257
|
+
if (findHat(hats, k)) throw new Error(`A hat with key '${k}' already exists in this org. Use quox hat edit ${k}.`);
|
|
258
|
+
let base = null;
|
|
259
|
+
if (options.from) {
|
|
260
|
+
const from = findHat(hats, options.from);
|
|
261
|
+
if (!from) throw new Error(`No hat '${options.from}' to copy. Available: ${hats.map(h => h.key).join(', ') || 'none'}`);
|
|
262
|
+
base = from.definition;
|
|
263
|
+
}
|
|
264
|
+
const agents = options.agents !== undefined ? parseList(options.agents) : undefined;
|
|
265
|
+
const tools = options.tools !== undefined ? parseList(options.tools) : undefined;
|
|
266
|
+
const { definition, autoAdded } = buildCustomDefinition({ base, name: options.name, purpose: options.purpose, agents, tools, mode: options.mode, offer: options.offer });
|
|
267
|
+
const named = definition.agents.specialists;
|
|
268
|
+
const check = await findUnknownAgents(client, orgId, named);
|
|
269
|
+
if (check.unknown.length) throw new Error(`Unknown agent(s): ${check.unknown.join(', ')}. See quox agent list.`);
|
|
270
|
+
const ep = resolveEndpoint('hat.create', { orgId });
|
|
271
|
+
const data = await client.post(ep.path, { service: ep.service, body: { key: k, name: String(options.name).trim(), definition, visibility: options.private ? 'private' : 'org' } });
|
|
272
|
+
const h = data?.hat;
|
|
273
|
+
if (!h) throw new Error(`Create of '${k}' returned no hat`);
|
|
274
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h, warnings: data.warnings || [], auto_added_tools: autoAdded }, null, 2)); return; }
|
|
275
|
+
const d = h.definition || definition;
|
|
276
|
+
console.log(` Ⓠ QUOX │ hat ${h.key} created │ rev ${h.current_revision} · agents ${(d.agents?.specialists || []).join(', ') || 'none'} · ${memberTools(d).length} tool(s) · offer ${d.tools?.offer || 'authorized'} · ${authzMode(d)} ✔`);
|
|
277
|
+
if (autoAdded.length) printInfo(`Added ${autoAdded.join(', ')} so the orchestrator can reach the hat's agents.`);
|
|
278
|
+
if (!check.checked) printInfo('Agent ids were not checked: the agent lists could not be read.');
|
|
279
|
+
if (d.authz?.mode === 'restrictive') printInfo('Restrictive: every agent working under this hat, its own agents included when delegated to, may only run the tools listed here.');
|
|
280
|
+
for (const w of data.warnings || []) printInfo(`warning: ${w}`);
|
|
281
|
+
printInfo(`Wear it: /hat ${h.key} in quox, or quox hat use ${h.key} --conversation <id>.`);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── hat edit (custom) ─────────────────────────────────────────────────────────
|
|
285
|
+
hat
|
|
286
|
+
.command('edit <hat>')
|
|
287
|
+
.description('Change a custom hat: add or remove agents and tools, rename, mode (appends an audited revision)')
|
|
288
|
+
.option('--name <name>', 'rename the hat')
|
|
289
|
+
.option('--purpose <text>', 'new one-line purpose')
|
|
290
|
+
.option('--agents <ids>', 'replace the agent list')
|
|
291
|
+
.option('--add-agents <ids>', 'comma-separated agents to add')
|
|
292
|
+
.option('--remove-agents <ids>', 'comma-separated agents to remove')
|
|
293
|
+
.option('--tools <names>', 'replace the explicit tool list')
|
|
294
|
+
.option('--add-tools <names>', 'comma-separated tools to add')
|
|
295
|
+
.option('--remove-tools <names>', 'comma-separated tools to remove')
|
|
296
|
+
.option('--mode <mode>', 'permissive or restrictive')
|
|
297
|
+
.option('--offer <offer>', 'members or authorized')
|
|
298
|
+
.option('--reason <text>', 'why (kept in the revision history)')
|
|
299
|
+
.action(async (ref, options) => {
|
|
300
|
+
auth.requireAuth();
|
|
301
|
+
const orgId = resolveOrgId(auth, program);
|
|
302
|
+
const list = await fetchList(client, orgId);
|
|
303
|
+
const found = findHat(list?.hats, ref);
|
|
304
|
+
if (!found) throw new Error(`No hat '${ref}' in this org. Available: ${(list?.hats || []).map(h => h.key).join(', ') || 'none'}`);
|
|
305
|
+
if (isBuiltin(found)) throw new Error(`'${found.key}' is a built-in hat: quox hat edit changes custom hats only. Reset it with quox hat reset ${found.key} --yes, or make your own from it: quox hat create <key> --name "<Name>" --from ${found.key}.`);
|
|
306
|
+
const opts = {
|
|
307
|
+
agents: options.agents !== undefined ? parseList(options.agents) : undefined,
|
|
308
|
+
addAgents: parseList(options.addAgents),
|
|
309
|
+
removeAgents: parseList(options.removeAgents),
|
|
310
|
+
tools: options.tools !== undefined ? parseList(options.tools) : undefined,
|
|
311
|
+
addTools: parseList(options.addTools),
|
|
312
|
+
removeTools: parseList(options.removeTools),
|
|
313
|
+
mode: options.mode,
|
|
314
|
+
offer: options.offer,
|
|
315
|
+
purpose: options.purpose,
|
|
316
|
+
};
|
|
317
|
+
const { definition, changes } = applyHatEdits(found.definition, opts);
|
|
318
|
+
const rename = options.name !== undefined && String(options.name).trim() !== found.name ? String(options.name).trim() : undefined;
|
|
319
|
+
if (rename !== undefined) changes.unshift(`name "${found.name}" -> "${rename}"`);
|
|
320
|
+
if (!changes.length) throw new Error(`Nothing to change on '${found.key}'.`);
|
|
321
|
+
const newAgents = [...(opts.agents || []), ...opts.addAgents];
|
|
322
|
+
const check = await findUnknownAgents(client, orgId, newAgents);
|
|
323
|
+
if (check.unknown.length) throw new Error(`Unknown agent(s): ${check.unknown.join(', ')}. See quox agent list.`);
|
|
324
|
+
const ep = resolveEndpoint('hat.update', { orgId, hatId: found.id });
|
|
325
|
+
const data = await client.put(ep.path, { service: ep.service, body: { definition, ...(rename !== undefined ? { name: rename } : {}), change_reason: options.reason || changes.join('; ') } });
|
|
326
|
+
const h = data?.hat;
|
|
327
|
+
if (!h) throw new Error(`Edit of '${found.key}' returned no hat`);
|
|
328
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h, changes, warnings: data.warnings || [] }, null, 2)); return; }
|
|
329
|
+
console.log(` Ⓠ QUOX │ hat ${h.key} edited │ rev ${h.current_revision} · ${changes.join(' · ')} ✔`);
|
|
330
|
+
for (const w of data.warnings || []) printInfo(`warning: ${w}`);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ── hat delete (custom) ───────────────────────────────────────────────────────
|
|
334
|
+
hat
|
|
335
|
+
.command('delete <hat>')
|
|
336
|
+
.description('Delete a custom hat (built-ins cannot be deleted; reset them instead). Audited.')
|
|
337
|
+
.option('--yes', 'confirm: conversations wearing it fall back to the default hat')
|
|
338
|
+
.action(async (ref, options) => {
|
|
339
|
+
auth.requireAuth();
|
|
340
|
+
const orgId = resolveOrgId(auth, program);
|
|
341
|
+
const list = await fetchList(client, orgId);
|
|
342
|
+
const found = findHat(list?.hats, ref);
|
|
343
|
+
if (!found) throw new Error(`No hat '${ref}' in this org. Available: ${(list?.hats || []).map(h => h.key).join(', ') || 'none'}`);
|
|
344
|
+
if (isBuiltin(found)) throw new Error(`'${found.key}' is a built-in hat and cannot be deleted. quox hat reset ${found.key} --yes returns it to its template.`);
|
|
345
|
+
if (!options.yes) throw new Error(`Deleting '${found.key}' moves every conversation wearing it back to the default hat. Re-run with --yes to confirm.`);
|
|
346
|
+
const ep = resolveEndpoint('hat.delete', { orgId, hatId: found.id });
|
|
347
|
+
const data = await client.delete(ep.path, { service: ep.service });
|
|
348
|
+
const d = data?.deleted;
|
|
349
|
+
if (!d) throw new Error(`Delete of '${found.key}' returned no confirmation`);
|
|
350
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ deleted: d }, null, 2)); return; }
|
|
351
|
+
console.log(` Ⓠ QUOX │ hat ${d.key} deleted │ was rev ${d.revision} · ${d.bindings_cleared} conversation(s) moved to the default · ${d.user_prefs_cleared} preference(s) cleared ✔`);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ── hat use ───────────────────────────────────────────────────────────────────
|
|
355
|
+
hat
|
|
356
|
+
.command('use <hat>')
|
|
357
|
+
.description('Wear a hat in one conversation (the same audited switch as /hat <name> in quox)')
|
|
358
|
+
.requiredOption('--conversation <id>', 'the conversation to switch')
|
|
359
|
+
.option('--yes', 'confirm a privileged hat')
|
|
360
|
+
.action(async (ref, options) => {
|
|
361
|
+
auth.requireAuth();
|
|
362
|
+
const orgId = resolveOrgId(auth, program);
|
|
363
|
+
const list = await fetchList(client, orgId);
|
|
364
|
+
const found = findHat(list?.hats, ref);
|
|
365
|
+
if (!found) throw new Error(`No hat '${ref}' in this org. Available: ${(list?.hats || []).map(h => h.key).join(', ') || 'none'}`);
|
|
366
|
+
if (found.definition?.transitions?.privileged && !options.yes) throw new Error(`'${found.key}' is a privileged hat. Re-run with --yes to wear it.`);
|
|
367
|
+
const ep = resolveEndpoint('hat.bind', { orgId, conversationId: options.conversation });
|
|
368
|
+
const data = await client.put(ep.path, { service: ep.service, body: { hat_id: found.id } });
|
|
369
|
+
const b = data?.binding;
|
|
370
|
+
if (!b) throw new Error(`Switch to '${found.key}' returned no binding`);
|
|
371
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ binding: b, hat: { id: found.id, key: found.key } }, null, 2)); return; }
|
|
372
|
+
console.log(` Ⓠ QUOX │ conversation ${options.conversation} now wears ${found.key} │ rev ${b.revision} · seq ${b.seq} ✔`);
|
|
373
|
+
});
|
|
205
374
|
}
|
package/lib/commands/helpers.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared helpers for command modules
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { QuoxValidationError } from '../core/errors.js';
|
|
5
|
+
import { QuoxValidationError, QuoxNetworkError } from '../core/errors.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Resolve the active org ID from --org flag or stored auth.
|
|
@@ -77,3 +77,20 @@ export function buildListQuery(options) {
|
|
|
77
77
|
if (options.sort) query.sort = options.sort;
|
|
78
78
|
return query;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run a request; if it times out, rethrow with what timed out and how to narrow
|
|
83
|
+
* it (still a QuoxNetworkError, so the exit code stays 7). A bare
|
|
84
|
+
* "Request timed out after 30000ms" does not tell the operator which call hung
|
|
85
|
+
* or what to try (quox#806, quox#807).
|
|
86
|
+
*/
|
|
87
|
+
export async function withTimeoutHint(what, hint, fn) {
|
|
88
|
+
try {
|
|
89
|
+
return await fn();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof QuoxNetworkError && /timed out/i.test(err.message || '')) {
|
|
92
|
+
throw new QuoxNetworkError(`${what} timed out (${err.message}). ${hint}`, err);
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox hyperion`: everything HYPERION (the governed Proxmox operator, agent id atlas) is doing in one place.
|
|
3
|
+
* Collector route GET /api/v1/proxmox/activity (org-scoped, read only).
|
|
4
|
+
*
|
|
5
|
+
* quox hyperion [status] [--json] VMs and who holds them, node load, pool counts, last 5 ops
|
|
6
|
+
* quox hyperion deck live full-screen view, refreshes every 10s (q quits, r refreshes)
|
|
7
|
+
*/
|
|
8
|
+
import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
9
|
+
import { fetchActivity, statusLines, summaryText } from '../core/hyperion.js';
|
|
10
|
+
|
|
11
|
+
export function registerHyperionCommands(program, ctx) {
|
|
12
|
+
const { client, auth } = ctx;
|
|
13
|
+
|
|
14
|
+
const hy = program
|
|
15
|
+
.command('hyperion')
|
|
16
|
+
.description('HYPERION activity: which VMs exist, who holds each and why, node load and recent operations')
|
|
17
|
+
.option('--json', 'print the full JSON response');
|
|
18
|
+
|
|
19
|
+
const wantJson = (cmd) => {
|
|
20
|
+
const o = cmd.optsWithGlobals ? cmd.optsWithGlobals() : cmd.opts();
|
|
21
|
+
return Boolean(o.json) || program.opts().output === 'json';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// An error body is not data: report the status and stop.
|
|
25
|
+
async function show(cmd) {
|
|
26
|
+
auth.requireAuth();
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
data = await fetchActivity(client);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const status = err && (err.status || err.statusCode);
|
|
32
|
+
if (!status) throw err;
|
|
33
|
+
console.error(`HYPERION request failed: HTTP ${status}${err.message ? ` ${err.message}` : ''}`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
|
|
38
|
+
printEvent(glyphLine('hyperion', 'activity', { result: summaryText(data) }));
|
|
39
|
+
console.log(statusLines(data).join('\n'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
hy.action(async (_o, cmd) => show(cmd));
|
|
43
|
+
|
|
44
|
+
hy
|
|
45
|
+
.command('status')
|
|
46
|
+
.description('Same as bare `quox hyperion`')
|
|
47
|
+
.option('--json', 'print the full JSON response')
|
|
48
|
+
.action(async (_o, cmd) => show(cmd));
|
|
49
|
+
|
|
50
|
+
hy
|
|
51
|
+
.command('deck')
|
|
52
|
+
.description('Live full-screen HYPERION deck (refreshes every 10s; r refreshes, q quits)')
|
|
53
|
+
.action(async () => {
|
|
54
|
+
auth.requireAuth();
|
|
55
|
+
const { launchHyperionDeck } = await import('../hyperion/deck.js');
|
|
56
|
+
await launchHyperionDeck({ activity: () => fetchActivity(client) });
|
|
57
|
+
});
|
|
58
|
+
}
|
package/lib/commands/inbox.js
CHANGED
|
@@ -5,39 +5,52 @@
|
|
|
5
5
|
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
6
6
|
import { resolveOrgId } from './helpers.js';
|
|
7
7
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
import { attachWalkthrough } from './walkthrough.js';
|
|
8
9
|
import { getApprovalContext, formatApprovalContext, approvalListColumns } from '../core/approvalContext.js';
|
|
10
|
+
import { QuoxUserError } from '../core/errors.js';
|
|
11
|
+
import { resolutionFromItem, alreadyResolvedError } from '../core/inboxResolution.js';
|
|
12
|
+
import chalk from 'chalk';
|
|
9
13
|
|
|
10
14
|
export function registerInboxCommands(program, ctx) {
|
|
11
15
|
const { client, auth } = ctx;
|
|
12
16
|
|
|
13
|
-
// An agent hands back a tool approvalId (tool_approvals.approval_id),
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
+
// An agent hands back a tool approvalId (tool_approvals.approval_id), stored on the card as
|
|
18
|
+
// aee_correlation_id. The server now resolves it on GET and respond (CLI-ERRORS); this client-side
|
|
19
|
+
// scan stays as the fallback for an older server. A failed scan is reported, never read as "no card".
|
|
20
|
+
async function findByApprovalId(orgId, id) {
|
|
21
|
+
const list = resolveEndpoint('inbox.list', { orgId });
|
|
22
|
+
const data = await client.get(list.path, { service: list.service, query: { status: 'all', kind: 'approval', limit: 200 } });
|
|
23
|
+
const items = (data && (data.items || data)) || [];
|
|
24
|
+
const hit = Array.isArray(items) && items.find((it) => it.aee_correlation_id === id || (it.payload && it.payload.approval_id === id));
|
|
25
|
+
return hit ? hit.id : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function notFound(id, cause) {
|
|
29
|
+
const err = new QuoxUserError(`No inbox item or approval with id ${id} in this org, open or resolved.`, {
|
|
30
|
+
next: "if it came from another org, switch with `quox org switch <org>`; `quox inbox list --status all` lists this org's items.",
|
|
31
|
+
exitCode: 5, code: 'not_found', status: 404, body: cause?.body,
|
|
32
|
+
});
|
|
33
|
+
return err;
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
async function resolveItemId(orgId, id) {
|
|
18
37
|
const direct = resolveEndpoint('inbox.get', { orgId, itemId: id });
|
|
19
38
|
try {
|
|
20
|
-
await client.get(direct.path, { service: direct.service });
|
|
21
|
-
|
|
39
|
+
const data = await client.get(direct.path, { service: direct.service });
|
|
40
|
+
// the server resolves an approvalId to its card; post to the card's own id
|
|
41
|
+
return (data && data.item && data.item.id) || id;
|
|
22
42
|
} catch (err) {
|
|
23
43
|
if (!err || err.status !== 404) throw err;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
data = await client.get(list.path, { service: list.service, query: { status: 'all', kind: 'approval', limit: 200 } });
|
|
28
|
-
} catch { data = null; }
|
|
29
|
-
const items = (data && (data.items || data)) || [];
|
|
30
|
-
const hit = Array.isArray(items) && items.find((it) => it.aee_correlation_id === id || (it.payload && it.payload.approval_id === id));
|
|
31
|
-
if (hit) return hit.id;
|
|
32
|
-
err.message += `\nHint: ${id} is not an inbox item id, and no approval card in your recent approvals carries it as an approvalId. `
|
|
33
|
-
+ 'Try `quox inbox list --status all` to find the card.';
|
|
34
|
-
throw err;
|
|
44
|
+
const hit = await findByApprovalId(orgId, id);
|
|
45
|
+
if (hit) return hit;
|
|
46
|
+
throw notFound(id, err);
|
|
35
47
|
}
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
const inbox = program
|
|
39
51
|
.command('inbox')
|
|
40
52
|
.description('Manage inbox and approval requests');
|
|
53
|
+
attachWalkthrough(inbox, ctx, program);
|
|
41
54
|
|
|
42
55
|
inbox
|
|
43
56
|
.command('list')
|
|
@@ -102,14 +115,52 @@ export function registerInboxCommands(program, ctx) {
|
|
|
102
115
|
.action(async (givenId, options) => {
|
|
103
116
|
auth.requireAuth();
|
|
104
117
|
const orgId = resolveOrgId(auth, program);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
body: { action: options.action, comment: options.comment }
|
|
109
|
-
}
|
|
110
|
-
|
|
118
|
+
const format = program.opts().output;
|
|
119
|
+
const post = (itemId) => {
|
|
120
|
+
const ep = resolveEndpoint('inbox.respond', { orgId, itemId });
|
|
121
|
+
return client.post(ep.path, { service: ep.service, body: { action: options.action, comment: options.comment } });
|
|
122
|
+
};
|
|
123
|
+
// Resolve an approvalId to its card (server-side on a current server, else the client scan). When
|
|
124
|
+
// this profile cannot see the card, still ask the server: it knows WHY (an agent login, another
|
|
125
|
+
// org, already decided) and says so, where "not found" would hide it.
|
|
126
|
+
let itemId = givenId;
|
|
127
|
+
try { itemId = await resolveItemId(orgId, givenId); } catch (err) { if (err?.code !== 'not_found') throw err; }
|
|
128
|
+
let result;
|
|
129
|
+
try {
|
|
130
|
+
result = await post(itemId);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err?.status === 404) throw notFound(givenId, err);
|
|
133
|
+
if (!result) {
|
|
134
|
+
const resolution = await alreadyResolvedFrom(err, orgId, itemId, options.action);
|
|
135
|
+
if (!resolution) throw err;
|
|
136
|
+
if (!resolution.matches) throw alreadyResolvedError(resolution, err);
|
|
137
|
+
if (format === 'json') console.log(JSON.stringify({ success: true, already_resolved: true, ...resolution }));
|
|
138
|
+
else if (format !== 'quiet') console.log(chalk.green(`✔ ${resolution.message}`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (result?.item?.status === 'expired') {
|
|
143
|
+
throw new QuoxUserError(result.message || 'This approval had expired, so nothing ran.', {
|
|
144
|
+
next: 're-run the original command to ask for a fresh approval.', exitCode: 1, code: 'approval_expired',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const PAST = { approve: 'approved', reject: 'rejected', deny: 'denied', defer: 'deferred', resolve: 'resolved', answer: 'answered' };
|
|
148
|
+
printSuccess(`Item ${PAST[options.action] || `${options.action} recorded`}.`);
|
|
111
149
|
});
|
|
112
150
|
|
|
151
|
+
// A respond on a closed card: the server's structured answer, else (older server) read the card.
|
|
152
|
+
async function alreadyResolvedFrom(err, orgId, givenId, action) {
|
|
153
|
+
if (!err || err.status !== 400) return null;
|
|
154
|
+
const body = err.body || {};
|
|
155
|
+
if (body.code === 'already_resolved' && body.resolution) return { ...body.resolution, message: body.message };
|
|
156
|
+
if (!/not open for response/i.test(`${body.error || ''} ${err.message || ''}`)) return null;
|
|
157
|
+
try {
|
|
158
|
+
const ep = resolveEndpoint('inbox.get', { orgId, itemId: givenId });
|
|
159
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
160
|
+
return resolutionFromItem(data.item || data, action);
|
|
161
|
+
} catch { return null; }
|
|
162
|
+
}
|
|
163
|
+
|
|
113
164
|
inbox
|
|
114
165
|
.command('stats')
|
|
115
166
|
.description('Show inbox statistics')
|
package/lib/commands/licence.js
CHANGED
|
@@ -5,53 +5,26 @@
|
|
|
5
5
|
* Phase 1 (instance-local): status reads the lease (never the key), refresh
|
|
6
6
|
* runs the renewal, seats lists/cleans band holders (dry run default).
|
|
7
7
|
*
|
|
8
|
-
* Phase 2 (owner-only
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* never sees, prints or sends key material or licence keys.
|
|
8
|
+
* Phase 2 (owner-only vendor tooling for the quox.ai website) lives in
|
|
9
|
+
* lib/vendor/licenceOwner.js and is registered only when that file is present.
|
|
10
|
+
* lib/vendor/ is excluded from the npm package (quox#1134): the public build
|
|
11
|
+
* never carries it.
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
|
-
import { writeFileSync } from 'node:fs';
|
|
16
14
|
import { formatOutput, formatDetail, printSuccess, printInfo, printWarning } from '../core/output.js';
|
|
17
15
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
18
16
|
import { QuoxValidationError } from '../core/errors.js';
|
|
19
|
-
import {
|
|
20
|
-
DEFAULT_WEBSITE_URL, DEFAULT_WEBSITE_PROFILE, normaliseWebsiteUrl, websiteLogin, websitePost,
|
|
21
|
-
saveWebsiteSession, clearWebsiteSession, getWebsiteSession,
|
|
22
|
-
} from '../core/websiteSession.js';
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const ID_LIST_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
33
|
-
|
|
34
|
-
export function parseIdList(value, flag) {
|
|
35
|
-
if (value === undefined) return undefined;
|
|
36
|
-
const ids = String(value).split(',').map((s) => s.trim()).filter(Boolean);
|
|
37
|
-
for (const id of ids) {
|
|
38
|
-
if (!ID_LIST_RE.test(id)) throw new QuoxValidationError(`${flag}: invalid plugin id "${id}"`);
|
|
39
|
-
}
|
|
40
|
-
return ids;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function parseOrgs(value) {
|
|
44
|
-
if (value === undefined) return undefined;
|
|
45
|
-
const n = Number(value);
|
|
46
|
-
if (!Number.isInteger(n) || n < 1 || n > 1000) throw new QuoxValidationError('--orgs must be an integer between 1 and 1000');
|
|
47
|
-
return n;
|
|
18
|
+
// quox#1134: the owner-only website verbs are vendor tooling, kept out of the published package (package.json
|
|
19
|
+
// "files" excludes lib/vendor/). Present in the dev tree, absent for every npm user.
|
|
20
|
+
let VENDOR_OWNER = null;
|
|
21
|
+
try {
|
|
22
|
+
VENDOR_OWNER = await import('../vendor/licenceOwner.js');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (!(e?.code === 'ERR_MODULE_NOT_FOUND' && String(e?.message || '').includes('licenceOwner'))) throw e;
|
|
48
25
|
}
|
|
49
26
|
|
|
50
|
-
function
|
|
51
|
-
return arr && arr.length ? arr.join(', ') : '(none)';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function registerLicenceCommands(program, ctx) {
|
|
27
|
+
export function registerLicenceCommands(program, ctx, { owner = VENDOR_OWNER } = {}) {
|
|
55
28
|
const { client, auth } = ctx;
|
|
56
29
|
const fetchImpl = ctx.websiteFetch || globalThis.fetch;
|
|
57
30
|
const json = () => program.opts().output === 'json';
|
|
@@ -60,8 +33,8 @@ export function registerLicenceCommands(program, ctx) {
|
|
|
60
33
|
.command('licence')
|
|
61
34
|
.alias('license')
|
|
62
35
|
.description('Instance licence status, lease refresh and seat bands')
|
|
63
|
-
.addHelpText('after', OWNER_HELP)
|
|
64
36
|
.action(() => { licence.help(); });
|
|
37
|
+
if (owner) licence.addHelpText('after', owner.OWNER_HELP);
|
|
65
38
|
|
|
66
39
|
licence
|
|
67
40
|
.command('status')
|
|
@@ -162,119 +135,5 @@ export function registerLicenceCommands(program, ctx) {
|
|
|
162
135
|
}
|
|
163
136
|
});
|
|
164
137
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
licence
|
|
168
|
-
.command('website-login')
|
|
169
|
-
.description('Log in to the quox.ai website admin API for reissue/catalogue-sign (stores the session cookie only, 0600)')
|
|
170
|
-
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
171
|
-
.option('--url <url>', 'website base URL', DEFAULT_WEBSITE_URL)
|
|
172
|
-
.option('--username <name>', 'website admin username')
|
|
173
|
-
.option('--password-stdin', 'read the password from stdin (one line) instead of prompting')
|
|
174
|
-
.action(async (opts) => {
|
|
175
|
-
normaliseWebsiteUrl(opts.url); // refuse a bad or plain-http URL before asking for a password
|
|
176
|
-
let { username } = opts;
|
|
177
|
-
let password;
|
|
178
|
-
if (opts.passwordStdin) {
|
|
179
|
-
const chunks = [];
|
|
180
|
-
for await (const c of process.stdin) chunks.push(Buffer.from(c));
|
|
181
|
-
password = Buffer.concat(chunks).toString('utf8').split('\n')[0].trim();
|
|
182
|
-
}
|
|
183
|
-
if (!username || !password) {
|
|
184
|
-
const { default: inquirer } = await import('inquirer');
|
|
185
|
-
if (!username) username = (await inquirer.prompt([{ type: 'input', name: 'u', message: 'Website admin username:' }])).u;
|
|
186
|
-
if (!password) password = (await inquirer.prompt([{ type: 'password', name: 'p', message: 'Password:', mask: '*' }])).p;
|
|
187
|
-
}
|
|
188
|
-
if (!username || !password) throw new QuoxValidationError('username and password are required');
|
|
189
|
-
const session = await websiteLogin({ url: opts.url, username, password, fetchImpl });
|
|
190
|
-
password = null;
|
|
191
|
-
saveWebsiteSession(opts.websiteProfile, session);
|
|
192
|
-
if (json()) { console.log(JSON.stringify({ profile: opts.websiteProfile, url: session.url, username: session.username, must_change_password: session.must_change_password }, null, 2)); return; }
|
|
193
|
-
printSuccess(`Website session saved for ${session.username} at ${session.url} (profile ${opts.websiteProfile}, valid ~24h). Password not stored.`);
|
|
194
|
-
if (session.must_change_password) printWarning('This admin must change their password on the website before licence operations will be allowed (HTTP 403 until then).');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
licence
|
|
198
|
-
.command('website-logout')
|
|
199
|
-
.description('End the website admin session and delete the stored cookie')
|
|
200
|
-
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
201
|
-
.action(async (opts) => {
|
|
202
|
-
if (getWebsiteSession(opts.websiteProfile)) {
|
|
203
|
-
try { await websitePost(opts.websiteProfile, '/api/admin/logout', {}, { fetchImpl }); } catch { /* session may already be dead */ }
|
|
204
|
-
}
|
|
205
|
-
const had = clearWebsiteSession(opts.websiteProfile);
|
|
206
|
-
printSuccess(had ? `Website session ${opts.websiteProfile} removed.` : `No website session ${opts.websiteProfile} was stored.`);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
licence
|
|
210
|
-
.command('reissue')
|
|
211
|
-
.description('Owner-only: band bare paid ids and add ids on a licence record, re-signed on the website (dry run unless --apply)')
|
|
212
|
-
.argument('<licenseId>', 'licence id, e.g. lic_abc123')
|
|
213
|
-
.option('--orgs <n>', 'org band for every banded/added id (default 10)')
|
|
214
|
-
.option('--add <ids>', 'comma-separated ids to ADD as <id>@orgs=N')
|
|
215
|
-
.option('--band <ids>', 'comma-separated ids already present to FORCE-band')
|
|
216
|
-
.option('--apply', 'actually reissue (otherwise dry run, nothing changes)')
|
|
217
|
-
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
218
|
-
.action(async (licenseId, opts) => {
|
|
219
|
-
const body = { dryRun: !opts.apply };
|
|
220
|
-
const orgs = parseOrgs(opts.orgs);
|
|
221
|
-
if (orgs !== undefined) body.orgs = orgs;
|
|
222
|
-
const add = parseIdList(opts.add, '--add');
|
|
223
|
-
if (add) body.add = add;
|
|
224
|
-
const band = parseIdList(opts.band, '--band');
|
|
225
|
-
if (band) body.band = band;
|
|
226
|
-
|
|
227
|
-
const d = await websitePost(opts.websiteProfile, `/api/admin/licenses/${encodeURIComponent(licenseId)}/reissue`, body, { fetchImpl });
|
|
228
|
-
if (json()) { console.log(JSON.stringify(d, null, 2)); return; }
|
|
229
|
-
|
|
230
|
-
printInfo(`Licence ${d.license_id}${d.customer_email ? ` (${d.customer_email})` : ''}: max_servers ${d.max_servers ?? '?'}, expires ${d.expires_at || 'never'}`);
|
|
231
|
-
const removed = new Set(d.diff?.removed || []);
|
|
232
|
-
const added = new Set(d.diff?.added || []);
|
|
233
|
-
for (const e of d.current || []) console.log(` ${removed.has(e) ? '-' : ' '} ${e}`);
|
|
234
|
-
for (const e of d.next || []) if (added.has(e)) console.log(` + ${e}`);
|
|
235
|
-
if (d.unknown_ids?.length) printWarning(`Not issuable (unknown to the signed catalogue), apply will be refused: ${d.unknown_ids.join(', ')}`);
|
|
236
|
-
|
|
237
|
-
if (!d.changed) { printSuccess('No change needed: the licence already has these entitlements.'); return; }
|
|
238
|
-
if (d.dry_run) { printInfo('DRY RUN: nothing changed. Re-run with --apply to reissue.'); return; }
|
|
239
|
-
printSuccess(`Reissued ${d.license_id}. Audit/backup id: ${d.backup_id}`);
|
|
240
|
-
printInfo('Instances pick this up on their next lease renewal (~6h). To pull it now, on the instance: quox licence refresh');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
licence
|
|
244
|
-
.command('catalogue-sign')
|
|
245
|
-
.alias('catalog-sign')
|
|
246
|
-
.description('Owner-only: re-sign the product catalogue with the live key on the website (dry run unless --apply)')
|
|
247
|
-
.option('--apply', 'actually sign and publish to the website API (otherwise dry run)')
|
|
248
|
-
.option('--out <path>', 'after --apply, save the signed catalogue (public artifact) here for committing')
|
|
249
|
-
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
250
|
-
.action(async (opts) => {
|
|
251
|
-
if (opts.out && !opts.apply) throw new QuoxValidationError('--out is only meaningful with --apply');
|
|
252
|
-
const d = await websitePost(opts.websiteProfile, '/api/admin/catalog/sign', { dryRun: !opts.apply }, { fetchImpl });
|
|
253
|
-
if (opts.out && d.artifact) writeFileSync(opts.out, JSON.stringify(d.artifact, null, 2) + '\n', 'utf8');
|
|
254
|
-
if (json()) {
|
|
255
|
-
const { artifact, ...rest } = d;
|
|
256
|
-
console.log(JSON.stringify({ ...rest, artifact_saved_to: opts.out && artifact ? opts.out : null }, null, 2));
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
const p = d.published || {};
|
|
260
|
-
formatDetail({
|
|
261
|
-
published: p.exists ? `${p.verified ? 'verified' : 'NOT VERIFIED'}, sha256 ${p.sha256 ? p.sha256.slice(0, 16) : '?'}..., ${p.generated_at || '?'}` : '(none)',
|
|
262
|
-
products: `${p.product_count ?? 0} -> ${d.candidate?.product_count ?? '?'}`,
|
|
263
|
-
governed: `${p.governed_count ?? 0} -> ${d.candidate?.governed_count ?? '?'}`,
|
|
264
|
-
products_added: list(d.diff?.products_added),
|
|
265
|
-
products_removed: list(d.diff?.products_removed),
|
|
266
|
-
products_changed: list(d.diff?.products_changed),
|
|
267
|
-
governed_added: list(d.diff?.governed_added),
|
|
268
|
-
governed_removed: list(d.diff?.governed_removed),
|
|
269
|
-
signing_key: d.signing ? (d.signing.trusted ? `trusted (${d.signing.trusted_key_prefix}...)` : (d.signing.configured ? 'NOT the trusted key: apply will be refused' : 'not configured: apply will be refused')) : '?',
|
|
270
|
-
}, { title: d.dry_run ? 'Catalogue sign (DRY RUN)' : 'Catalogue sign' });
|
|
271
|
-
if (d.diff?.governed_added?.length) {
|
|
272
|
-
printWarning(`Newly governed: ${d.diff.governed_added.join(', ')}. Reissue the owner licence with these ids (quox licence reissue <id> --add ...) and refresh its lease BEFORE instances install this catalogue, or they lock out.`);
|
|
273
|
-
}
|
|
274
|
-
if (!d.changed) { printSuccess('No change: the published catalogue already matches.'); return; }
|
|
275
|
-
if (d.dry_run) { printInfo('DRY RUN: nothing signed. Re-run with --apply to sign.'); return; }
|
|
276
|
-
printSuccess(`Catalogue signed. sha256 ${d.sha256}, governed ${d.governed_count}, audit id ${d.audit_id}`);
|
|
277
|
-
if (opts.out) printInfo(`Signed catalogue saved to ${opts.out}.`);
|
|
278
|
-
printInfo('The website container copy does not survive a rebuild: commit the artifact to quox-website api/data/signed-catalog.json and quox-dashboard services/auth/data/signed-catalog.json (use --out to save it).');
|
|
279
|
-
});
|
|
138
|
+
if (owner) owner.registerLicenceOwnerCommands(licence, { json, fetchImpl });
|
|
280
139
|
}
|