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/agent.js
CHANGED
|
@@ -2,41 +2,143 @@
|
|
|
2
2
|
* Agent commands: CRUD, activate/deactivate, tool assignments
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
5
6
|
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
6
7
|
import { resolveOrgId } from './helpers.js';
|
|
7
8
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
|
+
import { QuoxApiError } from '../core/errors.js';
|
|
10
|
+
import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
|
|
8
11
|
import { registerAgentAttachCommands } from './agentAttach.js';
|
|
9
12
|
|
|
13
|
+
// quox#1169: the prompt, domains and parent a user gives their own agent. Auth reads
|
|
14
|
+
// system_prompt_additions, domains[] and parent_agent_id; it has never read manifest_id.
|
|
15
|
+
function agentPromptFields(options) {
|
|
16
|
+
if (options.prompt !== undefined && options.promptFile !== undefined) {
|
|
17
|
+
throw new Error('Give either --prompt or --prompt-file, not both. Nothing was sent.');
|
|
18
|
+
}
|
|
19
|
+
if (options.manifestId !== undefined) {
|
|
20
|
+
throw new Error('--manifest-id is not supported by the server (it was always ignored). Nothing was sent.');
|
|
21
|
+
}
|
|
22
|
+
const out = {};
|
|
23
|
+
if (options.prompt !== undefined) out.system_prompt_additions = String(options.prompt);
|
|
24
|
+
if (options.promptFile !== undefined) {
|
|
25
|
+
try {
|
|
26
|
+
out.system_prompt_additions = readFileSync(options.promptFile, 'utf8');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error(`cannot read --prompt-file ${options.promptFile}: ${err.code || err.message}. Nothing was sent.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (options.domain !== undefined) out.domains = String(options.domain).split(',').map((d) => d.trim()).filter(Boolean);
|
|
32
|
+
if (options.parent !== undefined) out.parent_agent_id = options.parent;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
export function registerAgentCommands(program, ctx) {
|
|
11
37
|
const { client, auth } = ctx;
|
|
12
38
|
|
|
39
|
+
// Registered specialists (no org row): resolved from the collector registry, quox#927.
|
|
40
|
+
async function registryAgent(agentId) {
|
|
41
|
+
try {
|
|
42
|
+
const ep = resolveEndpoint('v1.agents.registry');
|
|
43
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
44
|
+
return (data?.agents || []).find((r) => r.agent_id === agentId) || null;
|
|
45
|
+
} catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
async function registryGrants(agentId) {
|
|
48
|
+
try {
|
|
49
|
+
const ep = resolveEndpoint('v1.tools.agents.list');
|
|
50
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
51
|
+
const list = data?.agentTools?.[agentId];
|
|
52
|
+
return Array.isArray(list) ? list : null;
|
|
53
|
+
} catch { return null; }
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
const agent = program
|
|
14
57
|
.command('agent')
|
|
15
58
|
.description('Manage agents');
|
|
16
59
|
|
|
17
60
|
agent
|
|
18
61
|
.command('list')
|
|
19
|
-
.description('List agents')
|
|
20
|
-
.option('--status <status>', 'filter by status')
|
|
62
|
+
.description('List registered specialist agents and the org\'s own agents')
|
|
63
|
+
.option('--status <status>', 'filter by status (org agents only)')
|
|
21
64
|
.option('--level <level>', 'filter by level (0-4)')
|
|
65
|
+
.option('--source <source>', 'registry | org | all', 'all')
|
|
66
|
+
.option('--plugin <id>', 'only agents that require this plugin')
|
|
67
|
+
.option('--search <text>', 'substring match on id, name or description')
|
|
22
68
|
.action(async (options) => {
|
|
23
69
|
auth.requireAuth();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
const source = String(options.source || 'all').toLowerCase();
|
|
71
|
+
if (!['registry', 'org', 'all'].includes(source)) {
|
|
72
|
+
throw new Error(`--source must be registry, org or all (got "${options.source}")`);
|
|
73
|
+
}
|
|
74
|
+
const notes = [];
|
|
75
|
+
|
|
76
|
+
// Registered specialists (server roster; bundled roster if the server lacks the route).
|
|
77
|
+
let registry = [];
|
|
78
|
+
if (source !== 'org') {
|
|
79
|
+
try {
|
|
80
|
+
const ep = resolveEndpoint('v1.agents.registry');
|
|
81
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
82
|
+
if (!Array.isArray(data?.agents)) throw new Error('unexpected response');
|
|
83
|
+
registry = data.agents.map(r => ({ ...r, source: 'registry' }));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
notes.push(`note: server registry unavailable (${err.message}); showing the bundled roster, which may be out of date.`);
|
|
86
|
+
registry = loadBundledAssistantsRoster().map(r => ({
|
|
87
|
+
agent_id: r.id, name: r.name || r.id, level: null, requires_plugin: null, tool_count: null, source: 'registry',
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The org's own agent rows.
|
|
93
|
+
let orgRows = [];
|
|
94
|
+
if (source !== 'registry') {
|
|
95
|
+
try {
|
|
96
|
+
const orgId = resolveOrgId(auth, program);
|
|
97
|
+
const query = { includeTools: 'true' };
|
|
98
|
+
if (options.status) query.status = options.status;
|
|
99
|
+
if (options.level) query.level = options.level;
|
|
100
|
+
const ep = resolveEndpoint('agents.list', { orgId });
|
|
101
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
102
|
+
orgRows = (data.agents || data || []).map(r => ({ ...r, source: 'org' }));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (source === 'org') throw err;
|
|
105
|
+
notes.push(`note: org agents unavailable (${err.message}); showing registered specialists only.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Merge by agent_id: org row wins for status, registry supplies plugin/description.
|
|
110
|
+
const merged = new Map();
|
|
111
|
+
for (const r of registry) merged.set(r.agent_id, r);
|
|
112
|
+
for (const o of orgRows) {
|
|
113
|
+
const key = o.agent_id || o.id;
|
|
114
|
+
const reg = merged.get(key);
|
|
115
|
+
merged.set(key, reg
|
|
116
|
+
? { ...reg, ...o, requires_plugin: reg.requires_plugin, description: reg.description ?? o.description, tool_count: o.tool_count ?? reg.tool_count, source: 'org+registry' }
|
|
117
|
+
: o);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let all = [...merged.values()];
|
|
121
|
+
if (options.level !== undefined) all = all.filter(r => String(r.level) === String(options.level));
|
|
122
|
+
if (options.status) all = all.filter(r => r.status === options.status);
|
|
123
|
+
if (options.plugin) all = all.filter(r => r.requires_plugin === options.plugin);
|
|
124
|
+
if (options.search) {
|
|
125
|
+
const q = options.search.toLowerCase();
|
|
126
|
+
all = all.filter(r => [r.agent_id, r.name, r.description].some(v => String(v ?? '').toLowerCase().includes(q)));
|
|
127
|
+
}
|
|
128
|
+
all.sort((x, y) => String(x.agent_id).localeCompare(String(y.agent_id)));
|
|
129
|
+
|
|
130
|
+
for (const n of notes) console.error(n);
|
|
131
|
+
const dash = (v) => (v === null || v === undefined || v === '' ? '-' : v);
|
|
32
132
|
formatOutput({
|
|
33
|
-
columns: ['
|
|
34
|
-
headers: ['
|
|
35
|
-
rows:
|
|
36
|
-
...
|
|
37
|
-
|
|
133
|
+
columns: ['agent_id', 'name', 'source', 'level', 'state', 'tool_count'],
|
|
134
|
+
headers: ['Agent ID', 'Name', 'Source', 'Level', 'Status/Plugin', 'Tools'],
|
|
135
|
+
rows: all.map(r => ({
|
|
136
|
+
...r,
|
|
137
|
+
level: dash(r.level),
|
|
138
|
+
state: r.status ? r.status : (r.requires_plugin ? `plugin:${r.requires_plugin}` : '-'),
|
|
139
|
+
tool_count: dash(r.tool_count),
|
|
38
140
|
})),
|
|
39
|
-
}, { format: program.opts().output });
|
|
141
|
+
}, { format: program.opts().output, raw: all, emptyMessage: 'No agents match.' });
|
|
40
142
|
});
|
|
41
143
|
|
|
42
144
|
agent
|
|
@@ -46,7 +148,16 @@ export function registerAgentCommands(program, ctx) {
|
|
|
46
148
|
.action(async (agentId) => {
|
|
47
149
|
auth.requireAuth();
|
|
48
150
|
const orgId = resolveOrgId(auth, program);
|
|
49
|
-
|
|
151
|
+
let data;
|
|
152
|
+
try {
|
|
153
|
+
data = await client.get(resolveEndpoint('agents.get', { orgId, agentId }).path, { service: resolveEndpoint('agents.get', { orgId, agentId }).service });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// quox#927: `agent list` also shows registered specialists that have no org row;
|
|
156
|
+
// resolve those from the registry instead of answering 404 for an id list printed.
|
|
157
|
+
const reg = err?.status === 404 ? await registryAgent(agentId) : null;
|
|
158
|
+
if (!reg) throw err;
|
|
159
|
+
data = { agent: { ...reg, source: 'registry' } };
|
|
160
|
+
}
|
|
50
161
|
formatDetail(data.agent || data, {
|
|
51
162
|
format: program.opts().output,
|
|
52
163
|
title: data.agent?.name || data.name,
|
|
@@ -62,19 +173,22 @@ export function registerAgentCommands(program, ctx) {
|
|
|
62
173
|
.option('--domain <domain>', 'agent domain')
|
|
63
174
|
.option('--description <desc>', 'description')
|
|
64
175
|
.option('--parent <parentId>', 'parent agent ID')
|
|
65
|
-
.option('--manifest-id <id>', '
|
|
176
|
+
.option('--manifest-id <id>', 'not supported: refused (the server never read it)')
|
|
177
|
+
.option('--prompt <text>', "the agent's instructions (its system prompt)")
|
|
178
|
+
.option('--prompt-file <path>', "read the agent's instructions from a file")
|
|
66
179
|
.action(async (options) => {
|
|
67
180
|
auth.requireAuth();
|
|
68
181
|
const orgId = resolveOrgId(auth, program);
|
|
182
|
+
const fields = agentPromptFields(options);
|
|
69
183
|
const body = {
|
|
70
184
|
name: options.name,
|
|
71
185
|
level: parseInt(options.level, 10),
|
|
186
|
+
...fields,
|
|
72
187
|
};
|
|
73
188
|
if (options.agentId) body.agent_id = options.agentId;
|
|
189
|
+
// legacy key kept so older servers and tests see it; auth reads domains[] (set above)
|
|
74
190
|
if (options.domain) body.domain = options.domain;
|
|
75
191
|
if (options.description) body.description = options.description;
|
|
76
|
-
if (options.parent) body.parent_id = options.parent;
|
|
77
|
-
if (options.manifestId) body.manifest_id = options.manifestId;
|
|
78
192
|
await client.post(resolveEndpoint('agents.create', { orgId }).path, { service: resolveEndpoint('agents.create', { orgId }).service, body });
|
|
79
193
|
printSuccess(`Agent "${options.name}" created.`);
|
|
80
194
|
});
|
|
@@ -87,10 +201,14 @@ export function registerAgentCommands(program, ctx) {
|
|
|
87
201
|
.option('--description <desc>', 'new description')
|
|
88
202
|
.option('--level <level>', 'new level')
|
|
89
203
|
.option('--status <status>', 'new status')
|
|
204
|
+
.option('--prompt <text>', "new instructions (system prompt)")
|
|
205
|
+
.option('--prompt-file <path>', 'read new instructions from a file')
|
|
206
|
+
.option('--domain <domains>', 'comma-separated domains')
|
|
207
|
+
.option('--parent <parentId>', 'parent agent ID')
|
|
90
208
|
.action(async (agentId, options) => {
|
|
91
209
|
auth.requireAuth();
|
|
92
210
|
const orgId = resolveOrgId(auth, program);
|
|
93
|
-
const body = {};
|
|
211
|
+
const body = { ...agentPromptFields(options) };
|
|
94
212
|
if (options.name) body.name = options.name;
|
|
95
213
|
if (options.description) body.description = options.description;
|
|
96
214
|
if (options.level) body.level = parseInt(options.level, 10);
|
|
@@ -133,7 +251,7 @@ export function registerAgentCommands(program, ctx) {
|
|
|
133
251
|
const orgId = resolveOrgId(auth, program);
|
|
134
252
|
await client.patch(resolveEndpoint('agents.update', { orgId, agentId }).path, {
|
|
135
253
|
service: resolveEndpoint('agents.update', { orgId, agentId }).service,
|
|
136
|
-
body: { status: '
|
|
254
|
+
body: { status: 'paused' }, // #985: auth accepts draft|active|paused|retired; 'inactive' 400'd
|
|
137
255
|
});
|
|
138
256
|
printSuccess('Agent deactivated.');
|
|
139
257
|
});
|
|
@@ -150,7 +268,15 @@ export function registerAgentCommands(program, ctx) {
|
|
|
150
268
|
.action(async (agentId) => {
|
|
151
269
|
auth.requireAuth();
|
|
152
270
|
const orgId = resolveOrgId(auth, program);
|
|
153
|
-
|
|
271
|
+
let data;
|
|
272
|
+
try {
|
|
273
|
+
data = await client.get(resolveEndpoint('agents.tools.list', { orgId, agentId }).path, { service: resolveEndpoint('agents.tools.list', { orgId, agentId }).service });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// quox#927: a registered specialist's tools are its registry grants, not org assignments.
|
|
276
|
+
const grants = err?.status === 404 ? await registryGrants(agentId) : null;
|
|
277
|
+
if (!grants) throw err;
|
|
278
|
+
data = { tools: grants.map((name) => ({ tool_id: name, tool_name: name, category: '-', access_mode: 'registry grant', enabled: true })) };
|
|
279
|
+
}
|
|
154
280
|
formatOutput({
|
|
155
281
|
// fleet #392: assignment rows carry tool_name/category/enabled.
|
|
156
282
|
columns: ['tool_id', 'tool_name', 'category', 'access_mode', 'enabled'],
|
|
@@ -168,10 +294,20 @@ export function registerAgentCommands(program, ctx) {
|
|
|
168
294
|
.action(async (agentId, options) => {
|
|
169
295
|
auth.requireAuth();
|
|
170
296
|
const orgId = resolveOrgId(auth, program);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
297
|
+
try {
|
|
298
|
+
await client.post(resolveEndpoint('agents.tools.create', { orgId, agentId }).path, {
|
|
299
|
+
service: resolveEndpoint('agents.tools.create', { orgId, agentId }).service,
|
|
300
|
+
body: { tool_id: options.tool, access_mode: options.accessMode },
|
|
301
|
+
});
|
|
302
|
+
} catch (err) {
|
|
303
|
+
// quox#1072: a built-in agent's tools come from its registry grant and
|
|
304
|
+
// hat. The server says so in prose; surface that sentence as the error
|
|
305
|
+
// with a stable code, so it does not read as a generic conflict.
|
|
306
|
+
if (err?.status === 409 && err.body?.code === 'BUILTIN_AGENT_TOOLS_FROM_GRANT') {
|
|
307
|
+
throw new QuoxApiError(409, err.body.code, err.body.message || err.body.error, undefined, err.body);
|
|
308
|
+
}
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
175
311
|
printSuccess(`Tool assigned to agent.`);
|
|
176
312
|
});
|
|
177
313
|
|
|
@@ -699,6 +699,36 @@ export async function heartbeatAttachLease(client, agentId, { getOrgId } = {}) {
|
|
|
699
699
|
return { leaseId, expires: newExpiry || 'unchanged' };
|
|
700
700
|
}
|
|
701
701
|
|
|
702
|
+
/**
|
|
703
|
+
* #893: re-lease an attach that LAPSED (expired while heartbeats could not get
|
|
704
|
+
* through, e.g. an auth stall longer than the TTL). Never takes over: a 409
|
|
705
|
+
* here means another session holds the agent, and the keeper then exits as
|
|
706
|
+
* before. Rewrites only the token, lease id and expiry; the installed kit
|
|
707
|
+
* (prompt, tools, memory) is unchanged.
|
|
708
|
+
*/
|
|
709
|
+
const leaseMetaPath = (agentId) => join(dirname(tokenPath(agentId)), 'lease-meta.json');
|
|
710
|
+
|
|
711
|
+
export async function reacquireAttachLease(client, agentId) {
|
|
712
|
+
const stored = readStoredKit(agentId);
|
|
713
|
+
if (!stored?.lease?.id) throw new QuoxApiError(404, 'no_local_lease', `No local attach state for "${agentId}"`);
|
|
714
|
+
let meta = {};
|
|
715
|
+
try { meta = JSON.parse(readFileSync(leaseMetaPath(agentId), 'utf-8')); } catch { /* defaults */ }
|
|
716
|
+
const ep = resolveEndpoint('agent-attach.lease.create', { agentId });
|
|
717
|
+
const data = await client.post(ep.path, {
|
|
718
|
+
service: ep.service,
|
|
719
|
+
body: {
|
|
720
|
+
session_label: meta.session_label || agentId,
|
|
721
|
+
ttl_seconds: meta.ttl_seconds || 900,
|
|
722
|
+
takeover: false,
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
if (!data?.token || !data?.lease?.id) throw new Error('re-lease returned no token or lease id');
|
|
726
|
+
writeFile0600(tokenPath(agentId), `${data.token}\n`);
|
|
727
|
+
stored.lease = { ...stored.lease, id: data.lease.id, expires_at: data.lease.expires_at || data.expires_at || null };
|
|
728
|
+
writeFile0600(kitPath(agentId), `${JSON.stringify(stored, null, 2)}\n`);
|
|
729
|
+
return { leaseId: data.lease.id };
|
|
730
|
+
}
|
|
731
|
+
|
|
702
732
|
/**
|
|
703
733
|
* P10: keep the bound slot (or this session's membership of it) alive with the
|
|
704
734
|
* ATTACH token. Failures are logged, never fatal to the lease heartbeat.
|
|
@@ -897,15 +927,36 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
897
927
|
});
|
|
898
928
|
} catch (err) {
|
|
899
929
|
const status = err instanceof QuoxApiError ? err.status : 'network error';
|
|
930
|
+
// quox#1026 (b): the kit cannot resolve this agent at all, so no retry can succeed. Release the lease
|
|
931
|
+
// just granted (never leave it held) and say plainly which agents are attachable.
|
|
932
|
+
if (err instanceof QuoxApiError && err.status === 404 && err.code === 'unknown_agent' && lease.id) {
|
|
933
|
+
let released = false;
|
|
934
|
+
try {
|
|
935
|
+
const relEp = resolveEndpoint('agent-attach.lease.release', { leaseId: lease.id });
|
|
936
|
+
await client.post(relEp.path, { service: relEp.service, body: {} });
|
|
937
|
+
released = true;
|
|
938
|
+
} catch { /* reported below */ }
|
|
939
|
+
throw new Error(
|
|
940
|
+
`"${agentId}" is not attachable: only core registry agents and custom assistants can be attached `
|
|
941
|
+
+ '(an agent made with `quox agent create` has no attach kit yet, quox#1026). '
|
|
942
|
+
+ (released ? `The lease ${lease.id} was released.` : `Releasing lease ${lease.id} failed: run \`quox agent detach ${agentId} --lease ${lease.id}\`.`)
|
|
943
|
+
);
|
|
944
|
+
}
|
|
900
945
|
throw new Error(
|
|
901
946
|
`GET ${kitEp.path} failed (${status}) after the lease was granted (lease ${lease.id || 'unknown'}). `
|
|
902
|
-
+ `The lease is still active: retry the kit fetch, or run \`quox agent detach ${agentId}\` to release it. `
|
|
947
|
+
+ `The lease is still active: retry the kit fetch, or run \`quox agent detach ${agentId}${lease.id ? ` --lease ${lease.id}` : ''}\` to release it. `
|
|
903
948
|
+ `Original error: ${err.message}`
|
|
904
949
|
);
|
|
905
950
|
}
|
|
906
951
|
|
|
907
952
|
const splinter = splinterInfoFrom(leaseData?.splinter);
|
|
908
953
|
const paths = installAttachKit(agentId, { kit, token, splinter });
|
|
954
|
+
// #893: remember the label and TTL (beside the token, not in kit.json,
|
|
955
|
+
// which stays exactly what the server sent) so the keeper can re-lease a
|
|
956
|
+
// lapsed attach under the same identity.
|
|
957
|
+
try {
|
|
958
|
+
writeFile0600(leaseMetaPath(agentId), `${JSON.stringify({ session_label: attachLabel, ttl_seconds: options.ttl })}\n`);
|
|
959
|
+
} catch { /* the attach stands; a re-lease falls back to defaults */ }
|
|
909
960
|
// ONE-BRAIN CX4: agents whose resolved tool list carries brain_context get
|
|
910
961
|
// a primer from the brain. Best effort, bounded, never fails the attach.
|
|
911
962
|
let primer = '';
|
|
@@ -1074,6 +1125,7 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1074
1125
|
.option('--handoff <file>', 'path to a file with a handoff note (state, open threads, next moves) to store before detaching')
|
|
1075
1126
|
.option('--handoff-text <text>', 'handoff note text, provided inline instead of a file')
|
|
1076
1127
|
.option('--force-detach', 'release the lease even if storing the handoff note fails')
|
|
1128
|
+
.option('--lease <leaseId>', 'release this lease when there is no local attach kit (an attach that failed after its lease was granted); the server allows only its holder or an org admin')
|
|
1077
1129
|
.option('--no-brain-note', 'do not also save the handoff to the knowledge brain as a private note')
|
|
1078
1130
|
.action(async (agentId, options) => {
|
|
1079
1131
|
auth.requireAuth();
|
|
@@ -1081,7 +1133,20 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1081
1133
|
agentId = await resolveAgentId(client, agentId);
|
|
1082
1134
|
const stored = readStoredKit(agentId);
|
|
1083
1135
|
const leaseId = stored?.lease?.id;
|
|
1084
|
-
|
|
1136
|
+
// #1003: no local kit (attach failed after the lease was granted). Release exactly the lease the caller
|
|
1137
|
+
// names, never a guessed one: a lease held by another session must not be released by inference.
|
|
1138
|
+
if (!leaseId && options.lease) {
|
|
1139
|
+
const relEp = resolveEndpoint('agent-attach.lease.release', { leaseId: options.lease });
|
|
1140
|
+
await client.post(relEp.path, { service: relEp.service, body: {} });
|
|
1141
|
+
removeAttachDir(agentId);
|
|
1142
|
+
printEvent(events.released('quox', `attach ${agentId}`, `lease ${String(options.lease).slice(0, 8)} (no local kit)`));
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (!leaseId) {
|
|
1146
|
+
throw new Error(`${noLocalLeaseError(agentId, 'attach-status').message} `
|
|
1147
|
+
+ `If an attach failed after its lease was granted, release that lease with \`quox agent detach ${agentId} --lease <leaseId>\` `
|
|
1148
|
+
+ `(the lease id is in the attach error and in \`quox agent attach-status ${agentId}\`).`);
|
|
1149
|
+
}
|
|
1085
1150
|
|
|
1086
1151
|
if (options.handoff && options.handoffText) {
|
|
1087
1152
|
throw new QuoxError('Pass only one of --handoff or --handoff-text.', 2);
|
|
@@ -1173,6 +1238,7 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1173
1238
|
sleep: ctx.sleep,
|
|
1174
1239
|
dir: ctx.keeperDir,
|
|
1175
1240
|
heartbeat: () => heartbeatAttachLease(client, agentId, { getOrgId: () => resolveOrgId(auth, program) }),
|
|
1241
|
+
reacquire: () => reacquireAttachLease(client, agentId),
|
|
1176
1242
|
release: async () => {
|
|
1177
1243
|
const leaseId = readStoredKit(agentId)?.lease?.id;
|
|
1178
1244
|
if (!leaseId) return;
|
|
@@ -1213,10 +1279,10 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1213
1279
|
// the route deployed) degrades to silence, not an error.
|
|
1214
1280
|
try {
|
|
1215
1281
|
const bindingEp = resolveEndpoint('agent-attach.slot-binding.get', { agentId });
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
}
|
|
1282
|
+
// quox#1308: the route answers { binding: { slot_name, ... } }; this read .name off the body and never printed.
|
|
1283
|
+
const res = await client.get(bindingEp.path, { service: bindingEp.service });
|
|
1284
|
+
const slotName = res?.binding?.slot_name;
|
|
1285
|
+
if (slotName) printInfo(` slot: ${slotName}`);
|
|
1220
1286
|
} catch {
|
|
1221
1287
|
// No binding, or the route isn't deployed yet: not a status-command failure.
|
|
1222
1288
|
}
|
|
@@ -1328,6 +1394,28 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1328
1394
|
if (program.opts().output === 'json') formatDetail(data, { format: 'json' });
|
|
1329
1395
|
});
|
|
1330
1396
|
|
|
1397
|
+
// quox#1308: bind-slot was write-only. Reading an address is open to any org member (the GET route has no
|
|
1398
|
+
// admin gate). No binding is an answer, not a failure: exit 0, and --json prints { binding: null }.
|
|
1399
|
+
agent
|
|
1400
|
+
.command('slot')
|
|
1401
|
+
.description("Show which slot an agent is bound to (the only slot a user-made agent may post to)")
|
|
1402
|
+
.argument('<agentId>', 'registry agent id')
|
|
1403
|
+
.action(async (agentId) => {
|
|
1404
|
+
auth.requireAuth();
|
|
1405
|
+
const ep = resolveEndpoint('agent-attach.slot-binding.get', { agentId });
|
|
1406
|
+
let binding = null;
|
|
1407
|
+
try {
|
|
1408
|
+
const res = await client.get(ep.path, { service: ep.service });
|
|
1409
|
+
binding = res?.binding || null;
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
const status = err instanceof QuoxApiError ? err.status : 'network error';
|
|
1412
|
+
if (!(status === 404 && err.code === 'no_binding')) throw new Error(`GET ${ep.path} failed (${status}): ${err.message}`);
|
|
1413
|
+
}
|
|
1414
|
+
if (program.opts().output === 'json') { formatDetail({ binding }, { format: 'json' }); return; }
|
|
1415
|
+
if (!binding) { printInfo(`${agentId} has no slot binding. An org admin can bind one: quox agent bind-slot ${agentId} <slot>`); return; }
|
|
1416
|
+
printInfo(`${agentId} is bound to slot ${binding.slot_name}${binding.created_at ? ` (since ${binding.created_at})` : ''}`);
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1331
1419
|
agent
|
|
1332
1420
|
.command('unbind-slot')
|
|
1333
1421
|
.description("Remove an agent identity's slot binding (org admin only)")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Approvals commands: result (quox#791)
|
|
2
|
+
* Approvals commands: result (quox#791), grants list|create|revoke (fleet standing grants, 2026-10-13)
|
|
3
3
|
*
|
|
4
4
|
* After a human approves a held tool call (e.g. a gated ssh_exec) the collector
|
|
5
5
|
* runs it and stores the outcome. `quox approvals result <id>` reads it back.
|
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
9
|
|
|
10
|
+
import { attachWalkthrough } from './walkthrough.js';
|
|
11
|
+
|
|
10
12
|
export function registerApprovalsCommands(program, ctx) {
|
|
11
13
|
const { client, auth } = ctx;
|
|
12
14
|
|
|
13
15
|
const approvals = program
|
|
14
16
|
.command('approvals')
|
|
15
17
|
.description('Tool approvals (held actions awaiting or past a human decision)');
|
|
18
|
+
attachWalkthrough(approvals, ctx, program);
|
|
16
19
|
|
|
17
20
|
approvals
|
|
18
21
|
.command('result <approvalId>')
|
|
@@ -44,4 +47,73 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
44
47
|
if (out !== undefined) console.log(typeof out === 'string' ? out : JSON.stringify(out, null, 2));
|
|
45
48
|
else console.log(JSON.stringify(r, null, 2));
|
|
46
49
|
});
|
|
50
|
+
|
|
51
|
+
// Fleet standing grants: an org admin pre-approves ONE host + an anchored command
|
|
52
|
+
// shape + a VMID range until an expiry, for ssh_exec. Every use still lands in
|
|
53
|
+
// approvals history with an AEE envelope; anything outside the grant files HITL.
|
|
54
|
+
const grants = approvals
|
|
55
|
+
.command('grants')
|
|
56
|
+
.description('Standing grants: org-admin pre-approved fleet command lanes (ssh_exec)');
|
|
57
|
+
|
|
58
|
+
grants
|
|
59
|
+
.command('list')
|
|
60
|
+
.description('List active standing grants (--all includes revoked and expired)')
|
|
61
|
+
.option('--all', 'include revoked and expired grants')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
auth.requireAuth();
|
|
64
|
+
const ep = resolveEndpoint('v1.approvals.grants.list');
|
|
65
|
+
const data = await client.get(ep.path, { service: ep.service, ...(options.all ? { query: { all: '1' } } : {}) });
|
|
66
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
67
|
+
const rows = data.grants || [];
|
|
68
|
+
if (!rows.length) { console.log('No standing grants.'); return; }
|
|
69
|
+
for (const g of rows) {
|
|
70
|
+
const state = g.revoked_at ? `revoked ${g.revoked_at}` : `expires ${g.expires_at}`;
|
|
71
|
+
const range = g.vmid_min != null ? ` vmids ${g.vmid_min}-${g.vmid_max}` : '';
|
|
72
|
+
console.log(`${g.grant_id} ${g.tool}@${g.host}${range} ${g.preset || `${g.patterns.length} patterns`} uses ${g.use_count} ${state}`);
|
|
73
|
+
console.log(` reason: ${g.reason} (by ${g.created_by})`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
grants
|
|
78
|
+
.command('create')
|
|
79
|
+
.description('Create a standing grant (human org admin only)')
|
|
80
|
+
.requiredOption('--host <hostId>', 'the ONE host the grant covers')
|
|
81
|
+
.requiredOption('--reason <text>', 'why (shown in history and evidence)')
|
|
82
|
+
.option('--preset <name>', 'proxmox-test-vm-lane (clone/power/destroy/guest-exec in a VMID range)')
|
|
83
|
+
.option('--template <vmid>', 'template VMID the lane clones from (preset)')
|
|
84
|
+
.option('--vmid-range <min-max>', 'VMID range, e.g. 9100-9199')
|
|
85
|
+
.option('--name-prefix <prefix>', 'VM name prefix the lane may create (preset)', 'z2h')
|
|
86
|
+
.option('--pattern <regex>', 'custom anchored command regex (repeatable; instead of --preset)', (v, a) => (a || []).concat([v]))
|
|
87
|
+
.option('--days <n>', 'expiry in days (max 30)', '14')
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
auth.requireAuth();
|
|
90
|
+
const body = { tool: 'ssh_exec', host: options.host, reason: options.reason, expires_in_days: Number(options.days) };
|
|
91
|
+
if (options.vmidRange) {
|
|
92
|
+
const m = /^(\d+)-(\d+)$/.exec(options.vmidRange);
|
|
93
|
+
if (!m) { console.error('--vmid-range must look like 9100-9199'); process.exitCode = 1; return; }
|
|
94
|
+
body.vmid_min = Number(m[1]); body.vmid_max = Number(m[2]);
|
|
95
|
+
}
|
|
96
|
+
if (options.preset) {
|
|
97
|
+
body.preset = options.preset; body.template_vmid = Number(options.template); body.name_prefix = options.namePrefix;
|
|
98
|
+
} else {
|
|
99
|
+
body.patterns = options.pattern || [];
|
|
100
|
+
}
|
|
101
|
+
const ep = resolveEndpoint('v1.approvals.grants.create');
|
|
102
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
103
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
104
|
+
const g = data.grant || {};
|
|
105
|
+
console.log(`Created ${g.grant_id}: ${g.tool}@${g.host}${g.vmid_min != null ? ` vmids ${g.vmid_min}-${g.vmid_max}` : ''}, expires ${g.expires_at}`);
|
|
106
|
+
console.log(`Revoke any time: quox approvals grants revoke ${g.grant_id}`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
grants
|
|
110
|
+
.command('revoke <grantId>')
|
|
111
|
+
.description('Revoke a standing grant now (human org admin only)')
|
|
112
|
+
.action(async (grantId) => {
|
|
113
|
+
auth.requireAuth();
|
|
114
|
+
const ep = resolveEndpoint('v1.approvals.grants.revoke', { id: grantId });
|
|
115
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
116
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
117
|
+
console.log(`Revoked ${data.grant?.grant_id || grantId} at ${data.grant?.revoked_at || 'now'}.`);
|
|
118
|
+
});
|
|
47
119
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox aside "<question>"
|
|
3
|
+
*
|
|
4
|
+
* A read-only side question: the model gets no tools and no conversation, so nothing it says can change
|
|
5
|
+
* anything or land in a conversation's history. The TUI's /aside is the same call with the recent
|
|
6
|
+
* exchanges passed as read-only context.
|
|
7
|
+
*/
|
|
8
|
+
import { askAside } from '../core/aside.js';
|
|
9
|
+
|
|
10
|
+
export function registerAsideCommand(program, ctx) {
|
|
11
|
+
const { client, auth } = ctx;
|
|
12
|
+
program
|
|
13
|
+
.command('aside')
|
|
14
|
+
.description('Ask a read-only side question: no tools, not saved to any conversation')
|
|
15
|
+
.argument('<question...>', 'the question')
|
|
16
|
+
.option('--agent <id>', 'who answers (default quox)')
|
|
17
|
+
.action(async (words, opts) => {
|
|
18
|
+
auth.requireAuth();
|
|
19
|
+
const r = await askAside(client, { question: words.join(' '), agent: opts.agent });
|
|
20
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ answer: r.text, model: r.model, provider: r.provider, tools_used: r.tools, read_only: true }, null, 2)); return; }
|
|
21
|
+
console.log(r.text || '(no answer)');
|
|
22
|
+
console.log(`· aside, read only: no tools, not saved${r.model ? ` · ${r.provider ? `${r.provider}/` : ''}${r.model}` : ''}`);
|
|
23
|
+
});
|
|
24
|
+
}
|