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/tool.js
CHANGED
|
@@ -2,10 +2,30 @@
|
|
|
2
2
|
* Tool commands: CRUD, catalog, install, publish
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import { missingReasonWarning } from '../core/callerSource.js';
|
|
6
7
|
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
7
8
|
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
8
9
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
10
|
+
import { capInline, checkShape, shapeOutput, shapeRecord } from '../toolOutputShape.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* QUOXON-LATENCY L2: the per-turn credential. QUOX_AUTH_TOKEN (cold spawn) or, on a warm
|
|
14
|
+
* `claude` spare, QUOX_AUTH_TOKEN_FILE: a file the collector writes for one turn and
|
|
15
|
+
* deletes when it ends. When the file variable is set it is the ONLY credential: a
|
|
16
|
+
* missing or empty file fails closed, never falling back to a stored session or the
|
|
17
|
+
* service key.
|
|
18
|
+
*/
|
|
19
|
+
function readTurnToken(env) {
|
|
20
|
+
if (env.QUOX_AUTH_TOKEN) return env.QUOX_AUTH_TOKEN;
|
|
21
|
+
if (!env.QUOX_AUTH_TOKEN_FILE) return '';
|
|
22
|
+
let token = '';
|
|
23
|
+
try { token = fs.readFileSync(env.QUOX_AUTH_TOKEN_FILE, 'utf8').trim(); } catch { token = ''; }
|
|
24
|
+
if (!token) throw new Error('No turn credential: QUOX_AUTH_TOKEN_FILE holds no token (the turn has ended). Refusing to use any other credential.');
|
|
25
|
+
return token;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const failedOf = (data) => !!((data && data.success === false) || (data && data.result && data.result.success === false));
|
|
9
29
|
|
|
10
30
|
export function registerToolCommands(program, ctx) {
|
|
11
31
|
const { client, auth } = ctx;
|
|
@@ -58,7 +78,7 @@ export function registerToolCommands(program, ctx) {
|
|
|
58
78
|
const orgId = resolveOrgId(auth, program);
|
|
59
79
|
const body = {
|
|
60
80
|
name: options.name,
|
|
61
|
-
|
|
81
|
+
tool_type: options.type, // #987: auth reads tool_type ('type' made every create 400)
|
|
62
82
|
risk_level: options.riskLevel,
|
|
63
83
|
};
|
|
64
84
|
if (options.description) body.description = options.description;
|
|
@@ -132,67 +152,59 @@ export function registerToolCommands(program, ctx) {
|
|
|
132
152
|
|
|
133
153
|
tool
|
|
134
154
|
.command('exec')
|
|
135
|
-
.description('Execute a governed tool via the collector (POST /api/v1/tools/execute)')
|
|
155
|
+
.description('Execute a governed tool via the collector (POST /api/v1/tools/execute). Large output: use --select/--summary, never a shell pipe.')
|
|
136
156
|
.argument('<name>', 'tool name/id (the TOOL_DEFS key, e.g. "codebase_query")')
|
|
137
157
|
.option('--args <json>', "tool arguments as a JSON string, e.g. '{\"query\":\"...\"}'", '{}')
|
|
138
158
|
// #620: docs/muscle-memory reach for --input; accept it as an alias for
|
|
139
159
|
// --args so the documented form works. --args still wins if both given.
|
|
140
160
|
.option('--input <json>', 'alias for --args (tool arguments as a JSON string)')
|
|
141
161
|
.option('--reason <text>', 'why you are running this; shown on the approval card if the call is held for a human')
|
|
162
|
+
// #970: governed shaping of a large result (the receipt records the shape). Grammar:
|
|
163
|
+
// a.b, a[0], list[field=value], list[field~text], list[*].field, list[*].{a,b}, list[field=value].name, count(list).
|
|
164
|
+
.option('--select <path>', 'return only this part of the result, e.g. agents[status=disconnected].name, streams[name~license] (contains, any case), layers[*].state (every item), containers[*].{host,name,status} (several fields in one call), count(urls), or several paths at once: total,count(urls)')
|
|
165
|
+
.option('--summary', 'honest summary of the main list: total, per-status counts, first N items ("showing N of M")')
|
|
166
|
+
.option('--limit <n>', 'cap the list to N items (with --summary: the sample size, default 10)')
|
|
142
167
|
.action(async (name, options) => {
|
|
143
|
-
auth.requireAuth();
|
|
144
168
|
// Prefer whichever was explicitly given; --args carries the '{}' default
|
|
145
169
|
// so it is always defined, hence check --input first.
|
|
146
170
|
const usedInput = options.input !== undefined && (options.args === undefined || options.args === '{}');
|
|
147
171
|
const raw = usedInput ? options.input : options.args;
|
|
148
172
|
const input = parseJSON(raw, usedInput ? '--input' : '--args');
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
throw new Error(
|
|
163
|
-
'QUOX_SERVICE_KEY is set but no org could be resolved. '
|
|
164
|
-
+ 'Set QUOX_ORG_ID=<org> for automation callers, or pass --org <id>.'
|
|
165
|
-
);
|
|
173
|
+
const shape = { select: options.select, summary: !!options.summary, limit: options.limit };
|
|
174
|
+
// A malformed shape fails BEFORE the tool runs: nothing executes, nothing to receipt.
|
|
175
|
+
const shapeErr = checkShape(shape);
|
|
176
|
+
if (shapeErr) throw new Error(shapeErr);
|
|
177
|
+
const outputShape = shapeRecord(shape);
|
|
178
|
+
const data = await executeGovernedTool({ client, auth, program }, name, input, { reason: options.reason, outputShape });
|
|
179
|
+
// Shape only a completed, successful result: a held or failed call is shown whole.
|
|
180
|
+
let shown = data;
|
|
181
|
+
const held = data && (data.pending || data._approval || data.status === 'pending_approval' || data.status === 'held');
|
|
182
|
+
if (outputShape && data && !held && !failedOf(data)) {
|
|
183
|
+
const wrapped = data.result !== undefined;
|
|
184
|
+
const value = shapeOutput(wrapped ? data.result : data, shape).value;
|
|
185
|
+
shown = wrapped ? { ...data, result: value, output_shape: outputShape } : { result: value, output_shape: outputShape };
|
|
166
186
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// "CLI exec silently becomes quox" bypass). The collector sets this from
|
|
183
|
-
// its own selected_agent; a service-key caller's known agent_id is
|
|
184
|
-
// trusted, and the P-B gate then authorizes against the specialist's own
|
|
185
|
-
// grant ceiling ∩ the hat mode.
|
|
186
|
-
if (process.env.QUOX_AGENT_ID) body.agent_id = process.env.QUOX_AGENT_ID;
|
|
187
|
-
|
|
188
|
-
const ep = resolveEndpoint('v1.tools.execute');
|
|
189
|
-
const data = await client.post(ep.path, { service: ep.service, body, timeout: 120000 });
|
|
187
|
+
// BASH-FALLBACK: on the model-driven path (a per-turn credential is set) an oversized
|
|
188
|
+
// result would be saved to a file by the Bash tool and the model would reach for jq on
|
|
189
|
+
// it, which the guard denies. Keep it inline as the honest summary view instead. Never
|
|
190
|
+
// for a held or failed call (the approval/error is shown whole), never for a human.
|
|
191
|
+
let modelPath = false;
|
|
192
|
+
try { modelPath = !!readTurnToken(process.env); } catch { modelPath = false; }
|
|
193
|
+
if (modelPath && data && !held && !failedOf(data)) {
|
|
194
|
+
const wrapped = shown && shown.result !== undefined;
|
|
195
|
+
const capped = capInline(wrapped ? shown.result : shown);
|
|
196
|
+
if (capped.capped) shown = wrapped ? { ...shown, result: capped.value } : capped.value;
|
|
197
|
+
}
|
|
198
|
+
// quox#1031 (same class as #999): a refused or failed call must exit non-zero, or a script reads
|
|
199
|
+
// success on failure. A HELD (pending) call awaits a human, so it stays 0.
|
|
200
|
+
const failed = failedOf(data);
|
|
201
|
+
if (failed && !(data && data.pending)) process.exitCode = 1;
|
|
190
202
|
|
|
191
203
|
if (program.opts().output === 'json') {
|
|
192
|
-
console.log(JSON.stringify(
|
|
204
|
+
console.log(JSON.stringify(shown, null, 2));
|
|
193
205
|
return;
|
|
194
206
|
}
|
|
195
|
-
const result =
|
|
207
|
+
const result = shown && (shown.result !== undefined ? shown.result : shown);
|
|
196
208
|
if (typeof result === 'string') {
|
|
197
209
|
console.log(result);
|
|
198
210
|
} else {
|
|
@@ -214,3 +226,79 @@ export function registerToolCommands(program, ctx) {
|
|
|
214
226
|
printSuccess('Tool installed.');
|
|
215
227
|
});
|
|
216
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* POST /api/v1/tools/execute for one governed tool, with the full identity
|
|
232
|
+
* contract `tool exec` established (per-turn QUOX_AUTH_TOKEN, org resolution,
|
|
233
|
+
* hat session/user/agent forwarding). Shared so a named verb that wraps a tool
|
|
234
|
+
* (e.g. `quox actions list`, #839) authenticates exactly like `tool exec`.
|
|
235
|
+
* Returns the raw response body.
|
|
236
|
+
*/
|
|
237
|
+
// quox#1034: the HTTP budget for one governed tool call. 120 s for a person at a terminal. A caller that spawns
|
|
238
|
+
// the CLI with a longer budget of its own (the collector's CLI-tools chat path, where delegate_to_agent waits up to
|
|
239
|
+
// the specialist's delegation.wait_ms, Browser-Q 180 s) states it in QUOX_TOOL_EXEC_TIMEOUT_MS, so the client hop
|
|
240
|
+
// never gives up before the server's own deadline and detach do. Capped at 15 minutes.
|
|
241
|
+
export function toolExecTimeoutMs(env = process.env) {
|
|
242
|
+
const n = Number(env && env.QUOX_TOOL_EXEC_TIMEOUT_MS);
|
|
243
|
+
if (!Number.isFinite(n) || n < 1000) return 120000;
|
|
244
|
+
return Math.min(Math.floor(n), 900000);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function executeGovernedTool({ client, auth, program }, name, input, { reason, outputShape } = {}) {
|
|
248
|
+
// QUOXON-IDENTITY: QUOX_AUTH_TOKEN is the collector's per-turn scoped
|
|
249
|
+
// credential for the model-driven CLI-tools path. When set it is the ONLY
|
|
250
|
+
// credential: sent as the bearer, never the service key, no stored-session
|
|
251
|
+
// or refresh fallback, and org/user come from the token (not the body).
|
|
252
|
+
const turnToken = readTurnToken(process.env);
|
|
253
|
+
if (!turnToken) auth.requireAuth();
|
|
254
|
+
const body = { tool_name: name, input };
|
|
255
|
+
if (reason) body.reason = String(reason);
|
|
256
|
+
// #970: the collector records the view the caller was shown in the tool receipt.
|
|
257
|
+
if (outputShape) body.output_shape = outputShape;
|
|
258
|
+
{ const w = missingReasonWarning(name, reason); if (w) console.error(w); }
|
|
259
|
+
|
|
260
|
+
// Same org-resolution precedence as `fleet exec` (lib/commands/fleet.js):
|
|
261
|
+
// the collector's /api/v1/tools/execute requires org_id in the body for
|
|
262
|
+
// service-key authenticated callers (isServiceKeyAuth = !req.user,
|
|
263
|
+
// services/collector/server.js ~8326) since there is no session to
|
|
264
|
+
// derive org from. A human-logged-in caller has req.user and the
|
|
265
|
+
// server derives org from the session instead — --org / QUOX_ORG_ID
|
|
266
|
+
// still wins when set explicitly.
|
|
267
|
+
const orgId = turnToken ? null : auth.getOrgId(program.opts().org || process.env.QUOX_ORG_ID);
|
|
268
|
+
if (!orgId && !turnToken && process.env.QUOX_SERVICE_KEY) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
'QUOX_SERVICE_KEY is set but no org could be resolved. '
|
|
271
|
+
+ 'Set QUOX_ORG_ID=<org> for automation callers, or pass --org <id>.'
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (orgId) body.org_id = orgId;
|
|
275
|
+
|
|
276
|
+
// QUOXHAT T0 (2026-10-01): automation callers (the collector's
|
|
277
|
+
// CLI-tools chat path) carry the conversation + acting user in env,
|
|
278
|
+
// exactly like QUOX_ORG_ID above. The collector's execute route keys
|
|
279
|
+
// hat-binding resolution on body.session_id and user prefs on
|
|
280
|
+
// body.user_id; without them every CLI-tools execution ran against the
|
|
281
|
+
// org default hat and user-less. Interactive humans have neither set.
|
|
282
|
+
if (process.env.QUOX_SESSION_ID) body.session_id = process.env.QUOX_SESSION_ID;
|
|
283
|
+
if (process.env.QUOX_USER_ID && !turnToken) body.user_id = process.env.QUOX_USER_ID;
|
|
284
|
+
// QUOXHAT #657 P-C.2: forward the ACTING AGENT so a specialist's CLI-tools
|
|
285
|
+
// chat turn executes as that specialist, not the orchestrator. Without it
|
|
286
|
+
// the spawned CLI is a service caller with no agent_id, which the
|
|
287
|
+
// collector defaults to 'quox' — so under enforce a restricted specialist
|
|
288
|
+
// would silently run tools against the orchestrator's broad grant (the
|
|
289
|
+
// "CLI exec silently becomes quox" bypass). The collector sets this from
|
|
290
|
+
// its own selected_agent; a service-key caller's known agent_id is
|
|
291
|
+
// trusted, and the P-B gate then authorizes against the specialist's own
|
|
292
|
+
// grant ceiling ∩ the hat mode.
|
|
293
|
+
if (process.env.QUOX_AGENT_ID) body.agent_id = process.env.QUOX_AGENT_ID;
|
|
294
|
+
|
|
295
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
296
|
+
// noAuth: client.js adds no service key / stored session / refresh retry.
|
|
297
|
+
const data = await client.post(ep.path, {
|
|
298
|
+
service: ep.service,
|
|
299
|
+
body,
|
|
300
|
+
timeout: toolExecTimeoutMs(process.env),
|
|
301
|
+
...(turnToken ? { noAuth: true, noActAs: true, headers: { Authorization: `Bearer ${turnToken}` } } : {}),
|
|
302
|
+
});
|
|
303
|
+
return data;
|
|
304
|
+
}
|
package/lib/commands/trace.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { randomUUID } from 'node:crypto';
|
|
22
22
|
import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
23
|
-
import { parseJSON } from './helpers.js';
|
|
23
|
+
import { parseJSON, withTimeoutHint } from './helpers.js';
|
|
24
24
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
25
25
|
|
|
26
26
|
/** Flatten the nested `actor` object into a display string for table rows. */
|
|
@@ -71,10 +71,14 @@ export function registerTraceCommands(program, ctx) {
|
|
|
71
71
|
if (options.denied) query.has_denial = 'true';
|
|
72
72
|
if (options.hitl) query.has_hitl = 'true';
|
|
73
73
|
|
|
74
|
-
const data = await
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const data = await withTimeoutHint(
|
|
75
|
+
'quox trace list',
|
|
76
|
+
'Try a smaller --limit or a narrower window (--after <iso>, --before <iso>).',
|
|
77
|
+
() => client.get(resolveEndpoint('traces.list').path, {
|
|
78
|
+
service: resolveEndpoint('traces.list').service,
|
|
79
|
+
query,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
78
82
|
const traces = (data.traces || data || []).map(normalizeTraceSummary);
|
|
79
83
|
|
|
80
84
|
formatOutput({
|
|
@@ -83,8 +87,9 @@ export function registerTraceCommands(program, ctx) {
|
|
|
83
87
|
rows: traces,
|
|
84
88
|
}, { format: program.opts().output, emptyMessage: 'No traces found.' });
|
|
85
89
|
|
|
86
|
-
if (program.opts().output !== 'json' && program.opts().output !== 'quiet'
|
|
87
|
-
printInfo(`Showing ${traces.length} of ${data.total} trace(s).`);
|
|
90
|
+
if (program.opts().output !== 'json' && program.opts().output !== 'quiet') {
|
|
91
|
+
if (typeof data.total === 'number') printInfo(`Showing ${traces.length} of ${data.total} trace(s).`);
|
|
92
|
+
else if (data.has_more) printInfo(`Showing ${traces.length} trace(s); more available (--offset ${Number(options.offset || 0) + Number(options.limit || 30)}).`);
|
|
88
93
|
}
|
|
89
94
|
});
|
|
90
95
|
|
package/lib/commands/trigger.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* quox trigger webhooks|schedules|events
|
|
7
7
|
* quox trigger workflow-activate|workflow-deactivate <workflow-id>
|
|
8
|
+
* quox trigger pause|resume <workflow-id> [node-id]
|
|
9
|
+
* quox trigger webhook-fire <path> [--body <json|@file>]
|
|
8
10
|
*
|
|
9
11
|
* Backend: quoxflow (NOT the auth service, NOT proxied by collector) — see
|
|
10
12
|
* cli/lib/api/endpoints/triggers.js header for the full access-path
|
|
@@ -21,7 +23,48 @@
|
|
|
21
23
|
import { formatOutput, formatDetail, printInfo } from '../core/output.js';
|
|
22
24
|
import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
23
25
|
import { resolveOrgId } from './helpers.js';
|
|
26
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
27
|
+
import { resolve as resolvePath, sep, basename } from 'node:path';
|
|
24
28
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
29
|
+
import { QuoxApiError } from '../core/errors.js';
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// A webhook path is plain segments under /webhooks: no .., no query or fragment, no encoding tricks, no spaces.
|
|
33
|
+
const WEBHOOK_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
34
|
+
export function normalizeWebhookPath(raw) {
|
|
35
|
+
let p = String(raw || '').trim().replace(/^\/+/, '');
|
|
36
|
+
if (p.startsWith('webhooks/')) p = p.slice('webhooks/'.length);
|
|
37
|
+
const segs = p.split('/');
|
|
38
|
+
if (!p || segs.some((x) => !WEBHOOK_SEGMENT.test(x) || x === '.' || x === '..')) {
|
|
39
|
+
throw new Error(`webhook path "${raw}" is not a plain path under /webhooks (letters, digits, . _ ~ - and /; no .., query, fragment or spaces)`);
|
|
40
|
+
}
|
|
41
|
+
return segs.join('/');
|
|
42
|
+
}
|
|
43
|
+
// HYPERION #23: --body @file is an ALLOWLIST. The file must be a regular file whose REAL path (symlinks resolved) is
|
|
44
|
+
// inside the working directory, and never a credential-shaped name: ~/.config/quox/config.json is JSON and holds the
|
|
45
|
+
// user's tokens, and the fired run's trigger data is org-readable and can be forwarded by the workflow.
|
|
46
|
+
const CREDENTIAL_NAME = /^(?:\.env(?:\..*)?|.*\.pem|.*\.key|id_[a-z0-9_]+(?:\.pub)?|credentials(?:\..*)?)$/i;
|
|
47
|
+
export function parseWebhookBody(raw, { cwd = process.cwd() } = {}) {
|
|
48
|
+
if (raw === undefined) return {};
|
|
49
|
+
let text = String(raw);
|
|
50
|
+
if (text.startsWith('@')) {
|
|
51
|
+
const asked = text.slice(1);
|
|
52
|
+
const rule = '--body @file reads only a regular file inside the current directory';
|
|
53
|
+
let real, root;
|
|
54
|
+
try { root = realpathSync(cwd); real = realpathSync(resolvePath(cwd, asked)); }
|
|
55
|
+
catch { throw new Error(`--body @${asked}: no such file (${rule})`); }
|
|
56
|
+
if (real !== root && !real.startsWith(root + sep)) throw new Error(`--body @${asked} is not inside the current directory (${rule}; symlinks are followed)`);
|
|
57
|
+
const rel = real.slice(root.length + 1);
|
|
58
|
+
if (CREDENTIAL_NAME.test(basename(real)) || /(^|[\\/])\.config[\\/].*config\.json$/i.test(rel)) throw new Error(`--body @${asked} looks like a credential file; refused (${rule}, never a key, token or config file)`);
|
|
59
|
+
if (!statSync(real).isFile()) throw new Error(`--body @${asked} is not a regular file (${rule})`);
|
|
60
|
+
text = readFileSync(real, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
try { return JSON.parse(text); } catch { throw new Error('--body is not JSON (pass a JSON string or @file with JSON in it)'); }
|
|
63
|
+
}
|
|
64
|
+
const apiFail = (method, path, err) => {
|
|
65
|
+
const status = err instanceof QuoxApiError ? err.status : 'network error';
|
|
66
|
+
return new Error(`${method} ${path} failed (${status}): ${err.message}`);
|
|
67
|
+
};
|
|
25
68
|
|
|
26
69
|
export function registerTriggerCommands(program, ctx) {
|
|
27
70
|
const { client, auth } = ctx;
|
|
@@ -125,4 +168,56 @@ export function registerTriggerCommands(program, ctx) {
|
|
|
125
168
|
printEvent(glyphLine('quox', 'workflow deactivated', { result: id, status: 'ok' }));
|
|
126
169
|
formatDetail(result, { format: program.opts().output });
|
|
127
170
|
});
|
|
171
|
+
|
|
172
|
+
// ── trigger pause | resume ─────────────────────────────────────────────────
|
|
173
|
+
// SITE-CLI 2026-10-19: pausing a schedule meant re-applying the workflow JSON with paused:true pasted in by hand.
|
|
174
|
+
// The node is found from the workflow when it has exactly one schedule; with several, name it (never guess).
|
|
175
|
+
const scheduleNode = async (workflowId, nodeId) => {
|
|
176
|
+
if (nodeId) return nodeId;
|
|
177
|
+
const ep = resolveEndpoint('triggers.schedule-list');
|
|
178
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
179
|
+
const rows = (Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []).filter((r) => r.workflowId === workflowId);
|
|
180
|
+
if (rows.length === 0) throw new Error(`no schedule found for workflow ${workflowId} (is it activated? quox trigger schedules)`);
|
|
181
|
+
if (rows.length > 1) throw new Error(`workflow ${workflowId} has ${rows.length} schedule nodes (${rows.map((r) => r.nodeId).join(', ')}): name one, quox trigger pause ${workflowId} <node>`);
|
|
182
|
+
return rows[0].nodeId;
|
|
183
|
+
};
|
|
184
|
+
for (const [verb, key, done] of [['pause', 'triggers.schedule-pause', 'paused'], ['resume', 'triggers.schedule-resume', 'resumed']]) {
|
|
185
|
+
trigger
|
|
186
|
+
.command(verb)
|
|
187
|
+
.description(verb === 'pause'
|
|
188
|
+
? 'Pause a workflow schedule (stays paused across restarts) without re-applying the workflow'
|
|
189
|
+
: 'Resume a paused workflow schedule')
|
|
190
|
+
.argument('<workflowId>', 'workflow ID')
|
|
191
|
+
.argument('[nodeId]', 'schedule node ID (needed only when the workflow has more than one schedule)')
|
|
192
|
+
.action(async (workflowId, nodeIdArg) => {
|
|
193
|
+
auth.requireAuth();
|
|
194
|
+
const nodeId = await scheduleNode(workflowId, nodeIdArg);
|
|
195
|
+
const ep = resolveEndpoint(key, { workflowId, nodeId });
|
|
196
|
+
let data;
|
|
197
|
+
try { data = await client.post(ep.path, { service: ep.service }); } catch (err) { throw apiFail('POST', ep.path, err); }
|
|
198
|
+
const result = data?.data || data;
|
|
199
|
+
printEvent(glyphLine('quox', `schedule ${done}`, { result: `${workflowId}:${nodeId}`, status: 'ok' }));
|
|
200
|
+
formatDetail(result, { format: program.opts().output });
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── trigger webhook-fire ───────────────────────────────────────────────────
|
|
205
|
+
// Test-fire a workflow's inbound webhook through the real public ingress. The route copies every request header
|
|
206
|
+
// into the run's trigger data, so this sends NO bearer and NO service key: the run is a system:webhook:<path> run,
|
|
207
|
+
// exactly like an outside caller's.
|
|
208
|
+
trigger
|
|
209
|
+
.command('webhook-fire')
|
|
210
|
+
.description('Fire a workflow inbound webhook (POST /webhooks/<path>) as an outside caller would, with no credentials')
|
|
211
|
+
.argument('<path>', 'webhook path, e.g. orders/new (a leading /webhooks/ is fine)')
|
|
212
|
+
.option('--body <json|@file>', 'JSON body (a JSON string, or @file); default {}')
|
|
213
|
+
.action(async (rawPath, options) => {
|
|
214
|
+
const path = normalizeWebhookPath(rawPath);
|
|
215
|
+
const body = parseWebhookBody(options.body, { cwd: (ctx.cwd || (() => process.cwd()))() });
|
|
216
|
+
const ep = resolveEndpoint('triggers.webhook-fire', { path });
|
|
217
|
+
let data;
|
|
218
|
+
try { data = await client.post(ep.path, { service: ep.service, body, noAuth: true, noActAs: true, anonymous: true }); } catch (err) { throw apiFail('POST', ep.path, err); }
|
|
219
|
+
const result = data?.data || data;
|
|
220
|
+
printEvent(glyphLine('quox', 'webhook fired', { result: `/webhooks/${path}${result?.executionId ? ` · run ${result.executionId}` : ''}`, status: 'ok' }));
|
|
221
|
+
formatDetail(result, { format: program.opts().output });
|
|
222
|
+
});
|
|
128
223
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox update` (and `quox --update`, rewritten to this in bin/quox.js): update this quox the way it was
|
|
3
|
+
* installed. The same updater as /update in the conversation client (lib/core/selfUpdate.js).
|
|
4
|
+
*
|
|
5
|
+
* quox update check, show what's new, ask y/N (a TTY), then run the update with the terminal
|
|
6
|
+
* quox update --check check and show, change nothing (exit 10 when an update is available)
|
|
7
|
+
* quox update --yes no question (needed when not a TTY)
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
|
+
import { detectInstall, checkUpdate, runUpdate, updateLines, updateAsk } from '../core/selfUpdate.js';
|
|
12
|
+
|
|
13
|
+
async function askYes(line) {
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
try { return /^y(es)?$/i.test((await rl.question(`${line} `)).trim()); } finally { rl.close(); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerUpdateCommand(program, ctx = {}) {
|
|
19
|
+
const d = ctx.updateDeps || {};
|
|
20
|
+
const out = d.log || ((l) => console.log(l));
|
|
21
|
+
program.command('update')
|
|
22
|
+
.description('Update quox to the latest version (npm, git checkout, or this box\'s shared CLI); also quox --update')
|
|
23
|
+
.option('--check', 'show what an update would do and change nothing')
|
|
24
|
+
.option('-y, --yes', 'update without asking (needed in a non-interactive shell)')
|
|
25
|
+
.action(async (o) => {
|
|
26
|
+
const info = (d.detect || detectInstall)({});
|
|
27
|
+
const plan = await (d.check || checkUpdate)(info);
|
|
28
|
+
for (const l of updateLines(plan)) out(l);
|
|
29
|
+
if (!plan.ok) { process.exitCode = 2; return; }
|
|
30
|
+
if (plan.upToDate) return;
|
|
31
|
+
if (plan.refused) { process.exitCode = 1; return; }
|
|
32
|
+
if (o.check) { out(`Update with: quox update (runs: ${plan.command.display})`); process.exitCode = 10; return; }
|
|
33
|
+
const yes = o.yes || program.opts().yes;
|
|
34
|
+
if (!yes) {
|
|
35
|
+
const tty = d.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
36
|
+
if (!tty) { out('Not updated: this needs a yes. Re-run with quox update --yes in a non-interactive shell.'); process.exitCode = 1; return; }
|
|
37
|
+
if (!(await (d.ask || askYes)(updateAsk(plan, { restart: false })))) { out('Not updated. Nothing was changed.'); return; }
|
|
38
|
+
}
|
|
39
|
+
out(chalk.dim(`Running: ${plan.command.display}`));
|
|
40
|
+
const r = (d.run || runUpdate)(plan);
|
|
41
|
+
if (r.ok) { out(chalk.green(`✔ Updated (exit 0). The next quox you start is the new one.`)); return; }
|
|
42
|
+
process.exitCode = r.code || 1;
|
|
43
|
+
out(chalk.red(`✖ Update failed: ${plan.command.display} ${r.error ? `could not start (${r.error})` : `exited ${r.code}`}. Nothing else was changed.`));
|
|
44
|
+
out(`Run it yourself to see why: ${plan.manual}`);
|
|
45
|
+
});
|
|
46
|
+
}
|
package/lib/commands/vault.js
CHANGED
|
@@ -943,6 +943,8 @@ export function registerVaultCommands(program, ctx) {
|
|
|
943
943
|
const credentials = data.credentials || data;
|
|
944
944
|
|
|
945
945
|
if (credentials.length === 0) {
|
|
946
|
+
// quox#920: --json always prints valid JSON, an honest empty included.
|
|
947
|
+
if (program.opts().output === 'json') { console.log('[]'); return; }
|
|
946
948
|
printInfo(`No credentials expiring within ${days} days.`);
|
|
947
949
|
return;
|
|
948
950
|
}
|
|
@@ -1373,15 +1375,36 @@ export function registerVaultCommands(program, ctx) {
|
|
|
1373
1375
|
|
|
1374
1376
|
vault
|
|
1375
1377
|
.command('health')
|
|
1376
|
-
.description('
|
|
1377
|
-
.option('--org-id <orgId>', 'org ID to scope the summary to (server default: "default")')
|
|
1378
|
+
.description('Vault health summary: total, healthy, failed, stale, expiring credentials')
|
|
1379
|
+
.option('--org-id <orgId>', 'with QUOX_SERVICE_KEY only: org ID to scope the summary to (server default: "default")')
|
|
1378
1380
|
.action(async (options) => {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1381
|
+
// #914: the internal route accepts only the service key, so an owner's `vault health` was a 401
|
|
1382
|
+
// "invalid service key". Automation (QUOX_SERVICE_KEY) keeps that route; a signed-in person reads
|
|
1383
|
+
// the same numbers from their org's vault stats.
|
|
1384
|
+
if (process.env.QUOX_SERVICE_KEY) {
|
|
1385
|
+
const ep = resolveEndpoint('vault.health', {});
|
|
1386
|
+
const data = await client.get(ep.path, {
|
|
1387
|
+
service: ep.service,
|
|
1388
|
+
query: options.orgId ? { org_id: options.orgId } : undefined,
|
|
1389
|
+
});
|
|
1390
|
+
formatDetail(data, { format: program.opts().output, title: 'QuoxVault Health' });
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
auth.requireAuth();
|
|
1394
|
+
const orgId = resolveOrgId(auth, program);
|
|
1395
|
+
const ep = resolveEndpoint('vault.vault-stats.list', { orgId });
|
|
1396
|
+
const st = await client.get(ep.path, { service: ep.service });
|
|
1397
|
+
const summary = {
|
|
1398
|
+
total: st.total ?? 0,
|
|
1399
|
+
healthy: st.healthy ?? 0,
|
|
1400
|
+
failed: st.failed ?? 0,
|
|
1401
|
+
untested: st.untested ?? 0,
|
|
1402
|
+
stale: st.stale ?? 0,
|
|
1403
|
+
expiring_soon: st.expiring ?? 0,
|
|
1404
|
+
rotation_overdue: st.rotationOverdue ?? 0,
|
|
1405
|
+
source: 'org vault stats',
|
|
1406
|
+
};
|
|
1407
|
+
formatDetail(summary, { format: program.opts().output, title: 'QuoxVault Health' });
|
|
1385
1408
|
});
|
|
1386
1409
|
|
|
1387
1410
|
// ── vault export (removed — WP0.2) ──────────────────────────────────────────
|