quoxcli 0.3.1 → 0.5.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 +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- 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/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -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 +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -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/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -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 +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- 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/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -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/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence commands (DEVFLEET EV-1): `quox evidence status` shows size, rows, time span, growth, archive and
|
|
3
|
+
* backup for the AEE, WARD and VOLT stores. Read-only; the collector route is instance admin only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { formatOutput } from '../core/output.js';
|
|
8
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import { exportConversationBundle, newestConversationId } from '../evidence/bundleExport.js';
|
|
11
|
+
import { verifyEvidenceBundle } from '../evidence/bundleVerify.js';
|
|
12
|
+
|
|
13
|
+
const NA = 'n/a';
|
|
14
|
+
|
|
15
|
+
// verify_state from auth, said once: 'verified', 'never verified', 'verify failed', 'verify n/a'.
|
|
16
|
+
function verifyLabel(v) {
|
|
17
|
+
if (v === null || v === undefined || v === '') return 'verify n/a';
|
|
18
|
+
if (v === 'verified') return 'verified';
|
|
19
|
+
if (v === 'failed') return 'verify failed';
|
|
20
|
+
if (v === 'never_verified') return 'never verified';
|
|
21
|
+
return `verify ${String(v).replace(/_/g, ' ')}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function bytes(n) {
|
|
25
|
+
if (n === null || n === undefined) return NA;
|
|
26
|
+
if (n < 1024) return `${n} B`;
|
|
27
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
28
|
+
let v = n; let i = -1;
|
|
29
|
+
do { v /= 1024; i++; } while (v >= 1024 && i < units.length - 1);
|
|
30
|
+
return `${v.toFixed(1)} ${units[i]}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const val = (v) => (v === null || v === undefined ? NA : String(v));
|
|
34
|
+
const day = (iso) => (iso ? String(iso).slice(0, 10) : NA);
|
|
35
|
+
|
|
36
|
+
function archiveCell(a) {
|
|
37
|
+
if (a === null || a === undefined) return NA;
|
|
38
|
+
if (typeof a === 'string') return a;
|
|
39
|
+
return `${a.anchors} anchors${a.newest_archived_at ? `, newest ${day(a.newest_archived_at)}` : ''}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function diskCell(d) {
|
|
43
|
+
if (!d) return NA;
|
|
44
|
+
return `${bytes(d.free_bytes)} free of ${bytes(d.total_bytes)} (${d.mount})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function registerEvidenceCommands(program, ctx) {
|
|
48
|
+
const { client, auth } = ctx;
|
|
49
|
+
|
|
50
|
+
const evidence = program
|
|
51
|
+
.command('evidence')
|
|
52
|
+
.description('Evidence stores: AEE, WARD, VOLT');
|
|
53
|
+
|
|
54
|
+
evidence
|
|
55
|
+
.command('status')
|
|
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).')
|
|
58
|
+
.action(async () => {
|
|
59
|
+
auth.requireAuth();
|
|
60
|
+
const ep = resolveEndpoint('evidence.status');
|
|
61
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
62
|
+
if (!data || !Array.isArray(data.stores)) {
|
|
63
|
+
throw new Error(`Unexpected evidence status response${data && data.error ? `: ${data.error}` : ''}`);
|
|
64
|
+
}
|
|
65
|
+
const format = program.opts().output;
|
|
66
|
+
if (format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
67
|
+
|
|
68
|
+
const rows = data.stores.map((s) => ({
|
|
69
|
+
store: s.name,
|
|
70
|
+
size: bytes(s.bytes),
|
|
71
|
+
wal: bytes(s.wal_bytes),
|
|
72
|
+
rows: val(s.rows),
|
|
73
|
+
oldest: day(s.oldest),
|
|
74
|
+
newest: day(s.newest),
|
|
75
|
+
last7d: s.per_day && s.per_day.last_7d ? `${s.per_day.last_7d.per_day}/day` : NA,
|
|
76
|
+
archive: archiveCell(s.archive),
|
|
77
|
+
disk: diskCell(s.disk),
|
|
78
|
+
}));
|
|
79
|
+
formatOutput({
|
|
80
|
+
columns: ['store', 'size', 'wal', 'rows', 'oldest', 'newest', 'last7d', 'archive', 'disk'],
|
|
81
|
+
headers: ['Store', 'Size', 'WAL', 'Rows', 'Oldest', 'Newest', 'Last 7d', 'Archive', 'Disk'],
|
|
82
|
+
rows,
|
|
83
|
+
}, { format, full: true });
|
|
84
|
+
|
|
85
|
+
const b = data.backup || {};
|
|
86
|
+
if (b.last) {
|
|
87
|
+
const l = b.last;
|
|
88
|
+
const when = l.ts ? `${String(l.ts).slice(0, 10)} ${String(l.ts).slice(11, 16)} UTC` : NA;
|
|
89
|
+
const parts = [
|
|
90
|
+
b.age_hours === null || b.age_hours === undefined ? 'age n/a' : `${b.age_hours} h ago`,
|
|
91
|
+
l.count === null || l.count === undefined ? 'count n/a' : `${l.count} backups`,
|
|
92
|
+
(b.scope && b.scope.ts) || 'all orgs',
|
|
93
|
+
];
|
|
94
|
+
const line = `Last backup ${when} (${parts.join(', ')})${b.stale ? ' STALE' : ''}`;
|
|
95
|
+
console.log(b.stale ? chalk.yellow(line) : line);
|
|
96
|
+
// The newest backup can belong to any org; health and verify state are the caller's org only.
|
|
97
|
+
console.log(`Backup health (${(b.scope && b.scope.health) || 'your org'}): ${val(l.status)}, ${verifyLabel(l.verified)}`);
|
|
98
|
+
if (l.last_failed) console.log(` last failure: ${val(l.last_failed.error)} (${l.last_failed.at ? String(l.last_failed.at).slice(0, 16).replace('T', ' ') + ' UTC' : NA})`);
|
|
99
|
+
if (b.verify_error) console.log(` ${b.verify_error}`);
|
|
100
|
+
} else {
|
|
101
|
+
console.log(`Backup: ${NA}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const errors = [];
|
|
105
|
+
for (const s of data.stores) for (const e of s.errors || []) errors.push(`${s.name}: ${e}`);
|
|
106
|
+
if (b.error) errors.push(`backup: ${b.error}`);
|
|
107
|
+
if (errors.length) {
|
|
108
|
+
console.log(chalk.yellow('Errors'));
|
|
109
|
+
for (const e of errors) console.log(` ${e}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
evidence
|
|
113
|
+
.command('bundle')
|
|
114
|
+
.description('Export a signed, offline-verifiable evidence bundle (zip) of every AEE envelope, WARD receipt and VOLT run for one conversation')
|
|
115
|
+
.argument('[conversation]', 'conversation id (the corr); default: your newest chat conversation')
|
|
116
|
+
.option('--out <file.zip>', 'where to write it (default ~/quox-evidence/<conversation>-<timestamp>.zip)')
|
|
117
|
+
.option('--json', 'print the result as JSON')
|
|
118
|
+
.action(async (conversation, options) => {
|
|
119
|
+
auth.requireAuth();
|
|
120
|
+
const corr = conversation || await newestConversationId(client);
|
|
121
|
+
if (!corr) throw new Error('No conversation to export: pass a conversation id (`quox conversation list`).');
|
|
122
|
+
const r = await exportConversationBundle({ client, corr, out: options.out });
|
|
123
|
+
const failed = r.verify.checks.filter((c) => !c.ok);
|
|
124
|
+
if (options.json || program.opts().output === 'json') {
|
|
125
|
+
console.log(JSON.stringify({ path: r.path, conversation: r.corr, bytes: r.bytes, envelopes: r.envelopes, receipts: r.receipts, verified: r.verify.ok, failed_checks: failed }, null, 2));
|
|
126
|
+
} else {
|
|
127
|
+
console.log(`${r.verify.ok ? chalk.green('Evidence bundle written') : chalk.red('Evidence bundle written but FAILED verification')}: ${r.path}`);
|
|
128
|
+
console.log(` conversation ${r.corr} | ${r.envelopes} envelopes | ${r.receipts} WARD receipts | ${r.bytes} bytes`);
|
|
129
|
+
console.log(` verify offline: quox evidence verify ${r.path}`);
|
|
130
|
+
for (const c of failed) console.log(chalk.red(` FAILED ${c.name}: ${c.detail || ''}`));
|
|
131
|
+
}
|
|
132
|
+
if (!r.verify.ok) process.exitCode = 1;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
evidence
|
|
136
|
+
.command('verify')
|
|
137
|
+
.description('Verify an evidence bundle zip offline: file hashes, envelope list hash, counts and the Ed25519 signature (no network)')
|
|
138
|
+
.argument('<file.zip>', 'a bundle from `quox evidence bundle`')
|
|
139
|
+
.option('--json', 'print the result as JSON')
|
|
140
|
+
.action(async (file, options) => {
|
|
141
|
+
let bytes;
|
|
142
|
+
try { bytes = fs.readFileSync(file); } catch (e) { throw new Error(`Cannot read ${file}: ${e.message}`); }
|
|
143
|
+
const v = await verifyEvidenceBundle(bytes);
|
|
144
|
+
if (options.json || program.opts().output === 'json') {
|
|
145
|
+
console.log(JSON.stringify({ file, verified: v.ok, checks: v.checks, counts: v.manifest?.counts || null, conversation: v.manifest?.corr || null, key_id: v.signature?.key_id || null }, null, 2));
|
|
146
|
+
} else {
|
|
147
|
+
for (const c of v.checks) console.log(` ${c.ok ? chalk.green('PASS') : chalk.red('FAIL')} ${c.name}${c.detail ? ` (${c.detail})` : ''}`);
|
|
148
|
+
const m = v.manifest;
|
|
149
|
+
console.log(v.ok
|
|
150
|
+
? chalk.green(`VERIFIED: ${m.counts.envelopes} envelopes, ${m.counts.ward_receipts} WARD receipts, conversation ${m.corr}, signed by ${v.signature.key_id}`)
|
|
151
|
+
: chalk.red('NOT VERIFIED: this bundle was altered or is not a Quox evidence bundle.'));
|
|
152
|
+
}
|
|
153
|
+
if (!v.ok) process.exitCode = 1;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox ext add|remove|list <pkg>
|
|
3
|
+
*
|
|
4
|
+
* The listing behind quoxon's extension host (cli/lib/quoxon/extensions.js, contract EXTENSIONS.md). It edits
|
|
5
|
+
* ~/.quox/extensions.json only; it does not install npm packages (npm i -g <pkg> does that). Nothing loads unless
|
|
6
|
+
* it is both installed and listed.
|
|
7
|
+
*/
|
|
8
|
+
import { readExtList, extAdd, extRemove, extListPath } from '../quoxon/extensions.js';
|
|
9
|
+
|
|
10
|
+
export function registerExtCommands(program) {
|
|
11
|
+
const ext = program.command('ext').description('Optional quoxon extensions: list, add or remove one (npm i -g <pkg> installs it)');
|
|
12
|
+
const out = (r) => { console.log(r.text); if (!r.ok) process.exitCode = 1; };
|
|
13
|
+
ext.command('list').description('Extensions listed in ~/.quox/extensions.json')
|
|
14
|
+
.option('--json', 'print as JSON')
|
|
15
|
+
.action((opts) => {
|
|
16
|
+
const list = readExtList();
|
|
17
|
+
if (opts.json || program.opts().output === 'json') { console.log(JSON.stringify({ file: extListPath(), extensions: list }, null, 2)); return; }
|
|
18
|
+
if (!list.length) { console.log('No extensions listed. quox ext add <pkg>'); return; }
|
|
19
|
+
for (const x of list) console.log(`${x.name} ${x.enabled ? 'on' : 'off'}`);
|
|
20
|
+
});
|
|
21
|
+
ext.command('add').description('List an extension so quoxon loads it once installed').argument('<pkg>').action((pkg) => out(extAdd(pkg)));
|
|
22
|
+
ext.command('remove').description('Stop listing an extension').argument('<pkg>').action((pkg) => out(extRemove(pkg)));
|
|
23
|
+
}
|
package/lib/commands/fleet.js
CHANGED
|
@@ -595,6 +595,31 @@ export function registerFleetCommands(program, ctx) {
|
|
|
595
595
|
}, { format: program.opts().output, emptyMessage: data && data.bastion === false ? 'No agents enrolled yet.' : 'No hosts in the Bastion registry.' });
|
|
596
596
|
});
|
|
597
597
|
|
|
598
|
+
fleet
|
|
599
|
+
.command('ping [host]')
|
|
600
|
+
.description('Refresh Bastion host state by pinging every host (or one); org admin, writes a quox.fleet.ping evidence envelope')
|
|
601
|
+
.action(async (host) => {
|
|
602
|
+
auth.requireAuth();
|
|
603
|
+
const ep = resolveEndpoint('v1.fleet.ping');
|
|
604
|
+
const data = await client.post(ep.path, { service: ep.service, body: host ? { host } : {}, timeout: 60000 });
|
|
605
|
+
const results = data.results || [];
|
|
606
|
+
const summary = data.summary || { requested: results.length, reachable: results.filter(r => r.reachable).length, unreachable: results.filter(r => !r.reachable).length };
|
|
607
|
+
if (program.opts().output === 'json') {
|
|
608
|
+
formatDetail({ success: data.success !== false, results, summary }, { format: 'json' });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
formatOutput({
|
|
612
|
+
columns: ['host_id', 'reachable', 'latency'],
|
|
613
|
+
headers: ['Host', 'Reachable', 'Latency'],
|
|
614
|
+
rows: results.map(r => ({
|
|
615
|
+
host_id: r.host_id,
|
|
616
|
+
reachable: r.reachable ? 'yes' : (r.error ? `no (${r.error})` : 'no'),
|
|
617
|
+
latency: typeof r.latency_ms === 'number' ? `${r.latency_ms}ms` : '-',
|
|
618
|
+
})),
|
|
619
|
+
}, { format: program.opts().output, emptyMessage: 'No hosts answered the ping.' });
|
|
620
|
+
printInfo(`${summary.reachable} of ${summary.requested} reachable, ${summary.unreachable} unreachable`);
|
|
621
|
+
});
|
|
622
|
+
|
|
598
623
|
fleet
|
|
599
624
|
.command('gpu')
|
|
600
625
|
.description('List the durable GPU fleet')
|
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/hat.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* quox hat list
|
|
5
5
|
* quox hat show <id|key> [--rev <n>]
|
|
6
6
|
* quox hat reset <id|key> --yes (audited reset-to-template, built-ins)
|
|
7
|
+
* quox hat upgrade <id|key> [--dry-run|--yes] (3-way merge of an org-edited built-in onto the current template)
|
|
7
8
|
* quox hat create <key> --name "<Name>" [--from <hat>] [--agents a,b] [--tools x,y] [--mode m] [--offer o]
|
|
8
9
|
* quox hat edit <hat> [--name N] [--add-agents|--remove-agents|--agents] [--add-tools|--remove-tools|--tools] [--mode m]
|
|
9
10
|
* quox hat delete <hat> --yes (custom hats only)
|
|
@@ -42,6 +43,12 @@ export function templateLabel(hat) {
|
|
|
42
43
|
const latestAuthor = Array.isArray(hat.revisions) && hat.revisions[0] ? hat.revisions[0].created_by : null;
|
|
43
44
|
const customised = typeof latestAuthor === 'string' && !latestAuthor.startsWith('template:');
|
|
44
45
|
if (latest && cur && cur < latest) return `v${cur} (template v${latest} available)`;
|
|
46
|
+
// Merge-upgrade: the last applied 3-way merge (org edits carried onto the new template).
|
|
47
|
+
const u = hat.last_upgrade;
|
|
48
|
+
if (u && u.to_template_version === cur) {
|
|
49
|
+
const k = Number(u.org_changes_kept) || 0;
|
|
50
|
+
return `v${cur}, upgraded from v${u.from_template_version}, ${k} org change${k === 1 ? '' : 's'} kept`;
|
|
51
|
+
}
|
|
45
52
|
return customised ? `v${cur || '?'} (edited)` : `v${cur || '?'}`;
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -89,6 +96,30 @@ export async function findUnknownAgents(client, orgId, ids) {
|
|
|
89
96
|
|
|
90
97
|
function isBuiltin(h) { return !!h?.template_key; }
|
|
91
98
|
|
|
99
|
+
const fmtItem = (e) => (e.item !== undefined ? `${e.path}: ${e.item}` : e.path);
|
|
100
|
+
|
|
101
|
+
/** Human preview of a merge upgrade (exported for reuse by other surfaces). */
|
|
102
|
+
export function renderUpgradePreview(key, p) {
|
|
103
|
+
const out = [];
|
|
104
|
+
out.push(chalk.bold(`Hat ${key}: template v${p.from_template_version} -> v${p.to_template_version}`) + chalk.dim(` (from rev ${p.from_revision})`));
|
|
105
|
+
out.push(`Tools added: ${(p.tools_added || []).join(', ') || 'none'}`);
|
|
106
|
+
out.push(`Tools removed: ${(p.tools_removed || []).join(', ') || 'none'}`);
|
|
107
|
+
const changed = (p.template_changed || []).map(c => c.path);
|
|
108
|
+
if (changed.length) out.push(`Template changed: ${changed.join(', ')}`);
|
|
109
|
+
out.push(chalk.bold(`Org changes kept (${(p.org_changes || []).length}):`));
|
|
110
|
+
for (const c of p.org_changes || []) out.push(` ${c.change.padEnd(7)} ${fmtItem(c)}`);
|
|
111
|
+
if (!(p.org_changes || []).length) out.push(' none');
|
|
112
|
+
out.push(chalk.bold(`Conflicts, org choice kept (${(p.conflicts || []).length}):`));
|
|
113
|
+
for (const c of p.conflicts || []) out.push(` ${c.path}${c.reason ? ` (${c.reason})` : ''}`);
|
|
114
|
+
if (!(p.conflicts || []).length) out.push(' none');
|
|
115
|
+
out.push(chalk.bold(`Held, would widen authorization (${(p.held || []).length}):`));
|
|
116
|
+
for (const c of p.held || []) out.push(` ${c.path} ${(c.fields || []).join(', ')}`);
|
|
117
|
+
if (!(p.held || []).length) out.push(' none');
|
|
118
|
+
out.push(`Valid: ${p.valid ? 'yes' : `NO: ${(p.errors || []).join('; ')}`}`);
|
|
119
|
+
out.push(`Digest: ${p.from_digest || '-'} -> ${p.digest}`);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
export function registerHatCommands(program, ctx) {
|
|
93
124
|
const { client, auth } = ctx;
|
|
94
125
|
|
|
@@ -234,6 +265,51 @@ export function registerHatCommands(program, ctx) {
|
|
|
234
265
|
if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h }, null, 2)); return; }
|
|
235
266
|
console.log(` Ⓠ QUOX │ hat ${h.key} reset to template │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
|
|
236
267
|
});
|
|
268
|
+
// ── hat upgrade ─────────────────────────────────────────────────────────────
|
|
269
|
+
hat
|
|
270
|
+
.command('upgrade <hat>')
|
|
271
|
+
.description('Carry an org-edited built-in hat onto the current template (3-way merge: org changes kept, conflicts keep the org choice, widening authz changes held); previews unless --yes')
|
|
272
|
+
.option('--dry-run', 'preview the merge only (writes nothing); the default without --yes')
|
|
273
|
+
.option('--yes', 'apply the previewed merge as a new system-authored revision (audited)')
|
|
274
|
+
.action(async (ref, options) => {
|
|
275
|
+
auth.requireAuth();
|
|
276
|
+
if (options.dryRun && options.yes) throw new Error('Pass either --dry-run (preview) or --yes (apply), not both.');
|
|
277
|
+
const orgId = resolveOrgId(auth, program);
|
|
278
|
+
const list = await fetchList(client, orgId);
|
|
279
|
+
const found = findHat(list?.hats, ref);
|
|
280
|
+
if (!found) {
|
|
281
|
+
const keys = (list?.hats || []).map(h => h.key).join(', ') || 'none';
|
|
282
|
+
throw new Error(`No hat '${ref}' in this org. Available: ${keys}`);
|
|
283
|
+
}
|
|
284
|
+
const pep = resolveEndpoint('hat.upgrade.preview', { orgId, hatId: found.id });
|
|
285
|
+
let data;
|
|
286
|
+
try {
|
|
287
|
+
data = await client.get(pep.path, { service: pep.service });
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const code = err?.response?.data?.error || err?.data?.error || err?.body?.error;
|
|
290
|
+
if (code === 'up_to_date') { console.log(` Ⓠ QUOX │ hat ${found.key} upgrade │ already on template v${found.template_version} ✔`); return; }
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
const p = data?.preview;
|
|
294
|
+
if (!p) throw new Error(`Upgrade preview for '${ref}' returned nothing`);
|
|
295
|
+
const json = program.opts().output === 'json';
|
|
296
|
+
|
|
297
|
+
if (!options.yes) {
|
|
298
|
+
if (json) { console.log(JSON.stringify({ preview: p, applied: false }, null, 2)); return; }
|
|
299
|
+
console.log(renderUpgradePreview(found.key, p).join('\n'));
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log(` dry run: nothing written. Apply with: quox hat upgrade ${found.key} --yes`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!p.valid) throw new Error(`The merged hat does not validate: ${(p.errors || []).join('; ')}`);
|
|
305
|
+
const aep = resolveEndpoint('hat.upgrade', { orgId, hatId: found.id });
|
|
306
|
+
const res = await client.post(aep.path, { service: aep.service, body: { expected_revision: p.from_revision } });
|
|
307
|
+
const h = res?.hat;
|
|
308
|
+
if (!h) throw new Error(`Upgrade of '${ref}' returned no hat`);
|
|
309
|
+
if (json) { console.log(JSON.stringify({ hat: h, preview: res.preview || p, applied: true }, null, 2)); return; }
|
|
310
|
+
console.log(` Ⓠ QUOX │ hat ${h.key} upgraded │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
|
|
311
|
+
});
|
|
312
|
+
|
|
237
313
|
// ── hat create (custom) ───────────────────────────────────────────────────────
|
|
238
314
|
hat
|
|
239
315
|
.command('create <key>')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox image generate "<prompt>" [--out file] [--provider gemini|openai] [--aspect-ratio 16:9] [--count n]
|
|
3
|
+
*
|
|
4
|
+
* CLI verb first for image generation. It runs the SAME governed tool the chat uses (gemini_image_generate via
|
|
5
|
+
* POST /api/v1/tools/execute), so the provider fallback, the receipt/witnessing and the plain all-fail message are
|
|
6
|
+
* the collector's, not re-implemented here. Providers tried: Gemini, then OpenAI, using only what the org already
|
|
7
|
+
* connected (`quox provider list`). Nothing here adds a provider or a key.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { printSuccess } from '../core/output.js';
|
|
12
|
+
import { executeGovernedTool } from './tool.js';
|
|
13
|
+
|
|
14
|
+
const EXT = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' };
|
|
15
|
+
|
|
16
|
+
export function registerImageCommands(program, ctx) {
|
|
17
|
+
const { client, auth } = ctx;
|
|
18
|
+
const image = program.command('image').description('Generate images with the providers your org has connected');
|
|
19
|
+
|
|
20
|
+
image
|
|
21
|
+
.command('generate')
|
|
22
|
+
.description('Generate an image from a prompt (tries Gemini, then OpenAI; says which made it)')
|
|
23
|
+
.argument('<prompt>', 'what to draw')
|
|
24
|
+
.option('--out <file>', 'where to save the image (default ./quox-image-<time>.png)')
|
|
25
|
+
.option('--provider <name>', 'use only this provider: gemini or openai (no fall-through)')
|
|
26
|
+
.option('--aspect-ratio <ratio>', 'e.g. 16:9 for a hero, 1:1 for social, 9:16 for stories')
|
|
27
|
+
.option('--count <n>', 'how many images (1-4)', '1')
|
|
28
|
+
.action(async (prompt, options) => {
|
|
29
|
+
const input = { prompt };
|
|
30
|
+
if (options.provider) input.provider = options.provider;
|
|
31
|
+
if (options.aspectRatio) input.aspect_ratio = options.aspectRatio;
|
|
32
|
+
const n = Number(options.count);
|
|
33
|
+
if (n > 1) input.number_of_images = Math.min(n, 4);
|
|
34
|
+
|
|
35
|
+
const data = await executeGovernedTool({ client, auth, program }, 'gemini_image_generate', input, { reason: 'quox image generate' });
|
|
36
|
+
const r = (data && data.result && typeof data.result === 'object') ? data.result : (data || {});
|
|
37
|
+
const failed = (data && data.success === false) || r.success === false;
|
|
38
|
+
|
|
39
|
+
if (program.opts().output === 'json') {
|
|
40
|
+
// Never dump base64 into a terminal or a pipe: report where the files went.
|
|
41
|
+
const out = failed ? r : { ...r, images: undefined };
|
|
42
|
+
if (!failed) out.files = saveImages(r.images, options.out);
|
|
43
|
+
console.log(JSON.stringify({ ...out, ...(failed && !r.error && data.error ? { error: data.error } : {}) }, null, 2));
|
|
44
|
+
if (failed) process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (failed) {
|
|
48
|
+
console.error(r.error || data.error || 'Image generation failed.');
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const files = saveImages(r.images, options.out);
|
|
53
|
+
printSuccess(`Made by ${r.provider_used || 'gemini'}${r.model_used ? ` (${r.model_used})` : ''}: ${files.join(', ')}`);
|
|
54
|
+
if (r.note) console.log(r.note);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveImages(images, out) {
|
|
59
|
+
const list = Array.isArray(images) ? images : [];
|
|
60
|
+
const stamp = Date.now();
|
|
61
|
+
return list.map((img, i) => {
|
|
62
|
+
const ext = EXT[img.mime_type] || 'png';
|
|
63
|
+
let file = out || `quox-image-${stamp}.${ext}`;
|
|
64
|
+
if (list.length > 1) { const p = path.parse(file); file = path.join(p.dir, `${p.name}-${i + 1}${p.ext || `.${ext}`}`); }
|
|
65
|
+
fs.writeFileSync(file, Buffer.from(img.base64, 'base64'));
|
|
66
|
+
return file;
|
|
67
|
+
});
|
|
68
|
+
}
|
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')
|