quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #970: governed output shaping for `quox tool exec` (--select / --summary / --limit).
|
|
3
|
+
*
|
|
4
|
+
* On the chat CLI-tools path every shell call except `quox tool exec` is denied, so a model
|
|
5
|
+
* facing a large result (wazuh agents, seo urls, a 618 KB calendar overlay) has no honest
|
|
6
|
+
* way to narrow it. This is that way, inside the governed call: the path grammar is the
|
|
7
|
+
* safe evaluator QuoxOn rows already use (parsePath: a.b, a[0], list[field=value], list[field~text], list[*], {a,b},
|
|
8
|
+
* count(path)), no eval and no new dependency. The shape is sent to the collector so the
|
|
9
|
+
* receipt records what view of the result the caller was shown.
|
|
10
|
+
*
|
|
11
|
+
* Honesty rules: a truncated list always says "showing N of M"; a path that matches
|
|
12
|
+
* nothing says so rather than returning null; a malformed path is an error BEFORE the
|
|
13
|
+
* tool runs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parsePath } from './quoxon/rows.js';
|
|
17
|
+
|
|
18
|
+
const BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);
|
|
19
|
+
const GRAMMAR = 'use a.b, a[0], list[field=value] (exact), list[field~text] (contains, any case), list[*].field (that field of every item), list[*].{a,b} (several fields of every item, one call), list[field=value].name, count(list), or several paths at once: total,count(list)';
|
|
20
|
+
export const DEFAULT_SAMPLE = 10;
|
|
21
|
+
const COUNT_KEY_RE = /^(status|state|type|kind|level|severity|health|healthy|enabled|active|category|source|platform|os|role|priority|result|outcome|connected|online)$/i;
|
|
22
|
+
|
|
23
|
+
// Split on commas outside [], {} and (): "total, count(a[x=1]), b.{c,d}" -> three paths. null when there is no
|
|
24
|
+
// top-level comma (a single path), [] entries kept so an empty part is an error, never silently dropped.
|
|
25
|
+
function splitTopLevel(e) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
let depth = 0, start = 0;
|
|
28
|
+
for (let i = 0; i < e.length; i++) {
|
|
29
|
+
const c = e[i];
|
|
30
|
+
if (c === '[' || c === '{' || c === '(') depth++;
|
|
31
|
+
else if (c === ']' || c === '}' || c === ')') depth = Math.max(0, depth - 1);
|
|
32
|
+
else if (c === ',' && depth === 0) { parts.push(e.slice(start, i).trim()); start = i + 1; }
|
|
33
|
+
}
|
|
34
|
+
if (!parts.length) return null;
|
|
35
|
+
parts.push(e.slice(start).trim());
|
|
36
|
+
return parts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseSelect(expr) {
|
|
40
|
+
const e = String(expr ?? '').trim();
|
|
41
|
+
// GUIDES-FIX: several paths in one call ("total,active_total"): each must be a valid path on its own.
|
|
42
|
+
const parts = splitTopLevel(e);
|
|
43
|
+
if (parts) {
|
|
44
|
+
if (parts.some((x) => !x)) return { error: `--select: empty path in ${JSON.stringify(e)}; separate several paths with commas, e.g. total,count(list); ${GRAMMAR}` };
|
|
45
|
+
const multi = [];
|
|
46
|
+
for (const x of parts) {
|
|
47
|
+
const p = parseSelect(x);
|
|
48
|
+
if (p.error) return p;
|
|
49
|
+
multi.push({ expr: x, ...p });
|
|
50
|
+
}
|
|
51
|
+
return { multi };
|
|
52
|
+
}
|
|
53
|
+
const fn = /^([A-Za-z_]+)\s*\(([\s\S]*)\)$/.exec(e);
|
|
54
|
+
if (fn) {
|
|
55
|
+
if (fn[1] !== 'count') return { error: `--select: unknown function ${fn[1]}() (only count); ${GRAMMAR}` };
|
|
56
|
+
const steps = parsePath(fn[2]);
|
|
57
|
+
return steps ? { fn: 'count', steps } : { error: `--select: count() needs a path; ${GRAMMAR}` };
|
|
58
|
+
}
|
|
59
|
+
const steps = parsePath(e);
|
|
60
|
+
return steps ? { steps } : { error: `--select: bad path ${JSON.stringify(e)}; ${GRAMMAR}` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseLimit(v) {
|
|
64
|
+
if (v === undefined || v === null || v === true) return { value: undefined };
|
|
65
|
+
const n = Number(v);
|
|
66
|
+
if (!Number.isInteger(n) || n < 1 || n > 10000) return { error: `--limit: expected a whole number from 1 to 10000, got ${JSON.stringify(String(v))}` };
|
|
67
|
+
return { value: n };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Validate a shape before the tool runs -> error string or null. */
|
|
71
|
+
export function checkShape({ select, limit } = {}) {
|
|
72
|
+
if (select !== undefined) { const p = parseSelect(select); if (p.error) return p.error; }
|
|
73
|
+
const l = parseLimit(limit);
|
|
74
|
+
return l.error || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The content-free record of the shape, as sent to the collector for the receipt. */
|
|
78
|
+
export function shapeRecord({ select, summary, limit } = {}) {
|
|
79
|
+
const r = {};
|
|
80
|
+
if (select !== undefined) r.select = String(select);
|
|
81
|
+
if (summary) r.summary = true;
|
|
82
|
+
const l = parseLimit(limit);
|
|
83
|
+
if (l.value !== undefined) r.limit = l.value;
|
|
84
|
+
return Object.keys(r).length ? r : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isObj = (o) => o !== null && typeof o === 'object' && !Array.isArray(o);
|
|
88
|
+
const own = (o, k) => isObj(o) && !BLOCKED.has(k) && Object.hasOwn(o, k);
|
|
89
|
+
|
|
90
|
+
/** Like the rows evaluator, plus projection: a key step on a list maps over its items. */
|
|
91
|
+
function walk(data, steps) {
|
|
92
|
+
let cur = data;
|
|
93
|
+
for (const st of steps) {
|
|
94
|
+
if (cur === undefined || cur === null) return undefined;
|
|
95
|
+
if ('key' in st) {
|
|
96
|
+
if (Array.isArray(cur)) cur = cur.filter((it) => own(it, st.key)).map((it) => it[st.key]);
|
|
97
|
+
else { if (!own(cur, st.key)) return undefined; cur = cur[st.key]; }
|
|
98
|
+
} else if ('index' in st) {
|
|
99
|
+
if (!Array.isArray(cur)) return undefined; cur = cur[st.index];
|
|
100
|
+
} else if (st.wildcard) {
|
|
101
|
+
// list[*]: every item; the key steps after it already map over a list.
|
|
102
|
+
if (!Array.isArray(cur)) return undefined;
|
|
103
|
+
} else if (st.fields) {
|
|
104
|
+
// #1044: {a,b}: only these fields, of every item on a list. Blocked keys are never copied.
|
|
105
|
+
const pick = (o) => { const r = {}; for (const f of st.fields) if (own(o, f)) r[f] = o[f]; return r; };
|
|
106
|
+
cur = Array.isArray(cur) ? cur.map((it) => (isObj(it) ? pick(it) : {})) : isObj(cur) ? pick(cur) : undefined;
|
|
107
|
+
} else {
|
|
108
|
+
if (!Array.isArray(cur)) return undefined;
|
|
109
|
+
const sub = parsePath(st.field);
|
|
110
|
+
cur = cur.filter((it) => {
|
|
111
|
+
const v = sub ? walk(it, sub) : undefined;
|
|
112
|
+
if (v === undefined || v === null || typeof v === 'object') return false;
|
|
113
|
+
return 'contains' in st ? String(v).toLowerCase().includes(st.contains) : String(v) === st.value;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return cur;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const countOf = (v) => (Array.isArray(v) ? v.length : isObj(v) ? Object.keys(v).length : v === undefined ? 0 : 1);
|
|
121
|
+
|
|
122
|
+
/** The main list of a value: itself, else its largest array (top level, then one level down). */
|
|
123
|
+
function mainList(value) {
|
|
124
|
+
if (Array.isArray(value)) return { path: '', list: value };
|
|
125
|
+
if (!isObj(value)) return null;
|
|
126
|
+
let best = null;
|
|
127
|
+
const consider = (path, v) => { if (Array.isArray(v) && (!best || v.length > best.list.length)) best = { path, list: v }; };
|
|
128
|
+
for (const [k, v] of Object.entries(value)) consider(k, v);
|
|
129
|
+
if (!best) for (const [k, v] of Object.entries(value)) if (isObj(v)) for (const [k2, v2] of Object.entries(v)) consider(`${k}.${k2}`, v2);
|
|
130
|
+
return best;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fieldCounts(list) {
|
|
134
|
+
const counts = {};
|
|
135
|
+
const keys = new Set();
|
|
136
|
+
for (const it of list) if (isObj(it)) for (const k of Object.keys(it)) if (COUNT_KEY_RE.test(k)) keys.add(k);
|
|
137
|
+
for (const k of keys) {
|
|
138
|
+
const c = {};
|
|
139
|
+
for (const it of list) {
|
|
140
|
+
const v = isObj(it) ? it[k] : undefined;
|
|
141
|
+
if (v === undefined || v === null || typeof v === 'object') continue;
|
|
142
|
+
const key = String(v);
|
|
143
|
+
c[key] = (c[key] || 0) + 1;
|
|
144
|
+
}
|
|
145
|
+
if (Object.keys(c).length && Object.keys(c).length <= 20) counts[k] = c;
|
|
146
|
+
}
|
|
147
|
+
return counts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Honest summary: total, per-field counts where obvious, a bounded sample, "showing N of M". */
|
|
151
|
+
export function summarize(value, { limit = DEFAULT_SAMPLE } = {}) {
|
|
152
|
+
const found = mainList(value);
|
|
153
|
+
if (!found) {
|
|
154
|
+
return isObj(value)
|
|
155
|
+
? { note: 'no list in this result; showing its keys', keys: Object.keys(value) }
|
|
156
|
+
: { note: 'no list in this result', value };
|
|
157
|
+
}
|
|
158
|
+
const M = found.list.length;
|
|
159
|
+
const N = Math.min(limit, M);
|
|
160
|
+
const out = { list_path: found.path || '(root)', total: M, showing: N, note: `showing ${N} of ${M}` };
|
|
161
|
+
const counts = fieldCounts(found.list);
|
|
162
|
+
if (Object.keys(counts).length) out.counts = counts;
|
|
163
|
+
if (isObj(value)) {
|
|
164
|
+
const scalars = Object.fromEntries(Object.entries(value).filter(([, v]) => v === null || typeof v !== 'object'));
|
|
165
|
+
if (Object.keys(scalars).length) out.fields = scalars;
|
|
166
|
+
}
|
|
167
|
+
out.sample = found.list.slice(0, N);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function truncate(value, n) {
|
|
172
|
+
if (Array.isArray(value)) {
|
|
173
|
+
if (value.length <= n) return value;
|
|
174
|
+
return { total: value.length, showing: n, note: `showing ${n} of ${value.length}`, items: value.slice(0, n) };
|
|
175
|
+
}
|
|
176
|
+
const found = mainList(value);
|
|
177
|
+
if (!found || !found.path || found.list.length <= n) return value;
|
|
178
|
+
const copy = structuredClone(value);
|
|
179
|
+
const parts = found.path.split('.');
|
|
180
|
+
let host = copy;
|
|
181
|
+
for (const p of parts.slice(0, -1)) host = host[p];
|
|
182
|
+
host[parts[parts.length - 1]] = found.list.slice(0, n);
|
|
183
|
+
copy._truncated = { path: found.path, total: found.list.length, showing: n, note: `showing ${n} of ${found.list.length}` };
|
|
184
|
+
return copy;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function asData(raw) {
|
|
188
|
+
if (typeof raw !== 'string') return raw;
|
|
189
|
+
const t = raw.trim();
|
|
190
|
+
if (t.startsWith('{') || t.startsWith('[')) { try { return JSON.parse(t); } catch { /* text */ } }
|
|
191
|
+
return raw;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Apply select, then summary or limit. -> { value } (throws on a malformed shape). */
|
|
195
|
+
export function shapeOutput(raw, shape = {}) {
|
|
196
|
+
const err = checkShape(shape);
|
|
197
|
+
if (err) throw new Error(err);
|
|
198
|
+
let value = asData(raw);
|
|
199
|
+
if (shape.select !== undefined) {
|
|
200
|
+
const p = parseSelect(shape.select);
|
|
201
|
+
if (p.multi) {
|
|
202
|
+
// each path under its own expression; a path that matched nothing is named, never dropped
|
|
203
|
+
const out = {};
|
|
204
|
+
const unmatched = [];
|
|
205
|
+
for (const q of p.multi) {
|
|
206
|
+
const v = walk(value, q.steps);
|
|
207
|
+
if (q.fn === 'count') out[q.expr] = countOf(v);
|
|
208
|
+
else if (v === undefined) unmatched.push(q.expr);
|
|
209
|
+
else out[q.expr] = v;
|
|
210
|
+
}
|
|
211
|
+
if (!Object.keys(out).length) {
|
|
212
|
+
return { value: { select: String(shape.select), matched: false, note: 'none of the --select paths matched anything in the result', top_keys: isObj(value) ? Object.keys(value) : [] } };
|
|
213
|
+
}
|
|
214
|
+
if (unmatched.length) { out._unmatched = unmatched; out._note = `${unmatched.length} of ${p.multi.length} --select paths matched nothing in the result`; }
|
|
215
|
+
value = out;
|
|
216
|
+
} else {
|
|
217
|
+
const v = walk(value, p.steps);
|
|
218
|
+
if (p.fn === 'count') value = countOf(v);
|
|
219
|
+
else if (v === undefined) {
|
|
220
|
+
return { value: { select: String(shape.select), matched: false, note: 'the --select path matched nothing in the result', top_keys: isObj(value) ? Object.keys(value) : [] } };
|
|
221
|
+
} else value = v;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const { value: limit } = parseLimit(shape.limit);
|
|
225
|
+
if (shape.summary) return { value: summarize(value, { limit: limit ?? DEFAULT_SAMPLE }) };
|
|
226
|
+
if (limit !== undefined) return { value: truncate(value, limit) };
|
|
227
|
+
return { value };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* BASH-FALLBACK: keep an unshaped result inline on the model-driven path.
|
|
232
|
+
*
|
|
233
|
+
* Claude Code saves Bash output above its inline limit (30000 chars by default) to a file
|
|
234
|
+
* and shows only a path plus a 2 KB preview. The model then reaches for jq/python/grep on
|
|
235
|
+
* that file, which the CLI-tools Bash guard denies (55 of 64 Bash attempts in the
|
|
236
|
+
* 2026-10-14/15 proof evidence). Capping here, below that limit, means the model never
|
|
237
|
+
* gets a file to post-process: it gets the honest --summary view plus how to re-run with
|
|
238
|
+
* --select. The cap only ever shrinks what is SHOWN; the call itself already ran governed.
|
|
239
|
+
*/
|
|
240
|
+
export const INLINE_MAX_CHARS = 20000;
|
|
241
|
+
|
|
242
|
+
const jsonLen = (v) => (typeof v === 'string' ? v.length : JSON.stringify(v, null, 2).length);
|
|
243
|
+
|
|
244
|
+
/** A {output: "<json>"} envelope (quox_cli and some tools) -> its parsed body, else null. */
|
|
245
|
+
function innerOutput(value) {
|
|
246
|
+
if (!isObj(value) || typeof value.output !== 'string') return null;
|
|
247
|
+
const parsed = asData(value.output);
|
|
248
|
+
return typeof parsed === 'string' ? null : parsed;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* FLEET-UNHEALTHY: the small non-scalar top-level fields beside the main list (a short "not_healthy" list, a
|
|
253
|
+
* "by_state" count object) are often the answer itself; the summary used to drop every non-scalar. Each kept
|
|
254
|
+
* whole or left out whole, never cut: at most SMALL_FIELD_MAX chars each and SMALL_FIELDS_MAX in total.
|
|
255
|
+
*/
|
|
256
|
+
const SMALL_FIELD_MAX = 6000;
|
|
257
|
+
const SMALL_FIELDS_MAX = 9000;
|
|
258
|
+
function smallTopFields(body, found) {
|
|
259
|
+
if (!isObj(body)) return null;
|
|
260
|
+
const mainKey = found && found.path ? found.path.split('.')[0] : null;
|
|
261
|
+
const out = {};
|
|
262
|
+
let used = 0;
|
|
263
|
+
for (const [k, v] of Object.entries(body)) {
|
|
264
|
+
if (k === mainKey || BLOCKED.has(k) || v === null || typeof v !== 'object') continue;
|
|
265
|
+
const n = jsonLen(v);
|
|
266
|
+
if (n > SMALL_FIELD_MAX || used + n > SMALL_FIELDS_MAX) continue;
|
|
267
|
+
out[k] = v;
|
|
268
|
+
used += n;
|
|
269
|
+
}
|
|
270
|
+
return Object.keys(out).length ? out : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function capInline(value, { maxChars = INLINE_MAX_CHARS } = {}) {
|
|
274
|
+
const size = jsonLen(value);
|
|
275
|
+
if (size <= maxChars) return { value, capped: false };
|
|
276
|
+
const fullKb = Math.round(size / 1024);
|
|
277
|
+
const data = asData(value);
|
|
278
|
+
if (typeof data === 'string') {
|
|
279
|
+
const wrap = (keep) => ({
|
|
280
|
+
oversized: true,
|
|
281
|
+
full_size_kb: fullKb,
|
|
282
|
+
note: `showing the first ${keep} of ${data.length} characters`,
|
|
283
|
+
next: "Too large to show whole. Run the same call again with narrower --args, or --select '<path>' / --limit N if it is JSON. Never read or post-process a saved output with a shell.",
|
|
284
|
+
text: data.slice(0, keep),
|
|
285
|
+
});
|
|
286
|
+
// JSON escaping (newlines, quotes) inflates the text: shrink until the wrapper fits.
|
|
287
|
+
let keep = Math.max(0, maxChars - 1200);
|
|
288
|
+
let out = wrap(keep);
|
|
289
|
+
while (keep > 0 && jsonLen(out) > maxChars) { keep = Math.floor(keep * 0.8); out = wrap(keep); }
|
|
290
|
+
return { capped: true, value: out };
|
|
291
|
+
}
|
|
292
|
+
const inner = innerOutput(data);
|
|
293
|
+
const body = inner ?? data;
|
|
294
|
+
const via = inner ? 'output (a JSON string: --select cannot reach inside it, so narrow it with --args or the tool\'s own filters)' : null;
|
|
295
|
+
const found = mainList(body);
|
|
296
|
+
const lp = found && found.path ? found.path : (found ? '' : null);
|
|
297
|
+
const example = lp === null
|
|
298
|
+
? "--select '<key>'"
|
|
299
|
+
: `--select '${lp ? `${lp}[field=value].name` : '[0]'}', --select 'count(${lp || ''})' or --select '${lp ? `${lp}[*].field` : '[*].field'}'`;
|
|
300
|
+
const next = `Result is ${fullKb} KB, too large to show whole, so this is the --summary view. For the exact part you need, run the same quox tool exec again with ${example} (or --summary --limit N for a bigger sample)${via ? `; the list is inside ${via}` : ''}. Never read or post-process a saved output with a shell.`;
|
|
301
|
+
const head = { oversized: true, full_size_kb: fullKb, next, top_keys: isObj(body) ? Object.keys(body).slice(0, 40) : undefined };
|
|
302
|
+
const extra = smallTopFields(body, found);
|
|
303
|
+
for (const limit of [DEFAULT_SAMPLE, 5, 3, 1, 0]) {
|
|
304
|
+
const s = summarize(body, { limit });
|
|
305
|
+
const out = { ...head, ...s, ...(extra ? { fields: { ...(s.fields || {}), ...extra } } : {}) };
|
|
306
|
+
if (jsonLen(out) <= maxChars) return { capped: true, value: out };
|
|
307
|
+
}
|
|
308
|
+
for (const limit of [DEFAULT_SAMPLE, 5, 3, 1, 0]) {
|
|
309
|
+
const out = { ...head, ...summarize(body, { limit }) };
|
|
310
|
+
if (jsonLen(out) <= maxChars) return { capped: true, value: out };
|
|
311
|
+
}
|
|
312
|
+
// Even an empty sample is too big (a huge scalar field): keep only the counts and shape.
|
|
313
|
+
const s = summarize(body, { limit: 0 });
|
|
314
|
+
return { capped: true, value: { ...head, list_path: s.list_path, total: s.total, showing: 0, note: s.note || 'fields too large to show', counts: s.counts } };
|
|
315
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HyperionDeck: the full-screen live HYPERION activity view (`quox hyperion deck`), in the Quoxon look.
|
|
3
|
+
* Refreshes every intervalMs; r refreshes now, q quits. A failed fetch never blanks the screen: the last
|
|
4
|
+
* good data stays, marked STALE, with the error named in the header. Props are injected (api, intervalMs)
|
|
5
|
+
* so the component is testable without a network. api = { activity() }.
|
|
6
|
+
*
|
|
7
|
+
* Refresh props live in refs so the fetch loop is created once (the LUCID deck ran out of memory when a
|
|
8
|
+
* changing prop restarted its loop on every redraw).
|
|
9
|
+
*/
|
|
10
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
11
|
+
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
12
|
+
import { statusLines, summaryText } from '../../core/hyperion.js';
|
|
13
|
+
import { c, gradient } from '../../quoxon/theme.js';
|
|
14
|
+
|
|
15
|
+
function roleFor(line) {
|
|
16
|
+
if (/^(Node load|Pool|Recent ops)/.test(line)) return 'sky';
|
|
17
|
+
if (/^VMID/.test(line)) return 'label';
|
|
18
|
+
if (/unavailable|^⚠/.test(line)) return 'warn';
|
|
19
|
+
return 'value';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function HyperionDeck({ api, intervalMs = 10000 }) {
|
|
23
|
+
const { exit } = useApp();
|
|
24
|
+
const { stdout } = useStdout();
|
|
25
|
+
const rows = (stdout && stdout.rows) || 30;
|
|
26
|
+
const [data, setData] = useState(null);
|
|
27
|
+
const [err, setErr] = useState(null);
|
|
28
|
+
const [at, setAt] = useState(null);
|
|
29
|
+
const inflight = useRef(false);
|
|
30
|
+
const apiRef = useRef(api);
|
|
31
|
+
apiRef.current = api;
|
|
32
|
+
|
|
33
|
+
const refresh = useCallback(async () => {
|
|
34
|
+
if (inflight.current) return;
|
|
35
|
+
inflight.current = true;
|
|
36
|
+
try {
|
|
37
|
+
setData(await apiRef.current.activity());
|
|
38
|
+
setErr(null);
|
|
39
|
+
setAt(Date.now());
|
|
40
|
+
} catch (e) {
|
|
41
|
+
setErr(e);
|
|
42
|
+
} finally {
|
|
43
|
+
inflight.current = false;
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
refresh();
|
|
49
|
+
const t = setInterval(refresh, intervalMs);
|
|
50
|
+
return () => clearInterval(t);
|
|
51
|
+
}, [refresh, intervalMs]);
|
|
52
|
+
|
|
53
|
+
useInput((input) => {
|
|
54
|
+
if (input === 'q') exit();
|
|
55
|
+
else if (input === 'r') refresh();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const lines = data ? statusLines(data, { now: Date.now() }) : [];
|
|
59
|
+
const shown = lines.slice(0, Math.max(5, rows - 4));
|
|
60
|
+
const stale = err && data;
|
|
61
|
+
return (
|
|
62
|
+
<Box flexDirection="column">
|
|
63
|
+
<Text wrap="truncate">
|
|
64
|
+
{gradient(' HYPERION ').map((g, i) => <Text key={i} bold color={g.color}>{g.ch}</Text>)}
|
|
65
|
+
<Text color={c('value')}> {data ? summaryText(data) : 'loading'}</Text>
|
|
66
|
+
{stale ? <Text color={c('warn')}> STALE: {String(err.message || err).slice(0, 60)}</Text> : null}
|
|
67
|
+
{err && !data ? <Text color={c('error')}> unreachable: {String(err.message || err).slice(0, 60)}</Text> : null}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text> </Text>
|
|
70
|
+
{shown.map((l, i) => <Text key={i} wrap="truncate" color={c(roleFor(l))}>{l}</Text>)}
|
|
71
|
+
<Text> </Text>
|
|
72
|
+
<Text wrap="truncate">
|
|
73
|
+
<Text bold color={c('key')}>r</Text><Text color={c('value')}> refresh </Text>
|
|
74
|
+
<Text bold color={c('key')}>q</Text><Text color={c('value')}> quit </Text>
|
|
75
|
+
<Text color={c('value')}>{at ? `updated ${new Date(at).toISOString().slice(11, 19)} UTC, every ${Math.round(intervalMs / 1000)}s` : ''}</Text>
|
|
76
|
+
</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
}
|