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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON `/vm <verb> <target> [args]`: parse the words, call the shared engine
|
|
3
|
+
* (lib/core/vm.js, same as `quox vm`), return { lines, tone } for say().
|
|
4
|
+
* A held call becomes the approval hint (tone 'warn'); other failures throw.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
VM_ACTIONS, NODE_ACTIONS, VmPendingError, pendingHint, makeExec,
|
|
8
|
+
listVms, vmInfo, vmDoctor, doctorLines, vmAction, vmSnapshot, poolStatus, poolLease, poolDone, poolConfig,
|
|
9
|
+
nodePower, readPubkey, vmListLines, textOf,
|
|
10
|
+
} from './vm.js';
|
|
11
|
+
|
|
12
|
+
/** Split argv into positionals and --flag value pairs. */
|
|
13
|
+
export function splitFlags(argv) {
|
|
14
|
+
const pos = []; const flags = {};
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const a = argv[i];
|
|
17
|
+
if (a.startsWith('--')) {
|
|
18
|
+
const eq = a.indexOf('=');
|
|
19
|
+
if (eq > 0) flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
20
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--')) flags[a.slice(2)] = argv[++i];
|
|
21
|
+
else flags[a.slice(2)] = true;
|
|
22
|
+
} else pos.push(a);
|
|
23
|
+
}
|
|
24
|
+
return { pos, flags };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const asLines = (t) => String(t).split('\n');
|
|
28
|
+
export const VM_USAGE = 'Usage: /vm <list|info|doctor|start|stop|reboot|shutdown|snapshot|pool|node> <target> [args]';
|
|
29
|
+
|
|
30
|
+
export async function runVmCommand({ client, orgId, argv, exec: injected }) {
|
|
31
|
+
const exec = injected || makeExec({ client, orgId });
|
|
32
|
+
const { pos, flags } = splitFlags(argv || []);
|
|
33
|
+
const [verb, ...rest] = pos;
|
|
34
|
+
const reason = typeof flags.reason === 'string' ? flags.reason : undefined;
|
|
35
|
+
const need = (v, what) => { if (!v) throw new Error(`${what} is required. ${VM_USAGE}`); return v; };
|
|
36
|
+
try {
|
|
37
|
+
if (!verb) return { lines: [VM_USAGE], tone: 'warn' };
|
|
38
|
+
if (verb === 'list') {
|
|
39
|
+
return { lines: vmListLines(await listVms(exec, { node: flags.node, type: flags.type })) };
|
|
40
|
+
}
|
|
41
|
+
if (verb === 'info') {
|
|
42
|
+
const { vm, resources } = await vmInfo(exec, need(rest[0], 'a VM name or vmid'));
|
|
43
|
+
return { lines: [`${vm.name || vm.vmid} vmid ${vm.vmid} node ${vm.node} ${vm.type}`, ...asLines(textOf(resources))] };
|
|
44
|
+
}
|
|
45
|
+
if (verb === 'doctor') {
|
|
46
|
+
const report = await vmDoctor(exec, need(rest[0], 'a VM name or vmid'));
|
|
47
|
+
return { lines: doctorLines(report), tone: report.verdict === 'healthy' ? undefined : 'warn' };
|
|
48
|
+
}
|
|
49
|
+
if (VM_ACTIONS.includes(verb)) {
|
|
50
|
+
const { vm, result } = await vmAction(exec, verb, need(rest[0], 'a VM name or vmid'), { reason });
|
|
51
|
+
return { lines: [`${verb} ${vm.name || vm.vmid} (vmid ${vm.vmid} on ${vm.node})`, ...asLines(textOf(result))] };
|
|
52
|
+
}
|
|
53
|
+
if (verb === 'snapshot') {
|
|
54
|
+
const { vm, result } = await vmSnapshot(exec, need(rest[0], 'a VM name or vmid'), { name: typeof flags.name === 'string' ? flags.name : undefined, reason });
|
|
55
|
+
return { lines: [`snapshot ${vm.name || vm.vmid} (vmid ${vm.vmid})`, ...asLines(textOf(result))] };
|
|
56
|
+
}
|
|
57
|
+
if (verb === 'pool') {
|
|
58
|
+
const sub = rest[0];
|
|
59
|
+
if (sub === 'status') return { lines: asLines(textOf(await poolStatus(exec))) };
|
|
60
|
+
if (sub === 'lease') {
|
|
61
|
+
const pubkey = readPubkey(typeof flags['pubkey-file'] === 'string' ? flags['pubkey-file'] : undefined);
|
|
62
|
+
const p = await poolLease(exec, {
|
|
63
|
+
lessee: flags.lessee, pubkey, cores: flags.cores, memoryGb: flags['memory-gb'],
|
|
64
|
+
diskGb: flags['disk-gb'], purpose: flags.purpose, reason,
|
|
65
|
+
});
|
|
66
|
+
return { lines: asLines(textOf(p)) };
|
|
67
|
+
}
|
|
68
|
+
if (sub === 'done') return { lines: asLines(textOf(await poolDone(exec, need(rest[1], 'a vmid'), { reason }))) };
|
|
69
|
+
if (sub === 'config') {
|
|
70
|
+
let input = {};
|
|
71
|
+
if (flags.set) { try { input = JSON.parse(flags.set); } catch { throw new Error('--set must be a JSON object'); } }
|
|
72
|
+
return { lines: asLines(textOf(await poolConfig(exec, input, { reason }))) };
|
|
73
|
+
}
|
|
74
|
+
return { lines: ['Usage: /vm pool <status|lease|done <vmid>|config>'], tone: 'warn' };
|
|
75
|
+
}
|
|
76
|
+
if (verb === 'node') {
|
|
77
|
+
const [action, name] = rest;
|
|
78
|
+
if (!NODE_ACTIONS.includes(action)) return { lines: ['Usage: /vm node <reboot|shutdown> <node>'], tone: 'warn' };
|
|
79
|
+
return { lines: asLines(textOf(await nodePower(exec, action, need(name, 'a node name'), { reason }))) };
|
|
80
|
+
}
|
|
81
|
+
return { lines: [`Unknown /vm verb "${verb}". ${VM_USAGE}`], tone: 'warn' };
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e instanceof VmPendingError) return { lines: [pendingHint(e)], tone: 'warn' };
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WALKTHROUGH for a plain terminal (`quox approvals walkthrough`, `quox inbox walkthrough`): the waiting decisions one
|
|
3
|
+
* card at a time, using the same card and reply rules as the TUI (quoxon/approvalsView.js). Identity is the decision id.
|
|
4
|
+
* io = { readLine(prompt) -> Promise<string|null>, write(line), answer(item, key, note) -> Promise<void> }.
|
|
5
|
+
*/
|
|
6
|
+
import { isDecision, walkCard, walkReply, walkNotYesNo, walkNoCatch, walkSummary } from '../quoxon/approvalsView.js';
|
|
7
|
+
|
|
8
|
+
export async function runWalkthrough(items, io) {
|
|
9
|
+
const decs = (items || []).filter(isDecision);
|
|
10
|
+
if (!decs.length) { io.write('No question is waiting for you.'); return { answered: [], skipped: [], left: [] }; }
|
|
11
|
+
const st = { answered: [], skipped: [], left: [] };
|
|
12
|
+
io.write(`${decs.length} question${decs.length === 1 ? '' : 's'} waiting for you. One at a time: a choice, then an optional note.`);
|
|
13
|
+
for (let i = 0; i < decs.length; i++) {
|
|
14
|
+
const it = decs[i]; const card = walkCard(it, i + 1, decs.length);
|
|
15
|
+
io.write(''); card.lines.forEach((l) => io.write(l));
|
|
16
|
+
for (;;) {
|
|
17
|
+
const line = await io.readLine('Your choice? ');
|
|
18
|
+
if (line == null) { for (let j = i; j < decs.length; j++) st.left.push(decs[j].title || decs[j].id); io.write(''); walkSummary(st).forEach((l) => io.write(l)); return st; }
|
|
19
|
+
const rep = walkReply(it, line);
|
|
20
|
+
if (!rep) { io.write(walkNoCatch(it)); continue; }
|
|
21
|
+
if (rep.notYesNo) { io.write(walkNotYesNo(it)); continue; }
|
|
22
|
+
if (rep.defer) { st.skipped.push(it.title || it.id); break; }
|
|
23
|
+
if (rep.ask) { io.write(` Asked by ${it.payload.session || 'a session'} (id ${it.id}). ${it.payload.why || 'No further reason was given.'}`); continue; }
|
|
24
|
+
if (it.payload.high_risk === true && rep.key !== 'n') { io.write(' High risk: a yes needs your PIN, which this plain terminal cannot take. Answer it in `quox` with /approvals walkthrough, or skip it here.'); continue; }
|
|
25
|
+
try { await io.answer(it, rep.key, rep.note || null); }
|
|
26
|
+
catch (e) { io.write(` Not answered (${e.message}). The question stays open.`); continue; }
|
|
27
|
+
const label = it.payload.options.find((o) => o.key === rep.key).label;
|
|
28
|
+
io.write(`✔ ${it.title || it.id}: ${label}`);
|
|
29
|
+
st.answered.push({ title: it.title || it.id, word: label });
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
io.write(''); walkSummary(st).forEach((l) => io.write(l));
|
|
34
|
+
return st;
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launcher for the HYPERION deck TUI (`quox hyperion deck`). Same shape as lucid/deck.js: alt screen via
|
|
3
|
+
* fullscreen-ink when the terminal supports raw mode; the terminal is restored on every exit path.
|
|
4
|
+
*/
|
|
5
|
+
export async function launchHyperionDeck(api, { intervalMs = 10000 } = {}) {
|
|
6
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function' || !process.stdout.isTTY) {
|
|
7
|
+
console.error('quox hyperion deck needs an interactive terminal. Use `quox hyperion` for a one-shot view.');
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
// A long-running TUI must use React's production build (the development build grows the heap per render).
|
|
12
|
+
if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production';
|
|
13
|
+
await import('../tui/index.js'); // registers the .jsx loader hook
|
|
14
|
+
const React = (await import('react')).default;
|
|
15
|
+
const { HyperionDeck } = await import('../tui/views/HyperionDeck.jsx');
|
|
16
|
+
const { withFullScreen } = await import('fullscreen-ink');
|
|
17
|
+
const app = withFullScreen(React.createElement(HyperionDeck, { api, intervalMs }), { exitOnCtrlC: true });
|
|
18
|
+
const restore = () => { try { process.stdout.write('\x1b[?1049l\x1b[?25h'); } catch { /* best effort */ } };
|
|
19
|
+
process.once('SIGTERM', () => { restore(); process.exit(143); });
|
|
20
|
+
process.once('SIGHUP', () => { restore(); process.exit(129); });
|
|
21
|
+
process.once('exit', restore);
|
|
22
|
+
process.once('uncaughtException', (e) => { restore(); console.error(e); process.exit(1); });
|
|
23
|
+
try {
|
|
24
|
+
await app.start();
|
|
25
|
+
await app.waitUntilExit();
|
|
26
|
+
} finally {
|
|
27
|
+
restore();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <LucidCard/>: draws a Quoxon object-card view ({ box, rows }, see cards.js) for the deck.
|
|
3
|
+
*
|
|
4
|
+
* Same geometry and roles as quoxon/objects/view.js, plus what a navigable list needs:
|
|
5
|
+
* box.sel 'focus' | 'dim' the title sits on a bar (and inverse under NO_COLOR) so the selected card is unmistakable
|
|
6
|
+
* role 'sel' the highlighted action bar
|
|
7
|
+
* t (seconds) drop-in reveal: rows decode through half-width katakana, then a short colour
|
|
8
|
+
* afterglow on the real characters. Infinity = settled (what idle always shows).
|
|
9
|
+
* Colour comes from the theme roles (light/dark, 256-colour, NO_COLOR); the characters never change.
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { Text } from 'ink';
|
|
13
|
+
import { c, mono } from '../quoxon/theme.js';
|
|
14
|
+
import { agentColor } from '../quoxon/theme.js';
|
|
15
|
+
import { decodeRow, padRow, fit, len } from '../quoxon/objects/model.js';
|
|
16
|
+
import { rowStart } from '../quoxon/objects/view.js';
|
|
17
|
+
import { hexRgb, mix, rgbStr } from '../quoxon/motion/color.js';
|
|
18
|
+
|
|
19
|
+
const h = React.createElement;
|
|
20
|
+
export const BAR = { focus: '#1E3A8A', dim: '#27272A' }; // selection bars (white text on both)
|
|
21
|
+
|
|
22
|
+
function roleColor(role) {
|
|
23
|
+
if (mono()) return undefined;
|
|
24
|
+
if (typeof role === 'string' && role.startsWith('agent:')) return agentColor(role.slice(6));
|
|
25
|
+
return c(role || 'value');
|
|
26
|
+
}
|
|
27
|
+
/** A role colour lifted towards white by the afterglow (hex colours only; anything else stays as is). */
|
|
28
|
+
function glowColor(role, g) {
|
|
29
|
+
const base = roleColor(role);
|
|
30
|
+
return typeof base === 'string' && base[0] === '#' ? rgbStr(mix(hexRgb(base), [255, 255, 255], 0.65 * g)) : base;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runs(cells, key) {
|
|
34
|
+
const out = [];
|
|
35
|
+
let run = '', col, bold = false, bg, k = 0;
|
|
36
|
+
const flush = () => { if (run) out.push(h(Text, { key: `${key}-${k++}`, color: col, bold, backgroundColor: bg }, run)); run = ''; };
|
|
37
|
+
for (const cl of cells) {
|
|
38
|
+
let color, back;
|
|
39
|
+
if (cl.role === 'sel') { color = mono() ? undefined : '#FFFFFF'; back = mono() ? undefined : BAR.focus; }
|
|
40
|
+
else if (cl.glyph) color = roleColor('teal'); // the katakana flicker
|
|
41
|
+
else color = cl.glow > 0.05 ? glowColor(cl.role, cl.glow) : roleColor(cl.role);
|
|
42
|
+
const b = Boolean(cl.bold);
|
|
43
|
+
if (color !== col || b !== bold || back !== bg) { flush(); col = color; bold = b; bg = back; }
|
|
44
|
+
run += cl.ch;
|
|
45
|
+
}
|
|
46
|
+
flush();
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function LucidCard({ view, t = Infinity }) {
|
|
51
|
+
const b = view.box, w = b.width, inner = w - 4;
|
|
52
|
+
const rows = view.rows || [];
|
|
53
|
+
const bc = roleColor(b.border || 'faint');
|
|
54
|
+
const right = b.right ? ` ${b.right} ` : '';
|
|
55
|
+
const title = b.title ? ` ${fit(b.title, Math.max(4, w - 6 - (right ? len(right) + 1 : 0)))} ` : '';
|
|
56
|
+
const fill = Math.max(0, w - 3 - len(title) - len(right) - 1);
|
|
57
|
+
const bar = b.sel ? BAR[b.sel] : null;
|
|
58
|
+
const top = h(Text, { key: 'top', wrap: 'truncate' },
|
|
59
|
+
h(Text, { color: bc }, '╭─'),
|
|
60
|
+
h(Text, { color: bar && !mono() ? '#FFFFFF' : roleColor(b.titleRole || 'value'), backgroundColor: bar && !mono() ? bar : undefined, inverse: Boolean(bar) && mono(), bold: true }, title),
|
|
61
|
+
h(Text, { color: bc }, '─'.repeat(fill)),
|
|
62
|
+
h(Text, { color: roleColor(b.rightRole || 'muted') }, right),
|
|
63
|
+
h(Text, { color: bc }, '─╮'));
|
|
64
|
+
const body = rows.map((r, i) => h(Text, { key: `b${i}`, wrap: 'truncate' },
|
|
65
|
+
h(Text, { color: bc }, '│ '), ...runs(decodeRow(padRow(r, inner), t, rowStart(i, rows.length), 0.25, 70 + i), `b${i}`), h(Text, { color: bc }, ' │')));
|
|
66
|
+
const bottom = h(Text, { key: 'bot', color: bc }, '╰' + '─'.repeat(w - 2) + '╯');
|
|
67
|
+
return h(React.Fragment, null, top, ...body, bottom);
|
|
68
|
+
}
|
package/lib/lucid/LucidIntro.jsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LucidIntro: the ~1.
|
|
2
|
+
* LucidIntro: the ~1.6s startup for `quox lucid deck`.
|
|
3
3
|
*
|
|
4
|
-
* Frames (50ms): the
|
|
5
|
-
* ◉ LUCID mark fades in under it, then "tuning in…" resolves into
|
|
4
|
+
* Frames (50ms): the Quoxon particle collapse (particles swarm in and lock into the horned Q, tail last),
|
|
5
|
+
* the ◉ LUCID mark fades in under it, then "tuning in…" resolves into
|
|
6
6
|
* "◉ LUCID │ N sources live · M need you" once the first fetch has landed.
|
|
7
7
|
* The fetch is owned by the deck and runs in parallel, never gated on this.
|
|
8
8
|
* onDone fires once: at the natural end, on a hard 2.5s cap, or on a key
|
|
@@ -10,38 +10,25 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import React, { useState, useEffect, useRef } from 'react';
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
13
|
+
import { c, gradient, mono } from '../quoxon/theme.js';
|
|
14
|
+
import { QGlyph } from '../quoxon/qmotion.js';
|
|
15
|
+
import { Clock } from '../quoxon/motion/clock.js';
|
|
16
|
+
import { particleCellsAt, qCells, cellsToAnsi, labSize } from '../quoxon/qart.js';
|
|
16
17
|
|
|
17
18
|
export const FRAME_MS = 50;
|
|
18
|
-
const
|
|
19
|
-
const MIN_FRAMES =
|
|
20
|
-
const MAX_FRAMES = 50;
|
|
19
|
+
const PARTICLE_MS = 1100; // the collapse; the Q is fully settled ~470ms later
|
|
20
|
+
const MIN_FRAMES = 32; // 1.6s: the Q settles, then the live line shows
|
|
21
|
+
const MAX_FRAMES = 50; // 2.5s cap if the fetch never lands
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
for (const ch of LETTERS) {
|
|
29
|
-
const g = BLOCK_FONT[ch][row];
|
|
30
|
-
for (let i = 0; i < g.length; i++) {
|
|
31
|
-
const x = col + i;
|
|
32
|
-
const lit = g[i] !== ' ';
|
|
33
|
-
if (x > colsRevealed) { out += ' '; continue; }
|
|
34
|
-
if (!lit) { out += ' '; continue; }
|
|
35
|
-
const edge = colsRevealed - x < 2 && colsRevealed < total;
|
|
36
|
-
out += edge ? chalk.white('██') : rgb(gradientColor(x / total + frame * 0.02), '██');
|
|
37
|
-
}
|
|
38
|
-
out += ' ';
|
|
39
|
-
col += g.length + 1;
|
|
40
|
-
}
|
|
41
|
-
return out;
|
|
23
|
+
/** Rows of the particle Q at frame `frame` (motion off: the settled Q). Under 12 rows the compact Q. */
|
|
24
|
+
export function introQ(cols, rows, frame, motion = true) {
|
|
25
|
+
const size = labSize(cols, Math.max(0, rows - 8));
|
|
26
|
+
const color = !mono();
|
|
27
|
+
const t = frame * FRAME_MS;
|
|
28
|
+
return cellsToAnsi(motion ? particleCellsAt(size, t, { dur: PARTICLE_MS, color }) : qCells(size, { color }));
|
|
42
29
|
}
|
|
43
30
|
|
|
44
|
-
export function LucidIntro({ data, err, onDone, rows = 24 }) {
|
|
31
|
+
export function LucidIntro({ data, err, onDone, rows = 24, cols = 100, motion = true }) {
|
|
45
32
|
const [frame, setFrame] = useState(0);
|
|
46
33
|
const doneRef = useRef(false);
|
|
47
34
|
const readyAt = useRef(null);
|
|
@@ -49,10 +36,11 @@ export function LucidIntro({ data, err, onDone, rows = 24 }) {
|
|
|
49
36
|
|
|
50
37
|
if (sit && readyAt.current == null) readyAt.current = frame;
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
39
|
+
// The deck's shared clock drives the intro too: one timer, armed only while the intro still has frames to play.
|
|
40
|
+
const clockRef = useRef(null);
|
|
41
|
+
if (!clockRef.current) clockRef.current = new Clock(() => setFrame((f) => f + 1));
|
|
42
|
+
useEffect(() => { if (!doneRef.current) clockRef.current.schedule([{ every: FRAME_MS }]); });
|
|
43
|
+
useEffect(() => () => clockRef.current.stop(), []);
|
|
56
44
|
|
|
57
45
|
const finishAt = readyAt.current != null
|
|
58
46
|
? Math.max(MIN_FRAMES, readyAt.current + 6)
|
|
@@ -62,44 +50,43 @@ export function LucidIntro({ data, err, onDone, rows = 24 }) {
|
|
|
62
50
|
if (frame >= finishAt && !doneRef.current) { doneRef.current = true; onDone(); }
|
|
63
51
|
}, [frame, finishAt, onDone]);
|
|
64
52
|
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const markOn = frame >= 6;
|
|
69
|
-
const resolved = sit && frame >= 14 && frame >= (readyAt.current ?? 0) + 2;
|
|
53
|
+
const qRows = introQ(cols, rows, frame, motion);
|
|
54
|
+
const markOn = frame >= 14;
|
|
55
|
+
const resolved = sit && frame >= 22 && frame >= (readyAt.current ?? 0) + 2;
|
|
70
56
|
const cov = (sit && sit.cannot_see && sit.cannot_see.coverage) || { live: 0, expected: 0 };
|
|
71
57
|
const dots = '.'.repeat((frame % 3) + 1).padEnd(3, ' ');
|
|
72
58
|
|
|
73
|
-
const mark =
|
|
74
|
-
<Text
|
|
75
|
-
|
|
59
|
+
const mark = (
|
|
60
|
+
<Text>
|
|
61
|
+
<QGlyph state="working" ms={Date.now()} quiet={!motion} />
|
|
62
|
+
<Text> </Text>
|
|
63
|
+
{gradient('LUCID').map((g, i) => <Text key={i} bold color={g.color}>{g.ch}</Text>)}
|
|
64
|
+
</Text>
|
|
65
|
+
);
|
|
76
66
|
|
|
77
67
|
return (
|
|
78
68
|
<Box flexDirection="column" alignItems="center" justifyContent="center" flexGrow={1} paddingY={1}>
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
</Box>
|
|
83
|
-
) : (
|
|
84
|
-
<Text bold color={PALETTE.brand}>Q U O X</Text>
|
|
85
|
-
)}
|
|
69
|
+
<Box flexDirection="column" alignItems="center">
|
|
70
|
+
{qRows.map((l, r) => <Text key={r} wrap="truncate">{l}</Text>)}
|
|
71
|
+
</Box>
|
|
86
72
|
<Box marginTop={1}>{markOn ? mark : <Text> </Text>}</Box>
|
|
87
73
|
<Box marginTop={1}>
|
|
88
|
-
{frame <
|
|
74
|
+
{frame < 16 ? <Text> </Text> : resolved ? (
|
|
89
75
|
<Text>
|
|
90
|
-
<
|
|
91
|
-
<Text color={
|
|
92
|
-
<Text color={
|
|
93
|
-
<Text color={
|
|
94
|
-
<Text
|
|
76
|
+
<QGlyph state={(sit.needs_you || []).length ? 'waiting' : 'resting'} ms={Date.now()} quiet />
|
|
77
|
+
<Text bold color={c('sky')}> LUCID</Text>
|
|
78
|
+
<Text color={c('muted')}> │ </Text>
|
|
79
|
+
<Text color={c('ok')}>{cov.live} sources live</Text>
|
|
80
|
+
<Text color={c('muted')}> · </Text>
|
|
81
|
+
<Text bold color={(sit.needs_you || []).length ? c('warn') : c('ok')}>{(sit.needs_you || []).length} need you</Text>
|
|
95
82
|
</Text>
|
|
96
83
|
) : err && !sit ? (
|
|
97
|
-
<Text color={
|
|
84
|
+
<Text color={c('error')}>LUCID unreachable, still trying</Text>
|
|
98
85
|
) : (
|
|
99
|
-
<Text color={
|
|
86
|
+
<Text color={c('muted')}>tuning in{dots}</Text>
|
|
100
87
|
)}
|
|
101
88
|
</Box>
|
|
102
|
-
<Box marginTop={1}><Text color={
|
|
89
|
+
<Box marginTop={1}><Text color={c('muted')}>any key to skip</Text></Box>
|
|
103
90
|
</Box>
|
|
104
91
|
);
|
|
105
92
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LUCID object cards: pure views for the deck, in the Quoxon object-card shape ({ box, rows } of
|
|
3
|
+
* { text, role } segments, see quoxon/objects/model.js). An area is a tile card; a Needs-you item is a card
|
|
4
|
+
* that opens in place into who / what / why / when and the actions. Nothing here reads a clock or does I/O.
|
|
5
|
+
*
|
|
6
|
+
* Roles are theme roles; the renderer (LucidCard.jsx) also understands 'sel' (the selected action bar).
|
|
7
|
+
* Text is WRAPPED to the card, never clipped, except for a single action line and the command line.
|
|
8
|
+
*/
|
|
9
|
+
import { seg, fit, padRow } from '../quoxon/objects/model.js';
|
|
10
|
+
import { wrap, whenText, ageOf, GLYPH, clip } from './fmt.js';
|
|
11
|
+
|
|
12
|
+
export const URGENCY_ROLE = { now: 'error', today: 'warn', 'this week': 'sky', whenever: 'muted', fyi: 'muted' };
|
|
13
|
+
export const URGENCY_LABEL = { now: 'NOW', today: 'TODAY', 'this week': 'THIS WEEK', whenever: 'WHENEVER', fyi: 'FYI' };
|
|
14
|
+
export const STATE_ROLE = { ok: 'ok', attention: 'warn', unknown: 'muted', blind: 'error' };
|
|
15
|
+
export const STATE_WORD = { ok: 'ok', attention: 'attention', unknown: 'no signal', blind: 'blind' };
|
|
16
|
+
|
|
17
|
+
const nm = (x) => (typeof x === 'string' ? x : (x && (x.label || x.name || x.L0 || x.id)) || '');
|
|
18
|
+
const LW = 8; // label column of the open card
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An area tile. d: { area, label, state, L0, last, n (needs), active (filter on), selected, focused,
|
|
22
|
+
* expanded, now }. A tile is one status row; the selected tile in a focused pane opens to its sentence.
|
|
23
|
+
*/
|
|
24
|
+
export function lucidArea(d, cols) {
|
|
25
|
+
const w = Math.max(18, cols), inner = w - 4;
|
|
26
|
+
const all = d.area == null;
|
|
27
|
+
const glyph = all ? '◉' : (GLYPH[d.state] || '?');
|
|
28
|
+
const role = all ? 'sky' : (STATE_ROLE[d.state] || 'muted');
|
|
29
|
+
const right = `${d.active ? '● ' : ''}${d.n ? d.n : '·'}`;
|
|
30
|
+
const rows = [];
|
|
31
|
+
const word = all ? 'everything' : (STATE_WORD[d.state] || 'unknown');
|
|
32
|
+
const age = d.last && d.last.ts != null && d.now != null ? `last ${ageOf(d.last.ts, d.now)}` : '';
|
|
33
|
+
const line = age && [...`${word} · ${age}`].length <= inner ? `${word} · ${age}` : word;
|
|
34
|
+
rows.push([seg(line, d.selected ? 'value' : role === 'ok' ? 'muted' : role)]);
|
|
35
|
+
if (d.expanded && d.L0) for (const ln of wrap(d.L0, inner)) rows.push([seg(ln, 'value')]);
|
|
36
|
+
const mark = d.selected ? '▶ ' : '';
|
|
37
|
+
return {
|
|
38
|
+
box: {
|
|
39
|
+
width: w, title: `${mark}${glyph} ${fit(d.label || d.area || 'All areas', Math.max(4, inner - 8))}`, titleRole: role,
|
|
40
|
+
right, rightRole: d.n ? 'warn' : 'muted', border: d.selected ? 'sky' : 'faint', sel: d.selected ? (d.focused ? 'focus' : 'dim') : null,
|
|
41
|
+
},
|
|
42
|
+
rows,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A Needs-you card. d: { item, areaLabel, isNew, selected, focused, open, mSel, now }.
|
|
48
|
+
* Collapsed: the wrapped sentence. Open: the sentence, who/what/why/when, affects, then the actions with
|
|
49
|
+
* the highlighted one as a bar and its exact command beneath.
|
|
50
|
+
*/
|
|
51
|
+
export function lucidNeed(d, cols) {
|
|
52
|
+
const it = d.item, w = Math.max(24, cols), inner = w - 4;
|
|
53
|
+
const urg = it.urgency || 'whenever';
|
|
54
|
+
const a = it.L1 || {};
|
|
55
|
+
const ms = (it.L2 && it.L2.measures) || [];
|
|
56
|
+
const mark = d.selected ? '▶ ' : '';
|
|
57
|
+
const rows = [];
|
|
58
|
+
const text = (d.isNew ? '● ' : '') + (it.L0 || '');
|
|
59
|
+
wrap(text, inner).forEach((ln, i) => rows.push([seg(ln, d.isNew ? 'ok' : d.selected ? 'value' : 'text', d.selected)]));
|
|
60
|
+
if (d.open) {
|
|
61
|
+
const detail = (lbl, v) => wrap(v, inner - LW).forEach((ln, i) => rows.push([seg(i ? ' '.repeat(LW) : lbl.padEnd(LW), i ? 'muted' : 'label'), seg(ln, 'text')]));
|
|
62
|
+
for (const [lbl, v] of [['Who', a.who], ['What', a.what], ['Why', a.why], ['When', whenText(a.when, d.now)]]) if (v) detail(lbl, v);
|
|
63
|
+
if ((a.affected || []).length) rows.push([seg('Affects'.padEnd(LW), 'label'), seg(clip(a.affected.map(nm).filter(Boolean).join(', '), inner - LW), 'muted')]);
|
|
64
|
+
rows.push(ms.length ? [seg('What you can do', 'key', true)] : [seg('No actions to offer here. Esc closes.', 'muted')]);
|
|
65
|
+
ms.forEach((m, mi) => {
|
|
66
|
+
const on = mi === d.mSel;
|
|
67
|
+
const risk = ` ${m.risk}`;
|
|
68
|
+
const label = fit(m.say || m.label, Math.max(8, inner - 2 - risk.length));
|
|
69
|
+
if (on) rows.push([seg(`❯ ${label}${risk}`.padEnd(inner, ' '), 'sel', true)]);
|
|
70
|
+
else rows.push([seg(' ', 'muted'), seg(label, 'text'), seg(risk, m.risk === 'dangerous' ? 'error' : m.risk === 'gated' ? 'warn' : 'muted')]);
|
|
71
|
+
});
|
|
72
|
+
const cmd = ms[d.mSel] && ms[d.mSel].command;
|
|
73
|
+
if (cmd) rows.push([seg(' $ ', 'muted'), seg(clip(cmd, inner - 4), 'code')]);
|
|
74
|
+
else if (ms.some((m) => m.command)) rows.push([seg(' ', 'muted')]);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
box: {
|
|
78
|
+
width: w, title: `${mark}${URGENCY_LABEL[urg] || String(urg).toUpperCase()} · ${it.kind || 'item'}`, titleRole: URGENCY_ROLE[urg] || 'muted',
|
|
79
|
+
right: d.areaLabel || '', rightRole: 'muted', border: d.selected ? 'sky' : (urg === 'now' ? 'error' : 'faint'), sel: d.selected ? (d.focused ? 'focus' : 'dim') : null,
|
|
80
|
+
},
|
|
81
|
+
rows,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const cardHeight = (view) => view.rows.length + 2;
|
|
86
|
+
export { padRow };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sitrep as chat Markdown (Discord, Matrix, slots): short, scannable, under one Discord message.
|
|
3
|
+
* Owner 2026-10-17: "Format your sitrep" after the spoken paragraph was posted to Discord as a wall of text.
|
|
4
|
+
* Pure: data in, string out. No colour codes, no box drawing (chat clients mangle both).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const URGENCY = { now: '🔴', today: '🟠', this_week: '🟡', week: '🟡', whenever: '⚪', fyi: '⚪' };
|
|
8
|
+
const STATE = { attention: '⚠️', ok: '✅', partial: '🟡', unknown: '❔', blind: '⛔' };
|
|
9
|
+
export const MAX_CHARS = 1900; // Discord's limit is 2000 per message
|
|
10
|
+
|
|
11
|
+
const clip = (s, n) => {
|
|
12
|
+
const t = String(s || '').replace(/\s+/g, ' ').trim();
|
|
13
|
+
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function age(ms) {
|
|
17
|
+
if (!(ms > 0)) return '';
|
|
18
|
+
const m = Math.floor(ms / 60000);
|
|
19
|
+
if (m < 60) return `${m}m`;
|
|
20
|
+
const h = Math.floor(m / 60);
|
|
21
|
+
if (h < 48) return `${h}h`;
|
|
22
|
+
return `${Math.floor(h / 24)}d`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clockLondon(now) {
|
|
26
|
+
try {
|
|
27
|
+
return new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }).format(new Date(now));
|
|
28
|
+
} catch { return new Date(now).toISOString().slice(11, 16) + ' UTC'; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** opts: { now, maxNeeds (default 6), maxChars (default MAX_CHARS) } */
|
|
32
|
+
export function renderSitrepMarkdown(data, opts = {}) {
|
|
33
|
+
const sit = (data && data.situation) || data || {};
|
|
34
|
+
const now = opts.now ?? Date.now();
|
|
35
|
+
const maxNeeds = opts.maxNeeds ?? 6;
|
|
36
|
+
const maxChars = opts.maxChars ?? MAX_CHARS;
|
|
37
|
+
const cov = sit.coverage || (sit.cannot_see && sit.cannot_see.coverage) || { live: 0, expected: 0 };
|
|
38
|
+
const missing = sit.coverage && Array.isArray(sit.coverage.missing) ? sit.coverage.missing : [];
|
|
39
|
+
const needs = Array.isArray(sit.needs_you) ? sit.needs_you : [];
|
|
40
|
+
const low = needs.filter((n) => n.urgency === 'whenever' || n.urgency === 'fyi').length;
|
|
41
|
+
|
|
42
|
+
const out = [];
|
|
43
|
+
out.push(`**◉ LUCID sitrep** · ${clockLondon(now)} · ${cov.live}/${cov.expected} sources`);
|
|
44
|
+
out.push('');
|
|
45
|
+
if (!needs.length) {
|
|
46
|
+
out.push('**Needs you:** nothing right now.');
|
|
47
|
+
} else {
|
|
48
|
+
out.push(`**Needs you (${needs.length}${low ? `, ${low} low priority` : ''})**`);
|
|
49
|
+
needs.slice(0, maxNeeds).forEach((n, i) => {
|
|
50
|
+
const w = n.L1 && n.L1.when && n.L1.when.waited_ms;
|
|
51
|
+
const tail = [n.area, age(w)].filter(Boolean).join(', ');
|
|
52
|
+
out.push(`${i + 1}. ${URGENCY[n.urgency] || '•'} ${clip(n.L0, 90)}${tail ? ` _(${tail})_` : ''}`);
|
|
53
|
+
});
|
|
54
|
+
if (needs.length > maxNeeds) out.push(`…and ${needs.length - maxNeeds} more: \`quox sitrep\``);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const areas = Array.isArray(sit.areas) ? sit.areas : [];
|
|
58
|
+
if (areas.length) {
|
|
59
|
+
out.push('');
|
|
60
|
+
out.push('**At a glance**');
|
|
61
|
+
for (const a of areas) out.push(`${STATE[a.state] || '•'} **${a.label || a.area}**: ${clip(a.L0, 80)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dau = Array.isArray(sit.doesnt_add_up) ? sit.doesnt_add_up : [];
|
|
65
|
+
if (dau.length) {
|
|
66
|
+
out.push('');
|
|
67
|
+
out.push(`**Doesn't add up (${dau.length}):** ${clip(dau[0].L0, 90)}${dau.length > 1 ? ' …' : ''}`);
|
|
68
|
+
}
|
|
69
|
+
out.push('');
|
|
70
|
+
out.push(missing.length ? `**Can't see:** ${missing.join(', ')}` : `**Can't see:** nothing, every source is reporting.`);
|
|
71
|
+
|
|
72
|
+
// Stay inside one chat message: drop "at a glance" rows from the bottom, then needs, never the header.
|
|
73
|
+
let text = out.join('\n');
|
|
74
|
+
while (text.length > maxChars && out.length > 3) {
|
|
75
|
+
const i = out.findLastIndex((l) => /^(✅|⚠️|🟡|❔|⛔|•) \*\*/.test(l));
|
|
76
|
+
if (i < 0) break;
|
|
77
|
+
out.splice(i, 1);
|
|
78
|
+
text = out.join('\n');
|
|
79
|
+
}
|
|
80
|
+
return text.length > maxChars ? `${text.slice(0, maxChars - 1)}…` : text;
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LUCID deck motion: pure helpers for the deck's ONE clock (quoxon/motion/clock.js).
|
|
3
|
+
*
|
|
4
|
+
* Idle is still. The deck declares a demand each render; the clock keeps one timer for the soonest demand and
|
|
5
|
+
* none at all when nothing asks. Frames (~12 fps) are demanded ONLY while something is visibly moving: a fetch
|
|
6
|
+
* in flight (the Q works), the Q settling after a state change (a short window, then still colour), or a
|
|
7
|
+
* card dropping in. Everything else is a one-shot wake (toast expiry, ghost expiry, the minute tick).
|
|
8
|
+
*
|
|
9
|
+
* LUCID state -> Q state (qglyph.js): needs you = waiting, fetching = working, unreachable = failing,
|
|
10
|
+
* quiet = resting. Failing wins over working so an outage never reads as busy.
|
|
11
|
+
*/
|
|
12
|
+
import { FRAME_MS } from '../quoxon/motion/clock.js';
|
|
13
|
+
|
|
14
|
+
export { FRAME_MS };
|
|
15
|
+
/** After a state change the Q animates this long, then holds a still colour (no timer, no writes). */
|
|
16
|
+
export const SETTLE_MS = 2400;
|
|
17
|
+
/** A card dropping in is revealing for this long from its start (rows decode, then a short afterglow). */
|
|
18
|
+
export const REVEAL_MS = 1500;
|
|
19
|
+
/** Cards of one load start this far apart so the board fills top to bottom. */
|
|
20
|
+
export const STAGGER_MS = 70;
|
|
21
|
+
|
|
22
|
+
export function glyphStateOf({ err = null, fetching = false, needs = 0 } = {}) {
|
|
23
|
+
if (err) return 'failing';
|
|
24
|
+
if (fetching) return 'working';
|
|
25
|
+
return needs > 0 ? 'waiting' : 'resting';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Is the Q moving at `now`? Working moves while it lasts; every other state settles after SETTLE_MS. */
|
|
29
|
+
export function glyphAnimating({ state, since, now, motion }) {
|
|
30
|
+
if (!motion) return false;
|
|
31
|
+
if (state === 'working') return true;
|
|
32
|
+
return since != null && now - since < SETTLE_MS;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Seconds into a card's reveal, or Infinity when settled (or motion is off). A future start is negative. */
|
|
36
|
+
export function revealT(startAt, now, motion) {
|
|
37
|
+
if (!motion || startAt == null) return Infinity;
|
|
38
|
+
const ms = now - startAt;
|
|
39
|
+
return ms >= REVEAL_MS ? Infinity : ms / 1000;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The demands for this render. frames: something is moving right now. at: epoch ms of one-shot wakes
|
|
44
|
+
* (past or null entries are ignored). Without motion there are no frame demands at all.
|
|
45
|
+
*/
|
|
46
|
+
export function deckDemands({ motion = false, frames = false, at = [], now = Date.now() } = {}) {
|
|
47
|
+
const d = [];
|
|
48
|
+
if (motion && frames) d.push({ every: FRAME_MS });
|
|
49
|
+
let soonest = null;
|
|
50
|
+
for (const a of at) if (Number.isFinite(a) && a > now && (soonest == null || a < soonest)) soonest = a;
|
|
51
|
+
if (soonest != null) d.push({ at: soonest });
|
|
52
|
+
return d;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Next wall-clock minute boundary for a displayed time (the injected clock, so tests stay deterministic). */
|
|
56
|
+
export const nextMinuteAt = (wall, shown) => wall + (60000 - (shown % 60000));
|
package/lib/quoxon/activity.js
CHANGED
|
@@ -37,6 +37,13 @@ const TOOL_TABLE = [
|
|
|
37
37
|
[/(^|_)(list|get|status)$|^contacts_/, 'Looking up'],
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
+
/** QUOXON-FW3: "Delegating <Agent> · <task gist>" instead of an anonymous delegate_to_agent. */
|
|
41
|
+
function delegateTarget(ev) {
|
|
42
|
+
const id = ev.agent_id || (String(ev.summary || '').match(/\bagent_id=(\S+)/) || [])[1] || '';
|
|
43
|
+
const name = ev.agent_name || id;
|
|
44
|
+
return { ...ev, target: [name, ev.task_gist].filter(Boolean).join(' \u00b7 ') };
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
/** { word, detail } for a tool call. Unknown tools are "Working" with the tool name, never a guess. */
|
|
41
48
|
export function toolWord(tool, ev = {}) {
|
|
42
49
|
const name = String(tool || '').toLowerCase();
|
|
@@ -46,7 +53,7 @@ export function toolWord(tool, ev = {}) {
|
|
|
46
53
|
return { word: 'Working', detail: name || null };
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
const mk = (word, detail = null, now = 0, tone = 'work', flash = null) => ({ word, detail: detail ? clipD(detail) : null, tone, flash, since: now });
|
|
56
|
+
const mk = (word, detail = null, now = 0, tone = 'work', flash = null, tool = null) => ({ word, detail: detail ? clipD(detail) : null, tone, flash, since: now, ...(tool ? { tool: clipD(tool, 40) } : {}) });
|
|
50
57
|
export const initialActivity = (now = 0) => mk('Sending', null, now); // the request was dispatched: true from turn.begin
|
|
51
58
|
|
|
52
59
|
/** Trace lines from the governance chain. Labels as the server sends them. */
|
|
@@ -82,7 +89,12 @@ export function nextActivity(prev, ev, now = 0, ctx = {}) {
|
|
|
82
89
|
return p;
|
|
83
90
|
}
|
|
84
91
|
case 'trace': { const t = traceWord(ev); return t ? mk(t[0], t[1], now) : p; }
|
|
85
|
-
case 'tool.started': { const t = toolWord(ev.tool, ev); return mk(t.word, t.detail, now); }
|
|
92
|
+
case 'tool.started': { const t = toolWord(ev.tool, ev.tool === 'delegate_to_agent' ? delegateTarget(ev) : ev); return mk(t.word, t.detail, now, 'work', null, ev.tool || null); }
|
|
93
|
+
case 'delegation.progress': { // QUOXON 2026-10-17: a specialist's real tool call, forwarded by the server
|
|
94
|
+
if (!ev.tool) return p;
|
|
95
|
+
const who = ev.agent_name || ev.agent_id || 'specialist';
|
|
96
|
+
return mk('Delegating', `${who} \u00b7 ${ev.tool}${ev.detail ? ` (${ev.detail})` : ''}`, now, 'work', null, ev.tool);
|
|
97
|
+
}
|
|
86
98
|
case 'tool.finished': return ev.status === 'failed' ? { ...p, flash: now } : p; // keep the word, flash the Q only
|
|
87
99
|
case 'approval.requested': return mk('Waiting', 'for your approval', now, 'wait');
|
|
88
100
|
case 'approval.resolved': return mk('Resuming', null, now);
|