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/chat.js
CHANGED
|
@@ -26,6 +26,46 @@ export function buildStreamQuery({ queryId, message, agent, sessionId, context }
|
|
|
26
26
|
return body;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* TUI-DIVIDER: `quox chat --trace` prints the turn's protocol events as they arrive, so the
|
|
31
|
+
* per-layer AOCL record (`aocl.layer`: id, name, result, ms) is visible in the CLI before any
|
|
32
|
+
* UI animates it. Returns { text, tone } or null for an event --trace does not show.
|
|
33
|
+
* tone: ok | warn | fail | accent (no grey: every tone is a real colour).
|
|
34
|
+
*/
|
|
35
|
+
const RESULT_TONE = { pass: 'ok', gated: 'warn', blocked: 'fail', bypass: 'accent' };
|
|
36
|
+
const fmtTraceMs = (ms) => {
|
|
37
|
+
const n = Number(ms);
|
|
38
|
+
if (!Number.isFinite(n)) return '';
|
|
39
|
+
return n < 10 ? `${Math.round(n * 10) / 10} ms` : `${Math.round(n)} ms`;
|
|
40
|
+
};
|
|
41
|
+
export function formatTraceEvent(ev) {
|
|
42
|
+
if (!ev || typeof ev !== 'object') return null;
|
|
43
|
+
switch (ev.type) {
|
|
44
|
+
case 'turn.started':
|
|
45
|
+
return { text: `▸ turn.started ${[ev.provider, ev.model].filter(Boolean).join(' ')}`, tone: 'accent' };
|
|
46
|
+
case 'aocl.layer': {
|
|
47
|
+
const result = String(ev.result || 'pass');
|
|
48
|
+
const line = `▤ aocl.layer ${String(ev.id || '?').padEnd(4)}${String(ev.name || '').padEnd(10)}${result.padEnd(8)}${fmtTraceMs(ev.ms).padStart(8)} ${ev.phase || ''}${ev.reason ? ` ${ev.reason}` : ''}`;
|
|
49
|
+
return { text: line, tone: RESULT_TONE[result] || 'accent' };
|
|
50
|
+
}
|
|
51
|
+
case 'aocl.summary':
|
|
52
|
+
return { text: `▤ aocl.summary ${ev.accounted ?? '?'}/${ev.total ?? '?'} accounted · ${ev.passed ?? 0} passed · ${ev.bypassed ?? 0} bypassed · ${ev.rejected ?? 0} rejected${ev.ms != null ? ` · ${fmtTraceMs(ev.ms)}` : ''}`, tone: ev.rejected ? 'fail' : 'ok' };
|
|
53
|
+
case 'aocl.verdict':
|
|
54
|
+
return { text: `▤ aocl.verdict ${ev.layer || 'L8'} ${ev.verdict}${ev.reason ? `: ${ev.reason}` : ''}`, tone: ev.verdict === 'blocked' ? 'fail' : 'warn' };
|
|
55
|
+
case 'trace':
|
|
56
|
+
return { text: `· trace ${ev.protocol || ''} ${ev.label || ''}${ev.detail ? ` ${ev.detail}` : ''}`, tone: 'accent' };
|
|
57
|
+
case 'turn.finished':
|
|
58
|
+
return { text: `▸ turn.finished ${ev.status || ''}${ev.usage?.elapsed_ms != null ? ` · ${ev.usage.elapsed_ms} ms` : ''}`, tone: ev.status === 'completed' ? 'ok' : 'fail' };
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const TONE_PAINT = { ok: chalk.green, warn: chalk.yellow, fail: chalk.red, accent: chalk.cyan };
|
|
64
|
+
function printTrace(ev) {
|
|
65
|
+
const f = formatTraceEvent(ev);
|
|
66
|
+
if (f) process.stderr.write((TONE_PAINT[f.tone] || chalk.cyan)(f.text) + '\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
29
69
|
function generateQueryId() {
|
|
30
70
|
return `CLI_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
31
71
|
}
|
|
@@ -34,7 +74,7 @@ function generateQueryId() {
|
|
|
34
74
|
* Consume a chat SSE stream, printing chunks as they arrive.
|
|
35
75
|
* Returns { text, sessionId, agentUsed } once the stream completes.
|
|
36
76
|
*/
|
|
37
|
-
export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel } = {}) {
|
|
77
|
+
export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel, trace = false } = {}) {
|
|
38
78
|
const queryId = generateQueryId();
|
|
39
79
|
const body = buildStreamQuery({ queryId, message, agent, sessionId, context });
|
|
40
80
|
|
|
@@ -50,7 +90,19 @@ export async function consumeStream(client, { message, agent, sessionId, context
|
|
|
50
90
|
onAgentLabel?.(agentUsed);
|
|
51
91
|
};
|
|
52
92
|
|
|
53
|
-
|
|
93
|
+
const iter = client.stream(resolveEndpoint('chat.stream').path, { service: resolveEndpoint('chat.stream').service, body, untilClose: Boolean(trace) })[Symbol.asyncIterator]();
|
|
94
|
+
while (true) {
|
|
95
|
+
let step;
|
|
96
|
+
try {
|
|
97
|
+
step = await iter.next();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// A refusal before the stream opened carries the layers it really evaluated.
|
|
100
|
+
if (trace && Array.isArray(err?.body?.aocl_layers)) for (const l of err.body.aocl_layers) printTrace({ type: 'aocl.layer', ...l });
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
if (step.done) break;
|
|
104
|
+
const event = step.value;
|
|
105
|
+
if (trace) printTrace(event);
|
|
54
106
|
switch (event.type) {
|
|
55
107
|
case 'connected':
|
|
56
108
|
break;
|
|
@@ -120,6 +172,7 @@ export function registerChatCommands(program, ctx) {
|
|
|
120
172
|
.option('--agent <name>', 'route to a specific agent (e.g. sentinel, cipher, nova)')
|
|
121
173
|
.option('--no-stream', 'disable streaming output (one-shot request/response)')
|
|
122
174
|
.option('--conversation <id>', 'continue a conversation (session id)')
|
|
175
|
+
.option('--trace', 'print the turn\'s protocol events (per-layer AOCL, traces, summary) to stderr as they arrive')
|
|
123
176
|
.action(async (message, options) => {
|
|
124
177
|
auth.requireAuth();
|
|
125
178
|
const streaming = options.stream !== false;
|
|
@@ -161,6 +214,7 @@ export function registerChatCommands(program, ctx) {
|
|
|
161
214
|
sessionId,
|
|
162
215
|
}, {
|
|
163
216
|
onAgentLabel: (agentUsed) => process.stdout.write(chalk.green(`${(agentUsed || currentAgent).toUpperCase()}: `)),
|
|
217
|
+
trace: Boolean(options.trace),
|
|
164
218
|
});
|
|
165
219
|
if (result.sessionId) sessionId = result.sessionId;
|
|
166
220
|
console.log();
|
|
@@ -190,6 +244,7 @@ export function registerChatCommands(program, ctx) {
|
|
|
190
244
|
sessionId: options.conversation,
|
|
191
245
|
}, {
|
|
192
246
|
onAgentLabel: () => {}, // no label prefix in single-shot streaming; just print tokens
|
|
247
|
+
trace: Boolean(options.trace),
|
|
193
248
|
});
|
|
194
249
|
} catch (err) {
|
|
195
250
|
console.error(chalk.red(`Error: ${err.message}`));
|
package/lib/commands/claude.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* lives in core/governedSession.js; this file holds argument parsing and the
|
|
4
4
|
* commander wiring. argv arrays only; user input never reaches a shell string.
|
|
5
5
|
*/
|
|
6
|
+
import { markForwardsRawArgs } from '../core/global-flags.js';
|
|
6
7
|
import * as gs from '../core/governedSession.js';
|
|
7
8
|
import { validateName, PROGRAMS } from '../core/governedSession.js';
|
|
8
9
|
import { runClaudeLs } from '../core/claudeLs.js';
|
|
@@ -178,8 +179,8 @@ export function registerClaudeCommands(program, ctx, over) {
|
|
|
178
179
|
// `quox claude <args>` behaves exactly like `claude <args>`: every arg is
|
|
179
180
|
// forwarded verbatim. Only --no-tmux is quox's own. Management lives in
|
|
180
181
|
// `quox sessions` so it can never collide with claude's own arguments.
|
|
181
|
-
program
|
|
182
|
-
.command('claude')
|
|
182
|
+
markForwardsRawArgs(program
|
|
183
|
+
.command('claude'), 'forwards argv after the verb verbatim')
|
|
183
184
|
.description('Run Claude Code (same arguments as claude) in a tmux session that survives sleep and drops')
|
|
184
185
|
.argument('[args...]', 'passed to claude exactly as given (quox adds only --no-tmux, --as, --slot)')
|
|
185
186
|
.addHelpText('after', `
|
|
@@ -205,8 +206,8 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
205
206
|
const parsed = parseClaudeArgs(rest);
|
|
206
207
|
process.exitCode = runClaude(parsed, over);
|
|
207
208
|
});
|
|
208
|
-
program
|
|
209
|
-
.command('codex')
|
|
209
|
+
markForwardsRawArgs(program
|
|
210
|
+
.command('codex'), 'forwards argv after the verb verbatim')
|
|
210
211
|
.description('Run Codex (same arguments as codex) in a tmux session that survives sleep and drops')
|
|
211
212
|
.argument('[args...]', 'passed to codex exactly as given (quox adds only --no-tmux)')
|
|
212
213
|
.helpOption(false)
|
|
@@ -218,8 +219,8 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
218
219
|
const parsed = parseCodexArgs(i >= 0 ? raw.slice(i + 1) : []);
|
|
219
220
|
process.exitCode = runCodex(parsed, over);
|
|
220
221
|
});
|
|
221
|
-
program
|
|
222
|
-
.command('gemini')
|
|
222
|
+
markForwardsRawArgs(program
|
|
223
|
+
.command('gemini'), 'forwards argv after the verb verbatim')
|
|
223
224
|
.description('Run Gemini CLI (same arguments as gemini) in a tmux session that survives sleep and drops')
|
|
224
225
|
.argument('[args...]', 'passed to gemini exactly as given (quox adds only --no-tmux)')
|
|
225
226
|
.helpOption(false)
|
|
@@ -230,8 +231,8 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
230
231
|
const i = raw.indexOf('gemini', 2);
|
|
231
232
|
process.exitCode = runGemini(parseGeminiArgs(i >= 0 ? raw.slice(i + 1) : []), over);
|
|
232
233
|
});
|
|
233
|
-
program
|
|
234
|
-
.command('wrap')
|
|
234
|
+
markForwardsRawArgs(program
|
|
235
|
+
.command('wrap'), 'forwards argv after the verb verbatim')
|
|
235
236
|
.description('Run any program in a governed tmux session: quox wrap <program> [args...]')
|
|
236
237
|
.argument('<program>', 'command name on PATH')
|
|
237
238
|
.argument('[args...]', 'passed to the program exactly as given')
|
|
@@ -270,6 +271,31 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
270
271
|
const json = !!o.json || program.opts().output === 'json';
|
|
271
272
|
process.exitCode = await runSessionsOverview({ json, watch: !!o.watch, all: !!o.all }, { ...so, fetchIndex });
|
|
272
273
|
});
|
|
274
|
+
// `quox active [sessions|slots|agents]`: one short summary of who is doing what (QUOXON-1208).
|
|
275
|
+
program.command('active [filter]')
|
|
276
|
+
.description('Who is doing what: one line per active worker, grouped working / waiting on you / idle; filter: sessions, slots, agents')
|
|
277
|
+
.option('--json', 'print the summary as JSON')
|
|
278
|
+
.option('--all', 'show every worker (default: top 10 per group)')
|
|
279
|
+
.option('--lines', 'the older one-line-per-worker layout with state and age columns')
|
|
280
|
+
.action(async (filter, o) => {
|
|
281
|
+
const so = (over && over.sessionsOverview) || {};
|
|
282
|
+
const fetchIndex = so.fetchIndex || (async () => {
|
|
283
|
+
const { client, auth } = ctx;
|
|
284
|
+
auth.requireAuth();
|
|
285
|
+
const ep = resolveEndpoint('v1.sessions.index');
|
|
286
|
+
const org = orgOrNull(auth);
|
|
287
|
+
return client.get(ep.path, { service: ep.service, retries: 0, ...(org ? { query: { org_id: org } } : {}) });
|
|
288
|
+
});
|
|
289
|
+
const json = !!o.json || program.opts().output === 'json';
|
|
290
|
+
const { runActive } = await import('../core/activeSummary.js');
|
|
291
|
+
const fetchSlots = so.fetchSlots || (async () => {
|
|
292
|
+
const { client, auth } = ctx;
|
|
293
|
+
const ep = resolveEndpoint('v1.slots.list');
|
|
294
|
+
const r = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
295
|
+
return Array.isArray(r) ? r : (r?.slots || r?.data || []);
|
|
296
|
+
});
|
|
297
|
+
process.exitCode = await runActive({ filter: filter || null, json, all: !!o.all, lines: !!o.lines }, { ...so, fetchIndex, fetchSlots });
|
|
298
|
+
});
|
|
273
299
|
sessions.command('heartbeat <tmuxSession>', { hidden: true })
|
|
274
300
|
.description('Heartbeat one wrapped tmux session to the org session index (started by quox claude)')
|
|
275
301
|
.option('--loop', 'keep beating until the tmux session ends')
|
package/lib/commands/contacts.js
CHANGED
|
@@ -107,6 +107,7 @@ export function registerContactsCommands(program, ctx) {
|
|
|
107
107
|
const q = textQuery ?? options.q;
|
|
108
108
|
if (q) query.q = q;
|
|
109
109
|
if (options.status) query.status = options.status;
|
|
110
|
+
if (options.relationship) query.relationship = options.relationship;
|
|
110
111
|
if (options.limit !== undefined) query.limit = Number(options.limit);
|
|
111
112
|
const ep = resolveEndpoint('contactsq.contacts.list');
|
|
112
113
|
const data = await call(() => client.get(ep.path, { service: ep.service, query }));
|
|
@@ -186,6 +187,7 @@ export function registerContactsCommands(program, ctx) {
|
|
|
186
187
|
.description('List contacts')
|
|
187
188
|
.option('--q <text>', 'name filter')
|
|
188
189
|
.option('--status <s>', 'active | candidate | archived')
|
|
190
|
+
.option('--relationship <r>', 'primary relationship, e.g. customer | supplier | partner')
|
|
189
191
|
.option('--limit <n>', 'max contacts')
|
|
190
192
|
.action((options) => list(options));
|
|
191
193
|
|
|
@@ -373,6 +375,53 @@ export function registerContactsCommands(program, ctx) {
|
|
|
373
375
|
announce('contacts brief', `${d.candidates_to_review} candidates, ${d.duplicates} duplicates`);
|
|
374
376
|
});
|
|
375
377
|
|
|
378
|
+
contacts
|
|
379
|
+
.command('timeline <id>')
|
|
380
|
+
.description('Interaction history of a contact, newest first: surface, direction, outcome and time (refs only, no message bodies)')
|
|
381
|
+
.option('--limit <n>', 'max entries', '20')
|
|
382
|
+
.action(async (id, options) => {
|
|
383
|
+
auth.requireAuth();
|
|
384
|
+
const ep = resolveEndpoint('contactsq.timeline', { id });
|
|
385
|
+
const d = await call(() => client.get(ep.path, { service: ep.service, query: { limit: options.limit } }));
|
|
386
|
+
if (isJson(program)) return printJson(d);
|
|
387
|
+
const rows = d.timeline || [];
|
|
388
|
+
if (!rows.length) console.log('No interactions recorded.');
|
|
389
|
+
for (const t of rows) console.log(`${t.occurred_at || ''} ${t.surface || ''} ${t.direction || ''} ${t.outcome || ''}${t.agent_id ? ` (agent ${t.agent_id})` : ''}`);
|
|
390
|
+
announce('contacts timeline', `${rows.length} entries`);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
contacts
|
|
394
|
+
.command('best-channel <id>')
|
|
395
|
+
.description('Which channel (voice, email or sms) may be used to reach a contact for a purpose, with every alternative and its reason codes. Read-only; never shows a number or address')
|
|
396
|
+
.option('--purpose <purpose>', 'service, transactional, marketing or research', 'service')
|
|
397
|
+
.action(async (id, options) => {
|
|
398
|
+
auth.requireAuth();
|
|
399
|
+
const ep = resolveEndpoint('contactsq.best_channel', { id });
|
|
400
|
+
const d = await call(() => client.get(ep.path, { service: ep.service, query: { purpose: options.purpose } }));
|
|
401
|
+
if (isJson(program)) return printJson(d);
|
|
402
|
+
console.log(`channel: ${d.channel || 'none allowed'}`);
|
|
403
|
+
for (const a of d.alternatives || []) console.log(` ${a.channel}: ${a.decision}${(a.reasons || []).length ? ` (${a.reasons.join(', ')})` : ''}`);
|
|
404
|
+
announce('contacts best-channel', d.channel || 'none allowed');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
contacts
|
|
408
|
+
.command('count')
|
|
409
|
+
.description('How many contacts: the total (people and organisations, not archived or erased) with counts by kind, status and relationship. --kind narrows to person or organisation')
|
|
410
|
+
.option('--kind <kind>', 'person or organisation')
|
|
411
|
+
.action(async (options) => {
|
|
412
|
+
auth.requireAuth();
|
|
413
|
+
const ep = resolveEndpoint('contactsq.count');
|
|
414
|
+
const query = options.kind ? { kind: options.kind } : undefined;
|
|
415
|
+
const d = await call(() => client.get(ep.path, { service: ep.service, query }));
|
|
416
|
+
if (isJson(program)) return printJson(d);
|
|
417
|
+
const fmt = (o) => Object.entries(o || {}).map(([k, n]) => `${k} ${n}`).join(', ') || 'none';
|
|
418
|
+
formatDetail({
|
|
419
|
+
total: d.total, ...(d.kind ? { kind: d.kind } : {}), by_kind: fmt(d.by_kind), by_status: fmt(d.by_status),
|
|
420
|
+
by_relationship: fmt(d.by_relationship), candidates_to_review: d.candidates_to_review, generated_at: d.generated_at,
|
|
421
|
+
});
|
|
422
|
+
announce('contacts count', `${d.total} contacts${d.kind ? ` (${d.kind})` : ''}`);
|
|
423
|
+
});
|
|
424
|
+
|
|
376
425
|
contacts
|
|
377
426
|
.command('selftest')
|
|
378
427
|
.description('Drive the read path (search, find, resolve, best channel, timeline, touchpoint, brief) against a demo contact. Never dials or sends. --agent runs it through the attached agent tool scope')
|
|
@@ -16,11 +16,29 @@ export function registerConversationCommands(program, ctx) {
|
|
|
16
16
|
|
|
17
17
|
conv
|
|
18
18
|
.command('list')
|
|
19
|
-
.description('List conversations')
|
|
19
|
+
.description('List your chat conversations (newest first); --archive lists the imported conversation archive')
|
|
20
20
|
.option('--limit <n>', 'Max results', '20')
|
|
21
|
-
.option('--offset <n>', 'Skip results', '0')
|
|
21
|
+
.option('--offset <n>', 'Skip results (archive only)', '0')
|
|
22
|
+
.option('--archive', 'list the imported conversation archive instead of chat conversations')
|
|
22
23
|
.action(async (opts) => {
|
|
23
24
|
auth.requireAuth();
|
|
25
|
+
if (!opts.archive) {
|
|
26
|
+
const ep = resolveEndpoint('conversations.chat.list');
|
|
27
|
+
const data = await client.get(ep.path, { service: ep.service, query: { limit: opts.limit } });
|
|
28
|
+
const rows = data.conversations || [];
|
|
29
|
+
formatOutput({
|
|
30
|
+
columns: ['id', 'title', 'turns', 'agent', 'updated_at'],
|
|
31
|
+
headers: ['ID', 'Title', 'Turns', 'Agent', 'Updated'],
|
|
32
|
+
rows: rows.map((c) => ({
|
|
33
|
+
id: c.id,
|
|
34
|
+
title: (c.title || 'Untitled').slice(0, 50),
|
|
35
|
+
turns: c.turn_count ?? 0,
|
|
36
|
+
agent: c.agent || '-',
|
|
37
|
+
updated_at: c.updated_at ? new Date(c.updated_at).toLocaleString() : '-',
|
|
38
|
+
})),
|
|
39
|
+
}, { format: program.opts().output });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
24
42
|
const query = { limit: opts.limit, offset: opts.offset };
|
|
25
43
|
const data = await client.get(resolveEndpoint('conversations.list').path, { service: resolveEndpoint('conversations.list').service, query });
|
|
26
44
|
const conversations = data.conversations || data || [];
|
|
@@ -39,9 +57,22 @@ export function registerConversationCommands(program, ctx) {
|
|
|
39
57
|
conv
|
|
40
58
|
.command('get')
|
|
41
59
|
.argument('<id>', 'conversation ID')
|
|
42
|
-
.description('
|
|
43
|
-
.
|
|
60
|
+
.description('Show a chat conversation\'s turns; --archive reads the imported conversation archive')
|
|
61
|
+
.option('--limit <n>', 'Max turns (chat only)', '50')
|
|
62
|
+
.option('--archive', 'read the imported conversation archive instead of chat conversations')
|
|
63
|
+
.action(async (id, opts) => {
|
|
44
64
|
auth.requireAuth();
|
|
65
|
+
if (!opts.archive) {
|
|
66
|
+
const ep = resolveEndpoint('conversations.chat.turns', { id });
|
|
67
|
+
const data = await client.get(ep.path, { service: ep.service, query: { limit: opts.limit } });
|
|
68
|
+
const turns = data.turns || [];
|
|
69
|
+
formatOutput({
|
|
70
|
+
columns: ['ts', 'role', 'content'],
|
|
71
|
+
headers: ['Time', 'Role', 'Content'],
|
|
72
|
+
rows: turns.map((t) => ({ ts: t.ts ? new Date(t.ts).toLocaleString() : '-', role: t.role, content: t.content })),
|
|
73
|
+
}, { format: program.opts().output });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
45
76
|
const data = await client.get(resolveEndpoint('conversations.get', { id }).path, { service: resolveEndpoint('conversations.get', { id }).service });
|
|
46
77
|
formatDetail(data.conversation || data, { format: program.opts().output, title: 'Conversation' });
|
|
47
78
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox dashboard [page]`: open the web dashboard already signed in, via a one-time link
|
|
3
|
+
* (10 minutes, single use). Same engine as /dashboard in the Quox client (lib/quoxon/slash/dashboard.js).
|
|
4
|
+
* Over SSH it prints the link and does not open a browser. The link is a credential: it is printed
|
|
5
|
+
* to the terminal only, never logged.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
import { runDashboard, dashboardLines, PAGES } from '../quoxon/slash/dashboard.js';
|
|
9
|
+
import { supportsHyperlinks } from '../quoxon/markdown.js';
|
|
10
|
+
import { resolveDashboardBase, detectLanIp } from '../quoxon/hostUrls.js';
|
|
11
|
+
import { loadJourney } from '../quoxon/onboarding/journey.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Z2H-R4: the address to print, resolved exactly like /dashboard in the Quox client (api.js
|
|
15
|
+
* dashboardBase): server app URL, else a non-loopback profile URL, else the install finale's URL,
|
|
16
|
+
* else this machine's LAN IP. Over SSH a localhost link only works on the Quox box itself.
|
|
17
|
+
*/
|
|
18
|
+
export async function commandDashboardBase({ client, config, journey = loadJourney, lanIp = detectLanIp() } = {}) {
|
|
19
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
20
|
+
let serverApp = null;
|
|
21
|
+
try { const ep = resolveEndpoint('admin.instance.basics.get'); const r = await client.get(ep.path, { service: ep.service, retries: 0 }); serverApp = r?.effective?.appBaseUrl || null; } catch { serverApp = null; }
|
|
22
|
+
let journeyUrl = null; try { journeyUrl = journey()?.dashboardUrl || null; } catch { journeyUrl = null; }
|
|
23
|
+
return resolveDashboardBase({ serverApp, profileUrl, journeyUrl, lanIp }).url;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerDashboardCommands(program, ctx) {
|
|
27
|
+
const { client, auth, config, lanIp = detectLanIp, journey = loadJourney } = ctx;
|
|
28
|
+
program
|
|
29
|
+
.command('dashboard')
|
|
30
|
+
.description(`Open the web dashboard signed in (one-time link, 10 minutes). Pages: ${Object.keys(PAGES).join(', ')}`)
|
|
31
|
+
.argument('[page]', 'dashboard page to land on')
|
|
32
|
+
.action(async (page) => {
|
|
33
|
+
auth.requireAuth();
|
|
34
|
+
const baseUrl = await commandDashboardBase({ client, config, journey, lanIp: lanIp() });
|
|
35
|
+
const r = await runDashboard({ page, baseUrl, mint: () => { const ep = resolveEndpoint('auth.login-link'); return client.post(ep.path, { service: ep.service, body: {}, retries: 0 }); } });
|
|
36
|
+
if (program.opts().output === 'json') {
|
|
37
|
+
console.log(JSON.stringify({ kind: r.kind, url: r.url ?? null, opened: Boolean(r.opened), remote: Boolean(r.remote), error: r.error ?? null }, null, 2));
|
|
38
|
+
} else {
|
|
39
|
+
for (const l of dashboardLines(r, { hyperlinks: supportsHyperlinks() })) console.log(l.text);
|
|
40
|
+
}
|
|
41
|
+
if (r.kind === 'error') process.exitCode = 1;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -15,17 +15,38 @@ export function registerDepartmentCommands(program, ctx) {
|
|
|
15
15
|
|
|
16
16
|
dept
|
|
17
17
|
.command('list')
|
|
18
|
-
.description('List departments')
|
|
19
|
-
.
|
|
18
|
+
.description('List departments (all divisions unless --division; division ids: quox org divisions list)')
|
|
19
|
+
.option('--division <divId>', 'only this division')
|
|
20
20
|
.action(async (options) => {
|
|
21
21
|
auth.requireAuth();
|
|
22
22
|
const orgId = resolveOrgId(auth, program);
|
|
23
|
-
const
|
|
23
|
+
const listFor = async (division) => {
|
|
24
|
+
const ep = resolveEndpoint('divisions.departments.list', { orgId, division });
|
|
25
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
26
|
+
return data.departments || data || [];
|
|
27
|
+
};
|
|
28
|
+
if (options.division) {
|
|
29
|
+
formatOutput({
|
|
30
|
+
columns: ['id', 'name', 'description', 'created_at'],
|
|
31
|
+
headers: ['ID', 'Name', 'Description', 'Created'],
|
|
32
|
+
rows: await listFor(options.division),
|
|
33
|
+
}, { format: program.opts().output });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// quox#922: no --division lists every division's departments, so the read
|
|
37
|
+
// needs no id the department group cannot show.
|
|
38
|
+
const divEp = resolveEndpoint('divisions.list', { orgId });
|
|
39
|
+
const divData = await client.get(divEp.path, { service: divEp.service });
|
|
40
|
+
const divisions = divData.divisions || divData || [];
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (const d of divisions) {
|
|
43
|
+
for (const dept of await listFor(d.id)) rows.push({ ...dept, division: d.name || d.id });
|
|
44
|
+
}
|
|
24
45
|
formatOutput({
|
|
25
|
-
columns: ['id', 'name', 'description', 'created_at'],
|
|
26
|
-
headers: ['ID', 'Name', 'Description', 'Created'],
|
|
27
|
-
rows
|
|
28
|
-
}, { format: program.opts().output });
|
|
46
|
+
columns: ['id', 'name', 'division', 'description', 'created_at'],
|
|
47
|
+
headers: ['ID', 'Name', 'Division', 'Description', 'Created'],
|
|
48
|
+
rows,
|
|
49
|
+
}, { format: program.opts().output, emptyMessage: 'No departments.' });
|
|
29
50
|
});
|
|
30
51
|
|
|
31
52
|
dept
|
|
@@ -164,10 +164,13 @@ export function registerDevResidentCommands(dev, ctx) {
|
|
|
164
164
|
const slotName = resolveSlotName(options, hostId);
|
|
165
165
|
const sessionName = options.sessionName || defaultSessionName(slotName);
|
|
166
166
|
|
|
167
|
+
let released = true;
|
|
168
|
+
let killed = true;
|
|
167
169
|
try {
|
|
168
170
|
const releaseEp = resolveEndpoint('v1.slots.release', { name: slotName });
|
|
169
171
|
await client.post(releaseEp.path, { service: releaseEp.service, body: { org_id: orgId, force: true } });
|
|
170
172
|
} catch (err) {
|
|
173
|
+
released = false;
|
|
171
174
|
printWarning(`Releasing ${slotName} failed (continuing): ${err.message}`);
|
|
172
175
|
}
|
|
173
176
|
|
|
@@ -175,10 +178,11 @@ export function registerDevResidentCommands(dev, ctx) {
|
|
|
175
178
|
const killEp = resolveEndpoint('v1.dev.sessions.kill', { hostId, sessionId: sessionName, byName: true });
|
|
176
179
|
await client.delete(killEp.path, { service: killEp.service });
|
|
177
180
|
} catch (err) {
|
|
181
|
+
killed = false;
|
|
178
182
|
printWarning(`Killing tmux session ${sessionName} on ${hostId} failed (continuing): ${err.message}`);
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
return { host_id: hostId, slot_name: slotName, session_name: sessionName };
|
|
185
|
+
return { host_id: hostId, slot_name: slotName, session_name: sessionName, released, killed };
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
resident
|
|
@@ -290,8 +294,16 @@ export function registerDevResidentCommands(dev, ctx) {
|
|
|
290
294
|
.action(async (options) => {
|
|
291
295
|
auth.requireAuth();
|
|
292
296
|
const report = await releaseAndKill(options);
|
|
293
|
-
|
|
294
|
-
|
|
297
|
+
// #1018: only claim what happened, in the card title too; a retire that left the slot held or the session running failed.
|
|
298
|
+
const outcome = report.released && report.killed ? 'Resident retired'
|
|
299
|
+
: report.released || report.killed ? 'Resident retire incomplete' : 'Resident retire failed';
|
|
300
|
+
formatDetail(report, { format: program.opts().output, title: `${outcome} on ${report.host_id}` });
|
|
301
|
+
if (report.released && report.killed) {
|
|
302
|
+
printSuccess(`Resident retired on ${report.host_id}: slot ${report.slot_name} released, session ${report.session_name} killed.`);
|
|
303
|
+
} else {
|
|
304
|
+
printWarning(`Resident NOT fully retired on ${report.host_id}: slot ${report.slot_name} ${report.released ? 'released' : 'NOT released'}, session ${report.session_name} ${report.killed ? 'killed' : 'NOT killed'}.`);
|
|
305
|
+
process.exitCode = 1;
|
|
306
|
+
}
|
|
295
307
|
});
|
|
296
308
|
|
|
297
309
|
resident
|
|
@@ -29,7 +29,7 @@ import { resolveEndpoint } from '../api/endpoints.js';
|
|
|
29
29
|
import { resolveOrgId } from './helpers.js';
|
|
30
30
|
import { loadResidentProfile, expandHome, deriveSupervisorEnv } from '../core/residentAuth.js';
|
|
31
31
|
import { createInitialPumpState, runPumpOnce } from '../core/residentPump.js';
|
|
32
|
-
import { createInitialAlarmState, runWardenPass } from '../core/residentAlarm.js';
|
|
32
|
+
import { createInitialAlarmState, runWardenPass, sendAlarm } from '../core/residentAlarm.js';
|
|
33
33
|
import {
|
|
34
34
|
DEFAULT_SETTINGS,
|
|
35
35
|
DEFAULT_IDLE_RECHECK_SECONDS,
|
|
@@ -344,7 +344,17 @@ export function registerDevSupervisorCommands(dev, ctx) {
|
|
|
344
344
|
await client.post(ep.path, { service: ep.service, body: { ...fields, slot_name: slotName, org_id: orgId } });
|
|
345
345
|
};
|
|
346
346
|
|
|
347
|
+
// GRIPE-797: one owner alert per auth-expiry episode, same transport
|
|
348
|
+
// order as the warden alarm (tg-send.sh first, slot say fallback).
|
|
349
|
+
const alertOwner = async (text) => {
|
|
350
|
+
const script = ctx.tgSendScriptPath || join(homedir(), '.claude', 'skills', 'telegram-owner', 'tg-send.sh');
|
|
351
|
+
await sendAlarm({
|
|
352
|
+
tgSend: async (session, t) => { await (ctx.execFileAsync || promisify(execFile))('bash', [script, session, t]); },
|
|
353
|
+
postSlotNotice: postReply, text, sessionName: `SUPERVISOR-${hostId}`, log,
|
|
354
|
+
});
|
|
355
|
+
};
|
|
347
356
|
const commonPollArgs = {
|
|
357
|
+
alertOwner,
|
|
348
358
|
hostId,
|
|
349
359
|
slot: slotName,
|
|
350
360
|
selfIdentity,
|
|
@@ -633,6 +643,8 @@ export function registerDevSupervisorCommands(dev, ctx) {
|
|
|
633
643
|
}
|
|
634
644
|
} catch (err) {
|
|
635
645
|
log('error', `poll failed: ${err.message}`);
|
|
646
|
+
// #1018: a single pass that failed is a failed run, not a quiet success.
|
|
647
|
+
if (options.once) process.exitCode = 1;
|
|
636
648
|
if (continuous && waitMode) {
|
|
637
649
|
waitFailures += 1;
|
|
638
650
|
if (waitFailures >= DEFAULT_WAIT_FAILURE_THRESHOLD) {
|
package/lib/commands/discord.js
CHANGED
|
@@ -25,14 +25,10 @@ const QDP = process.env.QDP_CLI_PATH
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Per-verb usage, mirrored from qdp.mjs's own header doc block (bin/qdp.mjs
|
|
28
|
-
* in the quox-discord-pro repo)
|
|
29
|
-
* own
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* straight through to qdp — which silently ran the verb instead of showing
|
|
33
|
-
* help. Worst case for a mutating verb (bind/unbind/provision): "--help"
|
|
34
|
-
* would have actually bound/unbound/provisioned a channel. This map is
|
|
35
|
-
* checked BEFORE qdp is ever spawned so that can't happen (quox#603).
|
|
28
|
+
* in the quox-discord-pro repo), shown in each subcommand's --help. qdp has no
|
|
29
|
+
* --help handling of its own; before quox#1077 this command hand-rolled arg
|
|
30
|
+
* dispatch and had to intercept --help itself (quox#603). Commander
|
|
31
|
+
* subcommands now answer --help before any action runs.
|
|
36
32
|
*/
|
|
37
33
|
const VERB_USAGE = {
|
|
38
34
|
status: 'qdp status daemon liveness, instances, binding count',
|
|
@@ -56,41 +52,81 @@ const VERB_USAGE = {
|
|
|
56
52
|
+ ' --dry-run print exactly what it WOULD do, change nothing',
|
|
57
53
|
};
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
/**
|
|
56
|
+
* qdp verbs that only READ (quox#1077). Each was checked against bin/qdp.mjs:
|
|
57
|
+
* status/instances/bindings print config.json, the pid file and the systemd
|
|
58
|
+
* state; gaps GETs the open support-gap inbox items; health reads the pid and
|
|
59
|
+
* systemd state; doctor GETs the control API /health and Discord's channel,
|
|
60
|
+
* webhook and @me endpoints. None issues PUT/POST/PATCH/DELETE or writes a
|
|
61
|
+
* file. bind, unbind and provision PUT/DELETE bindings and create channels.
|
|
62
|
+
*/
|
|
63
|
+
export const DISCORD_READ_VERBS = ['status', 'instances', 'bindings', 'gaps', 'health', 'doctor'];
|
|
64
|
+
export const DISCORD_WRITE_VERBS = ['bind', 'unbind', 'provision'];
|
|
65
|
+
|
|
66
|
+
// No "Usage:" line here: the matrix's --help walker (scripts/matrix/cli-tree.mjs) reads one as the command's usage.
|
|
67
|
+
const GENERAL_USAGE = Object.values(VERB_USAGE).map((u) => ` ${u}`).join('\n')
|
|
61
68
|
+ '\n\nRun `quox discord <verb> --help` for a single verb\'s usage.';
|
|
62
69
|
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
65
|
-
process.
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
function runQdp(verb, args = []) {
|
|
71
|
+
if (!fs.existsSync(QDP)) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`Discord Pro control CLI (qdp) not found at ${QDP}\n` +
|
|
74
|
+
'Set QDP_CLI_PATH if it lives elsewhere, or install the quox-discord-pro daemon on this host.\n',
|
|
75
|
+
);
|
|
76
|
+
process.exit(2);
|
|
68
77
|
}
|
|
78
|
+
const r = spawnSync(process.execPath, [QDP, verb, ...args], { stdio: 'inherit' });
|
|
79
|
+
if (r.error) {
|
|
80
|
+
process.stderr.write(`qdp failed to run: ${r.error.message}\n`);
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
process.exit(r.status ?? 2);
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
const VERB_DESC = { unbind: 'remove a channel binding', doctor: 'per-binding view/webhook/liveness check (exits non-zero if any binding is broken)' };
|
|
87
|
+
const firstLine = (verb) => VERB_DESC[verb] || VERB_USAGE[verb].split('\n')[0].replace(/^qdp \S+(?: [<[][^\]>]*[>\]])*\s+/, '').trim();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One commander subcommand per qdp verb (quox#1077). It used to be a single
|
|
91
|
+
* `discord [args...]` leaf, so the CLI leaf policy could only class the whole
|
|
92
|
+
* group, and it classed it write: a read like `quox discord bindings` was held
|
|
93
|
+
* for approval. Per-verb leaves let reads be read and keep bind/unbind/provision
|
|
94
|
+
* write. Commander's own --help runs before any action, so `--help` can never
|
|
95
|
+
* reach qdp and run a mutating verb (quox#603).
|
|
96
|
+
*/
|
|
71
97
|
export function registerDiscordCommands(program) {
|
|
72
|
-
program
|
|
73
|
-
.command('discord
|
|
74
|
-
.description('Quox Discord Pro daemon: status | instances | bindings |
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
const discord = program
|
|
99
|
+
.command('discord')
|
|
100
|
+
.description('Quox Discord Pro daemon: status | instances | bindings | gaps | health | doctor | bind | unbind | provision')
|
|
101
|
+
.addHelpText('after', `\nPer-verb usage (each verb shells straight through to qdp):\n${GENERAL_USAGE}`);
|
|
102
|
+
|
|
103
|
+
for (const verb of DISCORD_READ_VERBS) {
|
|
104
|
+
discord
|
|
105
|
+
.command(verb)
|
|
106
|
+
.description(firstLine(verb) || verb)
|
|
107
|
+
.addHelpText('after', `\nqdp usage:\n ${VERB_USAGE[verb]}`)
|
|
108
|
+
.action(() => runQdp(verb));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const writeArgs = { bind: '<channelId> <mode> [pairs...]', unbind: '<channelId>', provision: '<agentId> [args...]' };
|
|
112
|
+
for (const verb of DISCORD_WRITE_VERBS) {
|
|
113
|
+
const leaf = discord
|
|
114
|
+
.command(`${verb} ${writeArgs[verb]}`);
|
|
115
|
+
// Only provision has a dry run in qdp. Declaring it here keeps it in --help and lets the
|
|
116
|
+
// root guard refuse `discord bind|unbind --dry-run` instead of binding for real.
|
|
117
|
+
if (verb === 'provision') leaf.option('--dry-run', 'print exactly what it WOULD do, change nothing');
|
|
118
|
+
leaf
|
|
119
|
+
.description(firstLine(verb) || verb)
|
|
120
|
+
.addHelpText('after', `\nqdp usage:\n ${VERB_USAGE[verb]}`)
|
|
121
|
+
// qdp parses its own options (provision --mode/--host/...), so they pass through unchanged.
|
|
122
|
+
.allowUnknownOption(true)
|
|
123
|
+
.action((...a) => {
|
|
124
|
+
const cmd = a[a.length - 1];
|
|
125
|
+
const args = [...cmd.args];
|
|
126
|
+
// --dry-run is also a root quox flag, so commander hands it to the root and it never reaches
|
|
127
|
+
// cmd.args: a requested dry run of provision used to provision for real. Forward it explicitly.
|
|
128
|
+
if (cmd.optsWithGlobals().dryRun && !args.includes('--dry-run')) args.push('--dry-run');
|
|
129
|
+
runQdp(verb, args);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
96
132
|
}
|