quoxcli 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/admin.js +7 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/governance.js +7 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +58 -0
- package/lib/api/endpoints/index.js +4 -0
- package/lib/api/endpoints/lucid.js +17 -0
- package/lib/api/endpoints/mcp.js +35 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/notesq.js +2 -0
- package/lib/api/endpoints/quoxplan.js +51 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/actions.js +51 -13
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agentAttach.js +15 -0
- package/lib/commands/approvals.js +193 -3
- package/lib/commands/ask.js +1 -1
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +15 -0
- package/lib/commands/claude.js +4 -1
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +2 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/evidence.js +2 -1
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +78 -5
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +30 -3
- package/lib/commands/lucid.js +7 -0
- package/lib/commands/lucidGoals.js +389 -0
- package/lib/commands/mail.js +427 -25
- package/lib/commands/mcp.js +190 -0
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +69 -144
- package/lib/commands/monitoring.js +68 -0
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +617 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +52 -12
- package/lib/commands/plan.js +487 -52
- package/lib/commands/provider.js +29 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/run.js +2 -1
- package/lib/commands/server.js +37 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/skills.js +4 -1
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/volt.js +6 -1
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +880 -0
- package/lib/commands/wazuhIntake.js +358 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +27 -1
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +42 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/governedSession.js +135 -5
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/offload.js +120 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +30 -5
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/selfUpdate.js +2 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/EXTENSIONS.md +142 -19
- package/lib/quoxon/api.js +189 -34
- package/lib/quoxon/app.js +1277 -188
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +56 -2
- package/lib/quoxon/approvalSentence.js +8 -4
- package/lib/quoxon/approvalsView.js +72 -12
- package/lib/quoxon/ask.js +4 -2
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +8 -2
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +8 -1
- package/lib/quoxon/commands.js +42 -12
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/delegationDragon.js +49 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +118 -0
- package/lib/quoxon/dragonCompanion.js +37 -7
- package/lib/quoxon/dragonMoments.js +78 -0
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +55 -8
- package/lib/quoxon/extensions.js +400 -38
- package/lib/quoxon/foreignText.js +49 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/launcher.js +2 -1
- package/lib/quoxon/loopCard.js +7 -2
- package/lib/quoxon/loopCards.js +17 -8
- package/lib/quoxon/loopStream.js +20 -9
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +27 -6
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/dockerOffer.js +3 -1
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +22 -0
- package/lib/quoxon/print.js +2 -0
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/render.js +68 -16
- package/lib/quoxon/resend.js +14 -0
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +3 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +88 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +48 -11
- package/lib/quoxon/slash/broadcastRun.js +24 -7
- package/lib/quoxon/slash/cliLeaves.json +98 -1
- package/lib/quoxon/slash/dashboard.js +2 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/listView.js +2 -1
- package/lib/quoxon/slash/palette.js +63 -1
- package/lib/quoxon/slash/paletteController.js +15 -5
- package/lib/quoxon/slash/restart.js +9 -4
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +24 -3
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/slotRun.js +2 -0
- package/lib/quoxon/suggestCommand.js +1 -1
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/webPty.js +15 -0
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +42 -0
- package/lib/render/planBrief.js +614 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +61 -3
- package/package.json +3 -2
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
11
|
-
import { capabilityLines, findCapability, offerFor } from '../quoxon/capabilities.js';
|
|
11
|
+
import { capabilityLines, findCapability, offerFor, needsSetupNames } from '../quoxon/capabilities.js';
|
|
12
12
|
|
|
13
13
|
export function registerCapabilitiesCommands(program, ctx) {
|
|
14
14
|
const { client, auth } = ctx;
|
|
15
15
|
program
|
|
16
16
|
.command('capabilities [name]')
|
|
17
17
|
.description('What this Quox can do for your org: ready, needs setup, not available, planned')
|
|
18
|
+
.addHelpText('after', '\nAdd --json for the full list (id, name, availability, reason, setup, permissions_ok).')
|
|
18
19
|
.action(async (name) => {
|
|
19
20
|
auth.requireAuth();
|
|
20
21
|
const ep = resolveEndpoint('v1.capabilities');
|
|
21
22
|
const data = await client.get(ep.path, { service: ep.service });
|
|
22
|
-
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
23
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ needs_setup: needsSetupNames(data?.capabilities), ...data }, null, 2)); return; }
|
|
23
24
|
const res = { ok: Array.isArray(data?.capabilities), capabilities: data?.capabilities, error: 'unexpected response' };
|
|
24
25
|
if (name) {
|
|
25
26
|
const c = findCapability(name, res);
|
package/lib/commands/chat.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { formatDetail } from '../core/output.js';
|
|
7
|
+
import { eventLine, announceMuted } from '../core/announce.js';
|
|
7
8
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
9
|
import { existsSync } from 'node:fs';
|
|
9
10
|
import { lastConversation, saveLastConversation } from './chatState.js';
|
|
@@ -70,6 +71,14 @@ function printTrace(ev) {
|
|
|
70
71
|
if (f) process.stderr.write((TONE_PAINT[f.tone] || chalk.cyan)(f.text) + '\n');
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/** A turn's quox_event frame as its QuoxGlyph line on stderr (stdout stays the answer); silent under --json/--quiet. */
|
|
75
|
+
export function printQuoxEvent(ev) {
|
|
76
|
+
if (announceMuted()) return null;
|
|
77
|
+
const q = eventLine(ev);
|
|
78
|
+
if (q) process.stderr.write(`${q}\n`);
|
|
79
|
+
return q;
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
function generateQueryId() {
|
|
74
83
|
return `CLI_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
75
84
|
}
|
|
@@ -81,6 +90,7 @@ function generateQueryId() {
|
|
|
81
90
|
export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel, trace = false, companionPath, companionDisplay } = {}) {
|
|
82
91
|
const queryId = generateQueryId();
|
|
83
92
|
const body = buildStreamQuery({ queryId, message, agent, sessionId, context });
|
|
93
|
+
body.turn_id = queryId; // an explicit turn id selects the v1 event stream: tool and quox_event frames only flow with it
|
|
84
94
|
|
|
85
95
|
let text = '';
|
|
86
96
|
let resultSessionId = sessionId;
|
|
@@ -145,6 +155,10 @@ export async function consumeStream(client, { message, agent, sessionId, context
|
|
|
145
155
|
write('\n');
|
|
146
156
|
break;
|
|
147
157
|
|
|
158
|
+
case 'quox_event':
|
|
159
|
+
printQuoxEvent(event);
|
|
160
|
+
break;
|
|
161
|
+
|
|
148
162
|
case 'error':
|
|
149
163
|
throw new Error(event.error || 'Stream error');
|
|
150
164
|
|
|
@@ -361,6 +375,7 @@ export function registerChatCommands(program, ctx) {
|
|
|
361
375
|
});
|
|
362
376
|
spinner.stop();
|
|
363
377
|
noteConversation(convId);
|
|
378
|
+
for (const qe of Array.isArray(data.quox_events) ? data.quox_events : []) printQuoxEvent(qe); // the server's per-turn Quox effects, when it returns them
|
|
364
379
|
|
|
365
380
|
if (program.opts().output === 'json') {
|
|
366
381
|
console.log(JSON.stringify({ ...data, conversation_id: convId || null }, null, 2));
|
package/lib/commands/claude.js
CHANGED
|
@@ -29,6 +29,7 @@ export {
|
|
|
29
29
|
*/
|
|
30
30
|
export function parseClaudeArgs(raw) {
|
|
31
31
|
let noTmux = false;
|
|
32
|
+
let force = false;
|
|
32
33
|
let as;
|
|
33
34
|
let slot;
|
|
34
35
|
const args = [];
|
|
@@ -38,6 +39,7 @@ export function parseClaudeArgs(raw) {
|
|
|
38
39
|
if (past) { args.push(a); continue; }
|
|
39
40
|
if (a === '--') { past = true; args.push(a); continue; }
|
|
40
41
|
if (a === '--no-tmux') noTmux = true;
|
|
42
|
+
else if (a === '--force') force = true;
|
|
41
43
|
else if ((a === '--as' || a === '--slot') && raw[i + 1] && !raw[i + 1].startsWith('-')) {
|
|
42
44
|
if (a === '--as') as = raw[i + 1]; else slot = raw[i + 1];
|
|
43
45
|
i++;
|
|
@@ -72,7 +74,7 @@ export function parseClaudeArgs(raw) {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
if (name !== undefined && !validateName(name)) name = undefined;
|
|
75
|
-
return { args, name, resume, noTmux, as, slot };
|
|
77
|
+
return { args, name, resume, noTmux, ...(force ? { force } : {}), as, slot };
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/**
|
|
@@ -151,6 +153,7 @@ quox's own flags (consumed here, not passed to claude):
|
|
|
151
153
|
--as <agent> start already attached to a Quox agent
|
|
152
154
|
--slot <name> start already holding a dev slot
|
|
153
155
|
--no-tmux run claude directly, without tmux
|
|
156
|
+
--force with --resume: start a second copy even if that conversation is already running
|
|
154
157
|
-n, --name <n> name the session (also names the tmux session; claude's own flag)
|
|
155
158
|
--resume <n> resume the named session (claude's own flag)
|
|
156
159
|
-h, --help show this help
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compliance commands: scheduled compliance report jobs.
|
|
3
|
+
*
|
|
4
|
+
* `compliance jobs list` calls GET /compliance/jobs (services/collector/server.js),
|
|
5
|
+
* the same route the compliance_list_jobs agent tool uses. Jobs can carry an
|
|
6
|
+
* optional name (set via the compliance_schedule_job tool / POST body `name`).
|
|
7
|
+
*
|
|
8
|
+
* `compliance reports list|show` read GET /compliance/reports and /compliance/reports/:id, the
|
|
9
|
+
* routes compliance_get_reports reads (org-scoped by the caller's session). The ORG column is
|
|
10
|
+
* always shown: a row with no org (org_id null) is printed as `none`, never hidden.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { formatOutput, formatDetail } from '../core/output.js';
|
|
14
|
+
import { resolveOrgId } from './helpers.js';
|
|
15
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
16
|
+
|
|
17
|
+
export function registerComplianceCommands(program, ctx) {
|
|
18
|
+
const { client, auth } = ctx;
|
|
19
|
+
|
|
20
|
+
const compliance = program
|
|
21
|
+
.command('compliance')
|
|
22
|
+
.description('Compliance: generated reports and scheduled report jobs');
|
|
23
|
+
|
|
24
|
+
const jobs = compliance
|
|
25
|
+
.command('jobs')
|
|
26
|
+
.description('Scheduled compliance report jobs');
|
|
27
|
+
|
|
28
|
+
jobs
|
|
29
|
+
.command('list')
|
|
30
|
+
.description('List scheduled compliance report jobs (with NAME)')
|
|
31
|
+
.option('--status <status>', 'filter by status (active|paused|error)')
|
|
32
|
+
.action(async (options) => {
|
|
33
|
+
auth.requireAuth();
|
|
34
|
+
const orgId = resolveOrgId(auth, program);
|
|
35
|
+
const ep = resolveEndpoint('compliance.jobs.list');
|
|
36
|
+
const data = await client.get(ep.path, { service: ep.service, query: { org_id: orgId } });
|
|
37
|
+
let rows = data.jobs || [];
|
|
38
|
+
if (options.status) rows = rows.filter((j) => j.status === options.status);
|
|
39
|
+
formatOutput({
|
|
40
|
+
columns: ['id', 'name', 'format', 'schedule_interval', 'status', 'next_run'],
|
|
41
|
+
headers: ['ID', 'NAME', 'Format', 'Interval', 'Status', 'Next run'],
|
|
42
|
+
rows: rows.map((j) => ({ ...j, name: j.name || '-' })),
|
|
43
|
+
}, { format: program.opts().output, emptyMessage: 'No compliance jobs found.', raw: rows });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const reports = compliance
|
|
47
|
+
.command('reports')
|
|
48
|
+
.description('Generated compliance reports (read only)');
|
|
49
|
+
|
|
50
|
+
const scoreOf = (r) => (r.summary && typeof r.summary === 'object' && r.summary.overallScore != null ? r.summary.overallScore : '-');
|
|
51
|
+
|
|
52
|
+
reports
|
|
53
|
+
.command('list')
|
|
54
|
+
.description('List generated compliance reports: framework, period, score, sampled evidence, ORG')
|
|
55
|
+
.option('--format <format>', 'only this framework (soc2, hipaa, gdpr, iso27001, euAiAct, iso42001, nistAiRmf)')
|
|
56
|
+
.option('--limit <n>', 'max rows', '20')
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
auth.requireAuth();
|
|
59
|
+
const orgId = resolveOrgId(auth, program);
|
|
60
|
+
const ep = resolveEndpoint('compliance.reports.list');
|
|
61
|
+
const query = { org_id: orgId, limit: String(options.limit || 20) };
|
|
62
|
+
if (options.format) query.format = options.format;
|
|
63
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
64
|
+
let rows = data.reports || [];
|
|
65
|
+
// the route does not filter by format, so the flag is applied here too
|
|
66
|
+
if (options.format) rows = rows.filter((r) => r.format === options.format);
|
|
67
|
+
formatOutput({
|
|
68
|
+
columns: ['id', 'format', 'generated_at', 'period_days', 'score', 'status', 'sample', 'org'],
|
|
69
|
+
headers: ['ID', 'Format', 'Generated', 'Days', 'Score', 'Status', 'Evidence sample', 'ORG'],
|
|
70
|
+
rows: rows.map((r) => ({
|
|
71
|
+
...r,
|
|
72
|
+
period_days: r.period_days ?? '-',
|
|
73
|
+
score: scoreOf(r),
|
|
74
|
+
status: (r.summary && r.summary.overallStatus) || '-',
|
|
75
|
+
sample: (r.summary && Array.isArray(r.summary.evidenceSample) && r.summary.evidenceSample.length) ? r.summary.evidenceSample.join(' ') : '-',
|
|
76
|
+
org: r.org_id || 'none',
|
|
77
|
+
})),
|
|
78
|
+
}, { format: program.opts().output, emptyMessage: 'No compliance reports found.', raw: rows });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
reports
|
|
82
|
+
.command('show <id>')
|
|
83
|
+
.description('Show one generated report: framework, period, score, per-control status, sampled evidence')
|
|
84
|
+
.action(async (id) => {
|
|
85
|
+
auth.requireAuth();
|
|
86
|
+
const orgId = resolveOrgId(auth, program);
|
|
87
|
+
const ep = resolveEndpoint('compliance.reports.get', { id });
|
|
88
|
+
const data = await client.get(ep.path, { service: ep.service, query: { org_id: orgId } });
|
|
89
|
+
const r = data.report || data;
|
|
90
|
+
const fmt = program.opts().output;
|
|
91
|
+
if (fmt && fmt !== 'table') { formatDetail(r, { format: fmt }); return; }
|
|
92
|
+
const s = (r.summary && typeof r.summary === 'object') ? r.summary : {};
|
|
93
|
+
const body = (r.report_data && typeof r.report_data === 'object') ? r.report_data : {};
|
|
94
|
+
const lines = [
|
|
95
|
+
`Report ${r.id}`,
|
|
96
|
+
`Framework ${s.framework || body.framework || r.format}`,
|
|
97
|
+
`Org ${r.org_id || 'none'}`,
|
|
98
|
+
`Period ${r.period_start || '-'} .. ${r.period_end || '-'} (${r.period_days ?? '-'} days)`,
|
|
99
|
+
`Score ${s.overallScore ?? body.overallScore ?? '-'} status ${s.overallStatus || body.overallStatus || '-'}`,
|
|
100
|
+
`Evidence ${Array.isArray(s.evidenceSample) && s.evidenceSample.length ? s.evidenceSample.join(' ') : '-'}`,
|
|
101
|
+
];
|
|
102
|
+
const sections = body.sections && typeof body.sections === 'object' ? body.sections : {};
|
|
103
|
+
for (const [key, sec] of Object.entries(sections)) {
|
|
104
|
+
lines.push('', `${key} ${sec && sec.name ? sec.name : ''}`.trimEnd());
|
|
105
|
+
for (const c of (sec && Array.isArray(sec.controls) ? sec.controls : [])) lines.push(` ${String(c.id || '-').padEnd(10)} ${String(c.status || '-').padEnd(16)} ${c.name || ''}`);
|
|
106
|
+
}
|
|
107
|
+
for (const l of lines) console.log(l);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox connect <url>: point a profile at a QuoxCORE server on another machine and sign in (ONBOARD-P1).
|
|
3
|
+
*
|
|
4
|
+
* Probes <url>/health, and when the server answers with its own certificate shows its SHA256 fingerprint and,
|
|
5
|
+
* after a yes (--trust-cert, or the y/N), stores that certificate for THAT PROFILE ONLY. It is never the global
|
|
6
|
+
* NODE_EXTRA_CA_CERTS and TLS checks are never turned off. Then hands to `quox -p <profile> login`.
|
|
7
|
+
* One implementation with the TUI's /connect server (lib/quoxon/onboarding/connectServer.js).
|
|
8
|
+
*/
|
|
9
|
+
import { announce, announceOnFail } from '../core/announce.js';
|
|
10
|
+
import { QuoxUserError } from '../core/errors.js';
|
|
11
|
+
import { confirmInteractive } from '../core/confirm.js';
|
|
12
|
+
import { profileNameForUrl } from '../core/profileTrust.js';
|
|
13
|
+
import { loadConfig } from '../core/config.js';
|
|
14
|
+
|
|
15
|
+
/** H1: no terminal and no pin: the safer non-interactive choice is the fingerprint pin, --trust-cert the alternative. */
|
|
16
|
+
export const noTerminalTrustMessage = () => 'No terminal to answer y/N here. Read the certificate fingerprint above, then re-run with --fingerprint <sha256> to trust only that exact certificate (safer), or --trust-cert to trust whatever this server presents.';
|
|
17
|
+
|
|
18
|
+
export function registerConnectCommand(program, ctx) {
|
|
19
|
+
program
|
|
20
|
+
.command('connect')
|
|
21
|
+
.description('Connect to a Quox server on another machine: probe it, trust its certificate for one profile, sign in')
|
|
22
|
+
.argument('<url>', 'the server address, for example https://192.0.2.10:3000')
|
|
23
|
+
.option('--profile <name>', 'profile to create or update (default: named from the address; -p also works)')
|
|
24
|
+
.option('--trust-cert', "trust the server's own certificate for this profile without asking (its fingerprint is still printed)")
|
|
25
|
+
.option('--fingerprint <sha256>', "trust the server's own certificate only if its SHA256 fingerprint is exactly this (colons optional); implies --trust-cert for that one certificate")
|
|
26
|
+
.option('--email <email>', 'sign in with this email without a prompt (use with --password-stdin to run with no terminal)')
|
|
27
|
+
.option('--password-stdin', 'read the password from stdin, one line (never an argument)')
|
|
28
|
+
.addHelpText('after', '\nA new QuoxCORE serves its own self-signed certificate. Compare the SHA256 printed here with the one `quox server status` shows on the server before you trust it. The certificate is stored for the one profile; TLS checks are never turned off.')
|
|
29
|
+
.action(announceOnFail('quox', (url) => `connect ${url}`, async (url, options, cmd) => {
|
|
30
|
+
const { connectServer } = await import('../quoxon/onboarding/connectServer.js');
|
|
31
|
+
const root = cmd.parent?.opts?.() || {};
|
|
32
|
+
const profile = options.profile || root.profile || profileNameForUrl(url);
|
|
33
|
+
// H1: the password comes from stdin only, never argv.
|
|
34
|
+
let password = null;
|
|
35
|
+
if (options.passwordStdin) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
38
|
+
password = Buffer.concat(chunks).toString('utf8').split('\n')[0].replace(/\r$/, '');
|
|
39
|
+
if (!password) throw new QuoxUserError('--password-stdin given but stdin was empty. Pipe the password in, for example: printf %s "$PW" | quox connect <url> --email <email> --password-stdin', { exitCode: 2, code: 'empty_password' });
|
|
40
|
+
}
|
|
41
|
+
const email = options.email ? String(options.email).trim() : null;
|
|
42
|
+
const pin = options.fingerprint ? String(options.fingerprint).replace(/[:\s]/g, '').toUpperCase() : null;
|
|
43
|
+
const trust = async (card, peer) => {
|
|
44
|
+
if (pin) {
|
|
45
|
+
const got = String(peer?.chain?.[0]?.fingerprint256 || '').replace(/:/g, '').toUpperCase();
|
|
46
|
+
if (got === pin) return true;
|
|
47
|
+
throw new QuoxUserError(`The certificate ${url} presents has SHA256 ${peer?.chain?.[0]?.fingerprint256 || '(unreadable)'}, not the --fingerprint you gave. Nothing was saved.`, { exitCode: 2, code: 'fingerprint_mismatch' });
|
|
48
|
+
}
|
|
49
|
+
if (options.trustCert) return true;
|
|
50
|
+
if (!process.stdin.isTTY) throw new QuoxUserError(noTerminalTrustMessage(), { exitCode: 2, code: 'no_terminal' });
|
|
51
|
+
return confirmInteractive(`Trust this server's certificate for profile "${profile}" only?`, { flag: '--trust-cert', purpose: 'trust it' });
|
|
52
|
+
};
|
|
53
|
+
// loadConfig, not ctx.config: fresh each time, because the sign-in runs in a child process and writes the file.
|
|
54
|
+
// ctx.connectOverrides is for tests only.
|
|
55
|
+
const r = await connectServer(url, { profile, trust, email, password, loadConfigImpl: loadConfig, ...(ctx.connectOverrides || {}) });
|
|
56
|
+
if (r.action === 'connected') {
|
|
57
|
+
announce('quox', `connected ${url}`, { result: `profile ${profile}` });
|
|
58
|
+
console.log(`Connected as profile "${profile}". Use it with: quox -p ${profile} <command>. Make it your default with: quox config profile use ${profile}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// H1: no terminal and no way to sign in: say which flags are missing, exit 2 (not 130, which means "cancelled").
|
|
62
|
+
const missing = [!email && '--email <email>', !password && '--password-stdin'].filter(Boolean);
|
|
63
|
+
if (!process.stdin.isTTY && missing.length && r.action !== 'connected') {
|
|
64
|
+
throw new QuoxUserError(`No terminal to sign in with. Re-run with ${missing.join(' and ')}; nothing was left saved.`, { exitCode: 2, code: 'no_terminal' });
|
|
65
|
+
}
|
|
66
|
+
if (r.action === 'connect-cancelled') { announce('quox', `connect ${url}`, { result: 'cancelled', status: 'warn' }); process.exitCode = 130; return; }
|
|
67
|
+
throw new QuoxUserError(`Could not connect to ${url}. See the message above; nothing was left saved.`, { exitCode: 1, code: 'connect_failed' });
|
|
68
|
+
}));
|
|
69
|
+
}
|
package/lib/commands/contacts.js
CHANGED
|
@@ -408,6 +408,8 @@ export function registerContactsCommands(program, ctx) {
|
|
|
408
408
|
if (isJson(program)) return printJson(d);
|
|
409
409
|
console.log(`channel: ${d.channel || 'none allowed'}`);
|
|
410
410
|
for (const a of d.alternatives || []) console.log(` ${a.channel}: ${a.decision}${(a.reasons || []).length ? ` (${a.reasons.join(', ')})` : ''}`);
|
|
411
|
+
if (d.evaluated_for) console.log(`for: ${d.evaluated_for === 'agent' ? 'an agent' : 'you (a person)'}`);
|
|
412
|
+
if ((d.quiet_hours_for_agents || []).length) console.log(`quiet hours: an agent reaching them by ${d.quiet_hours_for_agents.join(', ')} now needs approval`);
|
|
411
413
|
announce('contacts best-channel', d.channel || 'none allowed');
|
|
412
414
|
});
|
|
413
415
|
|
package/lib/commands/dev.js
CHANGED
|
@@ -325,14 +325,12 @@ export function registerDevCommands(program, ctx) {
|
|
|
325
325
|
printInfo(`Terms hash: ${offer.termsSha256} (presentation ${offer.presentationVersion})`);
|
|
326
326
|
|
|
327
327
|
if (!options.iAccept) {
|
|
328
|
-
const {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}]);
|
|
335
|
-
if (!ans.confirm) {
|
|
328
|
+
const { confirmInteractive } = await import('../core/confirm.js');
|
|
329
|
+
const accepted = await confirmInteractive(
|
|
330
|
+
`Do you accept these terms, on behalf of your organisation, for exactly this pack content?`,
|
|
331
|
+
{ flag: '--i-accept', purpose: 'accept them as yourself' },
|
|
332
|
+
);
|
|
333
|
+
if (!accepted) {
|
|
336
334
|
printInfo('Not accepted. Nothing was recorded.');
|
|
337
335
|
return;
|
|
338
336
|
}
|
package/lib/commands/doctor.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
11
14
|
import { SERVICE_PORTS } from '../core/config.js';
|
|
12
15
|
import { emitCliEvent } from '../core/aee.js';
|
|
13
16
|
import { resolveCliVersion, checkCliDrift } from '../core/version.js';
|
|
@@ -20,7 +23,7 @@ const CHECKS = [
|
|
|
20
23
|
{ name: 'QuoxFlow', service: 'quoxflow', port: 3200, path: '/health' },
|
|
21
24
|
{ name: 'Files service', service: 'files', port: 3102, path: '/health' },
|
|
22
25
|
// External services
|
|
23
|
-
{ name: 'QuoxAgent daemon', port: 9847, path: '/health', url: 'http://localhost:9847' },
|
|
26
|
+
{ name: 'QuoxAgent daemon', port: 9847, path: '/health', url: 'http://localhost:9847', optionalUnlessEnrolled: true },
|
|
24
27
|
{ name: 'Qdrant vectors', port: 6333, path: '/', url: 'http://localhost:6333' },
|
|
25
28
|
];
|
|
26
29
|
|
|
@@ -40,11 +43,20 @@ async function checkEndpoint(url, path, timeoutMs = 3000) {
|
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* F2 (ONBOARD-P1): is a QuoxAgent installed/enrolled on THIS machine? Its config is the evidence: the system
|
|
48
|
+
* install writes /etc/quoxagent/quoxagent.yaml, a user install ~/.quoxagent/. Absent both, nothing is enrolled
|
|
49
|
+
* and a daemon that is not running is the normal state of a single-box install, not a critical.
|
|
50
|
+
*/
|
|
51
|
+
export function defaultAgentEnrolled({ exists = existsSync, home = homedir() } = {}) {
|
|
52
|
+
return ['/etc/quoxagent/quoxagent.yaml', join(home, '.quoxagent', 'quoxagent.yaml'), join(home, '.quoxagent', 'config.yaml')].some((p) => exists(p));
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/**
|
|
44
56
|
* Every doctor check, in this process: services, auth token, CLI version. Shared by `quox doctor` and the
|
|
45
57
|
* conversation's /doctor so the two can never disagree. -> [{ name, status, detail?, latency_ms?, ... }].
|
|
46
58
|
*/
|
|
47
|
-
export async function runDoctorChecks({ config }) {
|
|
59
|
+
export async function runDoctorChecks({ config, agentEnrolled = defaultAgentEnrolled } = {}) {
|
|
48
60
|
// Check all services in parallel — per-item try/catch so one failure doesn't lose all results
|
|
49
61
|
const checks = await Promise.all(
|
|
50
62
|
CHECKS.map(async (check) => {
|
|
@@ -71,6 +83,9 @@ export async function runDoctorChecks({ config }) {
|
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
}
|
|
86
|
+
if (result.status === 'fail' && check.optionalUnlessEnrolled && !agentEnrolled()) {
|
|
87
|
+
result = { status: 'skip', latency_ms: null, detail: 'optional: not running, and no QuoxAgent is enrolled on this machine (install one with the fleet enrol steps if you want a fleet)' };
|
|
88
|
+
}
|
|
74
89
|
return {
|
|
75
90
|
name: check.name,
|
|
76
91
|
port: check.port,
|
|
@@ -130,6 +145,7 @@ export function registerDoctorCommand(program, ctx) {
|
|
|
130
145
|
program
|
|
131
146
|
.command('doctor')
|
|
132
147
|
.description('Run platform health diagnostics')
|
|
148
|
+
.addHelpText('after', '\nAdd --json for machine output: { checks: [...], summary: { passed, warnings, failed } }.')
|
|
133
149
|
.action(async (opts, cmd) => {
|
|
134
150
|
const outputFormat = cmd.parent?.opts()?.output || 'table';
|
|
135
151
|
const isJson = outputFormat === 'json' || cmd.parent?.opts()?.json;
|
|
@@ -140,9 +156,10 @@ export function registerDoctorCommand(program, ctx) {
|
|
|
140
156
|
const passed = checks.filter(c => c.status === 'pass').length;
|
|
141
157
|
const warnings = checks.filter(c => c.status === 'warn').length;
|
|
142
158
|
const failed = checks.filter(c => c.status === 'fail').length;
|
|
159
|
+
const skipped = checks.filter(c => c.status === 'skip').length;
|
|
143
160
|
|
|
144
161
|
if (isJson) {
|
|
145
|
-
console.log(JSON.stringify({ checks, summary: { passed, warnings, failed } }, null, 2));
|
|
162
|
+
console.log(JSON.stringify({ checks, summary: { passed, warnings, failed, skipped } }, null, 2));
|
|
146
163
|
return;
|
|
147
164
|
}
|
|
148
165
|
|
|
@@ -151,6 +168,7 @@ export function registerDoctorCommand(program, ctx) {
|
|
|
151
168
|
for (const c of checks) {
|
|
152
169
|
const icon = c.status === 'pass' ? chalk.green(' ✓')
|
|
153
170
|
: c.status === 'warn' ? chalk.yellow(' ⚠')
|
|
171
|
+
: c.status === 'skip' ? chalk.dim(' ·')
|
|
154
172
|
: chalk.red(' ✗');
|
|
155
173
|
|
|
156
174
|
const latency = c.latency_ms != null ? chalk.dim(` (${c.latency_ms}ms)`) : '';
|
|
@@ -165,6 +183,7 @@ export function registerDoctorCommand(program, ctx) {
|
|
|
165
183
|
if (passed) parts.push(chalk.green(`${passed} passed`));
|
|
166
184
|
if (warnings) parts.push(chalk.yellow(`${warnings} warning${warnings > 1 ? 's' : ''}`));
|
|
167
185
|
if (failed) parts.push(chalk.red(`${failed} critical`));
|
|
186
|
+
if (skipped) parts.push(chalk.dim(`${skipped} optional, not set up`));
|
|
168
187
|
console.log(` ${parts.join(' ')}`);
|
|
169
188
|
console.log('');
|
|
170
189
|
|
package/lib/commands/evidence.js
CHANGED
|
@@ -53,7 +53,8 @@ export function registerEvidenceCommands(program, ctx) {
|
|
|
53
53
|
|
|
54
54
|
evidence
|
|
55
55
|
.command('status')
|
|
56
|
-
.description('Evidence stores (AEE, WARD, VOLT): size, rows, time span, growth, archive and backup, read-only')
|
|
56
|
+
.description('Evidence stores (AEE, WARD, VOLT): size, rows, time span, growth, archive and backup, read-only. Instance admin only')
|
|
57
|
+
.addHelpText('after', '\nInstance admin only: an org owner or admin who is not an instance admin gets a 403 (exit 4).')
|
|
57
58
|
.action(async () => {
|
|
58
59
|
auth.requireAuth();
|
|
59
60
|
const ep = resolveEndpoint('evidence.status');
|
package/lib/commands/gripe.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { formatDetail, printInfo, printWarning } from '../core/output.js';
|
|
25
|
-
import { events, line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
25
|
+
import { events, gripeFiledLine, line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
26
26
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
27
27
|
import { resolveOrgId } from './helpers.js';
|
|
28
28
|
|
|
@@ -50,20 +50,28 @@ export function registerGripeCommands(program, { client, auth }) {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
// GRIPE-QUEUE: a timeout / network / 5xx saves the gripe locally instead of losing it (/gripes in quox lists it).
|
|
54
|
+
const { gripeQueue, fileOrQueue } = await import('../quoxon/gripeQueue.js');
|
|
55
|
+
let rawData = null;
|
|
56
|
+
const f = await fileOrQueue({
|
|
57
|
+
text: complaint, repo: options.repo || null, queue: gripeQueue(), queueable: !!options.yes,
|
|
58
|
+
run: async (key) => {
|
|
59
|
+
const o = { ...options, idempotencyKey: key, queueRetryable: true };
|
|
60
|
+
rawData = await askGripe({ client, complaint, repo: options.repo, label: options.label, yes: !!options.yes, orgId });
|
|
61
|
+
if (program.opts().output === 'json') return { json: true };
|
|
62
|
+
return settleGripe({ client, data: rawData, complaint, options: o, orgId });
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!f.ok && f.queued) { printWarning(`${f.saved} (${f.message})`); process.exitCode = 1; return; }
|
|
66
|
+
if (!f.ok) throw new Error(f.message);
|
|
67
|
+
if (f.json) { formatDetail(rawData, { format: 'json' }); return; }
|
|
68
|
+
const r = f;
|
|
61
69
|
console.log('');
|
|
62
70
|
if (r.superseded) printInfo(SUPERSEDED_MESSAGE);
|
|
63
71
|
else console.log(r.response || '(no response)');
|
|
64
72
|
console.log('');
|
|
65
73
|
for (const n of r.notes) {
|
|
66
|
-
if (n.kind === 'created') printEvent(
|
|
74
|
+
if (n.kind === 'created') { printEvent(gripeFiledLine(n.url, complaint)); console.log(n.url); }
|
|
67
75
|
else if (n.kind === 'info') printInfo(n.text);
|
|
68
76
|
else printWarning(n.text);
|
|
69
77
|
}
|
|
@@ -117,7 +125,7 @@ export async function settleGripe({ client, data, complaint, options = {}, orgId
|
|
|
117
125
|
const notes = [];
|
|
118
126
|
let url = null;
|
|
119
127
|
const det = async () => {
|
|
120
|
-
const r = await fileDeterministically({ client, complaint, options, orgId, quiet: true });
|
|
128
|
+
const r = await fileDeterministically({ client, complaint, options, orgId, quiet: true, throwRetryable: !!options.queueRetryable });
|
|
121
129
|
notes.push(...r.notes); if (r.url) url = r.url;
|
|
122
130
|
return r;
|
|
123
131
|
};
|
|
@@ -181,7 +189,7 @@ async function promptConfirmPublic(repo) {
|
|
|
181
189
|
} finally { rl.close(); }
|
|
182
190
|
}
|
|
183
191
|
|
|
184
|
-
export async function fileDeterministically({ client, complaint, options, orgId, quiet = false, confirmPublic = null }) {
|
|
192
|
+
export async function fileDeterministically({ client, complaint, options, orgId, quiet = false, confirmPublic = null, throwRetryable = false }) {
|
|
185
193
|
const notes = [];
|
|
186
194
|
const say = (n) => { notes.push(n); if (quiet) return; if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url)); else if (n.kind === 'info') printInfo(n.text); else printWarning(n.text); };
|
|
187
195
|
const repoArg = options.repo || 'quoxai/quox';
|
|
@@ -201,9 +209,10 @@ export async function fileDeterministically({ client, complaint, options, orgId,
|
|
|
201
209
|
owner,
|
|
202
210
|
repo,
|
|
203
211
|
title,
|
|
204
|
-
body:
|
|
212
|
+
body: `${options.idempotencyKey ? `<!-- quox-gripe-key: ${options.idempotencyKey} -->\n` : ''}## Gripe\n${complaint}\n\n## Context\n- Filed deterministically by \`quox gripe --yes\` (the GRIPE agent run did not file; quox#478 guarantee path)\n- Priority: minor / off-task observation\n\n## Suggested Action\nInvestigate and fix if confirmed.`,
|
|
205
213
|
labels: [options.label || 'fleet/qa'],
|
|
206
214
|
...(orgId ? { org_id: orgId } : {}),
|
|
215
|
+
...(options.idempotencyKey ? { idempotency_key: options.idempotencyKey } : {}),
|
|
207
216
|
...(confirmed ? { confirm_public: true } : {}),
|
|
208
217
|
},
|
|
209
218
|
});
|
|
@@ -216,6 +225,8 @@ export async function fileDeterministically({ client, complaint, options, orgId,
|
|
|
216
225
|
say({ kind: 'warn', text: 'Deterministic file returned no issue URL; treat as NOT filed until you can see the issue.' });
|
|
217
226
|
return { filed: false, notes };
|
|
218
227
|
} catch (err) {
|
|
228
|
+
// GRIPE-QUEUE: a timeout / network / 5xx is "Quox did not answer", not a refusal; the caller queues it.
|
|
229
|
+
if (throwRetryable && (err?.status == null || Number(err.status) >= 500)) throw err;
|
|
219
230
|
say({ kind: 'warn', text: `Not filed: the GRIPE run did not file and the deterministic fallback failed too (${err.message}).` });
|
|
220
231
|
return { filed: false, error: err.message, notes };
|
|
221
232
|
}
|
package/lib/commands/inbox.js
CHANGED
|
@@ -9,6 +9,8 @@ import { attachWalkthrough } from './walkthrough.js';
|
|
|
9
9
|
import { getApprovalContext, formatApprovalContext, approvalListColumns } from '../core/approvalContext.js';
|
|
10
10
|
import { QuoxUserError } from '../core/errors.js';
|
|
11
11
|
import { resolutionFromItem, alreadyResolvedError } from '../core/inboxResolution.js';
|
|
12
|
+
import { withStepUp } from '../core/stepUpPrompt.js';
|
|
13
|
+
import { announce, announceFailure } from '../core/announce.js';
|
|
12
14
|
import chalk from 'chalk';
|
|
13
15
|
|
|
14
16
|
export function registerInboxCommands(program, ctx) {
|
|
@@ -55,7 +57,7 @@ export function registerInboxCommands(program, ctx) {
|
|
|
55
57
|
inbox
|
|
56
58
|
.command('list')
|
|
57
59
|
.description('List inbox items')
|
|
58
|
-
.option('--status <status>', 'filter: one status, a comma list (resolved,cancelled,expired), or all (includes answered approvals). Default: active only')
|
|
60
|
+
.option('--status <status>', 'filter: one status, a comma list (resolved,cancelled,expired,withdrawn), or all (includes answered approvals). Default: active only')
|
|
59
61
|
.option('--decision <decision>', 'with resolved/all: approved or rejected')
|
|
60
62
|
.option('--page <n>', 'page number (newest first)')
|
|
61
63
|
.option('--priority <priority>', 'filter: low, normal, high, urgent')
|
|
@@ -78,7 +80,15 @@ export function registerInboxCommands(program, ctx) {
|
|
|
78
80
|
columns: ['id', 'kind', 'priority', 'title', 'source', 'reason', 'status', 'created_at'],
|
|
79
81
|
headers: ['ID', 'Kind', 'Priority', 'Title', 'Source', 'Reason', 'Status', 'Created'],
|
|
80
82
|
rows: items,
|
|
81
|
-
}, {
|
|
83
|
+
}, {
|
|
84
|
+
format: program.opts().output,
|
|
85
|
+
emptyMessage: 'Nothing waiting for you. Try `--status all` for answered items, or `--awaiting-human` for approvals waiting on a human org-wide.',
|
|
86
|
+
});
|
|
87
|
+
const limit = Number(options.limit);
|
|
88
|
+
if (program.opts().output !== 'json' && items.length > 0 && Number.isFinite(limit) && items.length >= limit) {
|
|
89
|
+
const total = typeof data.total === 'number' ? data.total : null;
|
|
90
|
+
console.log(`showing ${items.length}${total && total > items.length ? ` of ${total}` : ''} · --limit to see more`);
|
|
91
|
+
}
|
|
82
92
|
});
|
|
83
93
|
|
|
84
94
|
inbox
|
|
@@ -116,9 +126,13 @@ export function registerInboxCommands(program, ctx) {
|
|
|
116
126
|
auth.requireAuth();
|
|
117
127
|
const orgId = resolveOrgId(auth, program);
|
|
118
128
|
const format = program.opts().output;
|
|
129
|
+
// DS-010 / #1335: a step-up sink answers 403 step_up_required; ask the person for their PIN (hidden, TTY only) and retry once.
|
|
119
130
|
const post = (itemId) => {
|
|
120
131
|
const ep = resolveEndpoint('inbox.respond', { orgId, itemId });
|
|
121
|
-
return
|
|
132
|
+
return withStepUp(
|
|
133
|
+
(headers) => client.post(ep.path, { service: ep.service, headers, body: { action: options.action, comment: options.comment } }),
|
|
134
|
+
{ client, orgId, io: ctx.stepUpIO, what: 'This approval' },
|
|
135
|
+
);
|
|
122
136
|
};
|
|
123
137
|
// Resolve an approvalId to its card (server-side on a current server, else the client scan). When
|
|
124
138
|
// this profile cannot see the card, still ask the server: it knows WHY (an agent login, another
|
|
@@ -161,6 +175,26 @@ export function registerInboxCommands(program, ctx) {
|
|
|
161
175
|
} catch { return null; }
|
|
162
176
|
}
|
|
163
177
|
|
|
178
|
+
inbox
|
|
179
|
+
.command('unread')
|
|
180
|
+
.description('Mark inbox items unread again (undo a mark-as-read), for you only')
|
|
181
|
+
.argument('<itemId...>', 'one or more inbox item IDs')
|
|
182
|
+
.action(async (ids) => {
|
|
183
|
+
auth.requireAuth();
|
|
184
|
+
const orgId = resolveOrgId(auth, program);
|
|
185
|
+
let done = 0;
|
|
186
|
+
for (const id of ids) {
|
|
187
|
+
try {
|
|
188
|
+
await client.patch(`${resolveEndpoint('inbox.get', { orgId, itemId: encodeURIComponent(id) }).path}/read`, { service: resolveEndpoint('inbox.get', { orgId, itemId: id }).service, body: { is_read: false } });
|
|
189
|
+
done++;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
announceFailure('inbox', `unread ${id}`, err);
|
|
192
|
+
throw err.status === 404 ? notFound(id, err) : err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
announce('inbox', `marked ${done} unread`, { result: ids.length === 1 ? ids[0] : `${done} items` });
|
|
196
|
+
});
|
|
197
|
+
|
|
164
198
|
inbox
|
|
165
199
|
.command('stats')
|
|
166
200
|
.description('Show inbox statistics')
|