quoxcli 0.3.1 → 0.4.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/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +20 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -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/index.js +12 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +173 -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/v1.js +34 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +46 -7
- package/lib/commands/approvals.js +5 -0
- package/lib/commands/ask.js +3 -1
- package/lib/commands/blog.js +110 -0
- package/lib/commands/chat.js +127 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +3 -32
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/loop.js +291 -0
- package/lib/commands/objective.js +48 -1
- package/lib/commands/org.js +21 -4
- package/lib/commands/provider.js +58 -0
- 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 +18 -1
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +113 -0
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +20 -4
- package/lib/commands/vm.js +2 -2
- 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/client.js +10 -2
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +1 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +1 -3
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +36 -7
- package/lib/core/output.js +7 -7
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- 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 +23 -0
- package/lib/quoxon/api.js +192 -6
- package/lib/quoxon/app.js +655 -89
- package/lib/quoxon/approvalFacts.js +8 -6
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +63 -4
- package/lib/quoxon/commandShort.js +15 -3
- package/lib/quoxon/commands.js +75 -10
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/events.js +24 -3
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +12 -3
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/objectiveOffer.js +82 -0
- 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/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +1 -1
- package/lib/quoxon/orgSwitch.js +27 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +6 -3
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +130 -33
- package/lib/quoxon/roster.js +26 -12
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +76 -1
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/palette.js +1 -1
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +25 -1
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/suggestCommand.js +33 -2
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/whatsnew.jsonl +45 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/package.json +2 -1
- 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/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/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -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/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/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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension host (QUOXON, 2026-10-26; contract: EXTENSIONS.md). Optional npm packages that add surfaces to quoxon
|
|
3
|
+
* (a MIDI pad deck, a status bar, ...). Nothing loads unless the package is installed AND listed in
|
|
4
|
+
* ~/.quox/extensions.json (`quox ext add <pkg>`, `/ext list|on|off <name>`). Core knows nothing about any one extension:
|
|
5
|
+
* it pushes one generic snapshot (buildSnapshot) and accepts three emit actions (open_approval, pick, alert_ack).
|
|
6
|
+
* An extension can OPEN an approval or PRESELECT an answer; it can never resolve one.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
|
|
14
|
+
export const THROTTLE_MS = 1000;
|
|
15
|
+
export const notInstalled = (name) => `not installed: npm i -g ${name}`;
|
|
16
|
+
const HEALTH = new Set(['ok', 'warn', 'down']);
|
|
17
|
+
const CHAIN = new Set(['ok', 'broken', 'unknown']);
|
|
18
|
+
const FLEET_MAX = 16;
|
|
19
|
+
|
|
20
|
+
// ---- the listing: ~/.quox/extensions.json ---------------------------------------------------------------------------
|
|
21
|
+
export const extListPath = (home = os.homedir()) => path.join(home, '.quox', 'extensions.json');
|
|
22
|
+
const validName = (n) => typeof n === 'string' && /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(n);
|
|
23
|
+
|
|
24
|
+
export function readExtList(file = extListPath()) {
|
|
25
|
+
try {
|
|
26
|
+
const j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
27
|
+
return (Array.isArray(j?.extensions) ? j.extensions : []).filter((x) => validName(x?.name)).map((x) => ({ name: x.name, enabled: x.enabled !== false }));
|
|
28
|
+
} catch { return []; } // no file or unreadable: nothing is listed, so nothing loads
|
|
29
|
+
}
|
|
30
|
+
export function writeExtList(list, file = extListPath()) {
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
fs.writeFileSync(file, JSON.stringify({ extensions: list }, null, 2) + '\n');
|
|
33
|
+
}
|
|
34
|
+
export function extAdd(name, file = extListPath()) {
|
|
35
|
+
if (!validName(name)) return { ok: false, text: `"${name}" is not a package name.` };
|
|
36
|
+
const list = readExtList(file);
|
|
37
|
+
const had = list.find((x) => x.name === name);
|
|
38
|
+
if (had) had.enabled = true; else list.push({ name, enabled: true });
|
|
39
|
+
writeExtList(list, file);
|
|
40
|
+
return { ok: true, text: `${name} is listed. It loads in quoxon once installed (npm i -g ${name}).` };
|
|
41
|
+
}
|
|
42
|
+
export function extRemove(name, file = extListPath()) {
|
|
43
|
+
const list = readExtList(file);
|
|
44
|
+
if (!list.some((x) => x.name === name)) return { ok: false, text: `${name} is not listed.` };
|
|
45
|
+
writeExtList(list.filter((x) => x.name !== name), file);
|
|
46
|
+
return { ok: true, text: `${name} is no longer listed.` };
|
|
47
|
+
}
|
|
48
|
+
export function extSetEnabled(name, enabled, file = extListPath()) {
|
|
49
|
+
const list = readExtList(file);
|
|
50
|
+
const it = list.find((x) => x.name === name);
|
|
51
|
+
if (!it) return { ok: false, text: `${name} is not listed. quox ext add ${name}` };
|
|
52
|
+
it.enabled = enabled; writeExtList(list, file);
|
|
53
|
+
return { ok: true, text: `${name} ${enabled ? 'enabled' : 'disabled'}.` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- resolution: env override, local, global ------------------------------------------------------------------------
|
|
57
|
+
export const envKey = (name) => `QUOX_EXT_${String(name).replace(/^@/, '').replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}_MODULE`;
|
|
58
|
+
const defaultImporter = (spec) => import(spec);
|
|
59
|
+
const NOT_FOUND = (e) => e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND';
|
|
60
|
+
|
|
61
|
+
let npmRootCache; // `npm root -g` runs at most once per process
|
|
62
|
+
export function globalNpmRoot(run = execFileSync) {
|
|
63
|
+
if (npmRootCache !== undefined) return npmRootCache;
|
|
64
|
+
try { npmRootCache = String(run('npm', ['root', '-g'], { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] })).trim() || null; } catch { npmRootCache = null; }
|
|
65
|
+
return npmRootCache;
|
|
66
|
+
}
|
|
67
|
+
export const _resetNpmRootCache = () => { npmRootCache = undefined; };
|
|
68
|
+
|
|
69
|
+
/** The file a package directory imports: exports["."] (import/default), module, main, index.js. */
|
|
70
|
+
export function packageEntry(dir) {
|
|
71
|
+
try {
|
|
72
|
+
const pj = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
73
|
+
const ex = pj.exports;
|
|
74
|
+
const root = typeof ex === 'string' ? ex : (ex?.['.'] ?? ex);
|
|
75
|
+
const pick = typeof root === 'string' ? root : (root?.import || root?.default || (typeof root?.import === 'object' ? root.import.default : null));
|
|
76
|
+
return path.join(dir, pick || pj.module || pj.main || 'index.js');
|
|
77
|
+
} catch { return path.join(dir, 'index.js'); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find and import one extension. Order: QUOX_EXT_<NAME>_MODULE, local import(name), then the global npm root
|
|
82
|
+
* (dirname(execPath)/../lib/node_modules first, which costs no spawn; `npm root -g` once, cached, only if that misses).
|
|
83
|
+
* -> { ok:true, mod, where } | { ok:false, notFound:true, tried:[...] } | { ok:false, error }
|
|
84
|
+
*/
|
|
85
|
+
export async function resolveExtension(name, { env = process.env, importer = defaultImporter, execPath = process.execPath, npmRoot = globalNpmRoot, exists = fs.existsSync } = {}) {
|
|
86
|
+
const tried = [];
|
|
87
|
+
const ov = env?.[envKey(name)];
|
|
88
|
+
if (ov) {
|
|
89
|
+
tried.push(`${envKey(name)}=${ov}`);
|
|
90
|
+
try { return { ok: true, mod: await importer(path.isAbsolute(ov) ? pathToFileURL(ov).href : ov), where: `env ${envKey(name)} (${ov})` }; }
|
|
91
|
+
catch (e) { return { ok: false, error: e?.message || String(e), tried }; } // an override that fails is an error, never a silent fall-through
|
|
92
|
+
}
|
|
93
|
+
tried.push(`local import('${name}')`);
|
|
94
|
+
try { return { ok: true, mod: await importer(name), where: `local (${name})` }; }
|
|
95
|
+
catch (e) { if (!NOT_FOUND(e)) return { ok: false, error: e?.message || String(e), tried }; }
|
|
96
|
+
const roots = [];
|
|
97
|
+
const guess = path.join(path.dirname(execPath || ''), '..', 'lib', 'node_modules');
|
|
98
|
+
roots.push(guess);
|
|
99
|
+
let spawned = false;
|
|
100
|
+
for (let i = 0; i < 2; i++) {
|
|
101
|
+
for (const root of roots.splice(0)) {
|
|
102
|
+
const dir = path.join(root, name);
|
|
103
|
+
tried.push(`global ${dir}`);
|
|
104
|
+
if (!exists(dir)) continue;
|
|
105
|
+
try { return { ok: true, mod: await importer(pathToFileURL(packageEntry(dir)).href), where: `global (${dir})` }; }
|
|
106
|
+
catch (e) { return { ok: false, error: e?.message || String(e), tried }; }
|
|
107
|
+
}
|
|
108
|
+
if (spawned) break;
|
|
109
|
+
spawned = true;
|
|
110
|
+
const g = npmRoot();
|
|
111
|
+
if (g && path.resolve(g) !== path.resolve(guess)) roots.push(g);
|
|
112
|
+
}
|
|
113
|
+
return { ok: false, notFound: true, tried };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** An extension module -> { name, version, commands, create } or null. Accepts default export or the module itself. */
|
|
117
|
+
export function normalizeExtension(mod, fallbackName) {
|
|
118
|
+
const ext = mod?.default && typeof mod.default === 'object' ? mod.default : mod;
|
|
119
|
+
const create = typeof ext?.create === 'function' ? ext.create : (typeof mod?.createDeck === 'function' ? mod.createDeck : null); // createDeck: quoxdeck v1
|
|
120
|
+
if (!create) return null;
|
|
121
|
+
const commands = (Array.isArray(ext.commands) ? ext.commands : []).filter((c) => /^\/[a-z][\w-]*$/i.test(c?.name || '') && typeof c.run === 'function');
|
|
122
|
+
return { name: ext.name || fallbackName, version: ext.version || null, commands, create };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- the snapshot every extension gets ------------------------------------------------------------------------------
|
|
126
|
+
function itemRisk(it) {
|
|
127
|
+
return it?.privileged === true || it?.payload?.privileged === true || it?.risk === 'high' || it?.payload?.risk === 'high' ? 'high' : 'low';
|
|
128
|
+
}
|
|
129
|
+
const hostState = (s) => (s === 'ok' || s === 'warn' || s === 'down' ? s : (/^(down|offline|failed|unreachable)$/i.test(s || '') ? 'down' : (/^(warn|warning|degraded)$/i.test(s || '') ? 'warn' : 'ok')));
|
|
130
|
+
|
|
131
|
+
/** Pure. fleet input: [{id|name, health|state}] or null. */
|
|
132
|
+
export function buildFleet(hosts) {
|
|
133
|
+
if (!Array.isArray(hosts)) return { total: 0, online: 0, offline: 0, warn: 0, hosts: [] };
|
|
134
|
+
const list = hosts.map((h) => ({ name: String(h.name ?? h.id ?? '').slice(0, 60), state: hostState(h.state ?? h.health) }));
|
|
135
|
+
return {
|
|
136
|
+
total: list.length, online: list.filter((h) => h.state !== 'down').length,
|
|
137
|
+
offline: list.filter((h) => h.state === 'down').length, warn: list.filter((h) => h.state === 'warn').length,
|
|
138
|
+
hosts: list.slice(0, FLEET_MAX)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildSnapshot({ org = null, items = [], health = {}, loops = [], event = null, fleet = null, evidence = null, sessions = null } = {}) {
|
|
143
|
+
const list = (items || []).map((it, i) => {
|
|
144
|
+
const row = {
|
|
145
|
+
n: i + 1, id: it.id ?? null, risk: itemRisk(it),
|
|
146
|
+
title: String(it.title || it.summary || it.payload?.title || it.payload?.question || it.payload?.summary || '').slice(0, 120),
|
|
147
|
+
agent: it.agent || it.requester || it.payload?.agent || null
|
|
148
|
+
};
|
|
149
|
+
const opts = Array.isArray(it.payload?.options) ? it.payload.options.filter((o) => o?.key != null).map((o) => ({ key: String(o.key), label: String(o.label ?? o.key) })) : [];
|
|
150
|
+
if (opts.length) row.options = opts;
|
|
151
|
+
return row;
|
|
152
|
+
});
|
|
153
|
+
const h = {};
|
|
154
|
+
for (const [k, v] of Object.entries(health || {})) if (HEALTH.has(v)) h[k] = v;
|
|
155
|
+
const state = {
|
|
156
|
+
org: { id: org?.id ?? null, name: org?.name ?? null },
|
|
157
|
+
approvals: { count: list.length, items: list },
|
|
158
|
+
health: h,
|
|
159
|
+
loops: (loops || []).map((l) => ({ id: l.id, title: String(l.title || ''), turn: Number(l.turn) || 0, max: Number(l.max) || 0, status: l.status || 'running' })),
|
|
160
|
+
fleet: Array.isArray(fleet) ? buildFleet(fleet) : (fleet && typeof fleet === 'object' ? { total: fleet.total | 0, online: fleet.online | 0, offline: fleet.offline | 0, warn: fleet.warn | 0, hosts: (fleet.hosts || []).slice(0, FLEET_MAX) } : buildFleet(null)),
|
|
161
|
+
evidence: { chain: CHAIN.has(evidence?.chain) ? evidence.chain : 'unknown', checked_at: evidence?.checked_at ?? null },
|
|
162
|
+
sessions: { active: Math.max(0, Number(sessions?.active) || 0) }
|
|
163
|
+
};
|
|
164
|
+
if (event) state.event = { kind: event.kind, text: event.text, level: event.level || 'info' };
|
|
165
|
+
return state;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What the composer is filled with when an extension picks an option; the user still presses Enter. null = nothing to preselect. */
|
|
169
|
+
export function pickDraft(item, n, option) {
|
|
170
|
+
if (!item) return null;
|
|
171
|
+
const opt = String(option ?? '').trim();
|
|
172
|
+
if (!opt) return null;
|
|
173
|
+
const keys = (item.payload?.options || []).map((o) => String(o.key));
|
|
174
|
+
if (keys.length) return keys.includes(opt) ? `/answer ${n} ${opt}` : null;
|
|
175
|
+
if (/^(y|yes|approve)$/i.test(opt)) return `/approve ${n}`;
|
|
176
|
+
if (/^(n|no|deny)$/i.test(opt)) return `/deny ${n} `;
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---- the host -------------------------------------------------------------------------------------------------------
|
|
181
|
+
/**
|
|
182
|
+
* handlers: { openApproval(n), preselect(n, option), changed() } are the window's own actions. getSettings() feeds create().
|
|
183
|
+
* Loaded extensions all receive the same deduped, 1/s-throttled snapshot.
|
|
184
|
+
*/
|
|
185
|
+
export function createExtensionHost({ importer = defaultImporter, env = process.env, execPath, npmRoot, exists, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout,
|
|
186
|
+
handlers = {}, log = () => {}, getSettings = () => ({}) } = {}) {
|
|
187
|
+
const loaded = new Map(); // name -> { ext, inst, where }
|
|
188
|
+
const starting = new Map();
|
|
189
|
+
let lastSent = 0; let lastKey = null; let pending = null; let timer = null;
|
|
190
|
+
let event = null; const prevLoops = new Map(); let prevCount = null;
|
|
191
|
+
|
|
192
|
+
const sendAll = (state) => {
|
|
193
|
+
const key = JSON.stringify(state);
|
|
194
|
+
if (key === lastKey) return;
|
|
195
|
+
lastKey = key; lastSent = now();
|
|
196
|
+
for (const [name, l] of loaded) { try { l.inst?.update?.(state); } catch (e) { log(`extension ${name} update failed: ${e?.message || e}`); } }
|
|
197
|
+
};
|
|
198
|
+
const flush = () => { timer = null; if (pending && loaded.size) { const s = pending; pending = null; sendAll(s); } };
|
|
199
|
+
|
|
200
|
+
const emitFor = (name) => (msg) => {
|
|
201
|
+
if (!loaded.has(name) || !msg || typeof msg !== 'object') return false; // ignored unless this extension is loaded
|
|
202
|
+
try {
|
|
203
|
+
if (msg.type === 'open_approval' && Number.isInteger(msg.n) && msg.n >= 1) { handlers.openApproval?.(msg.n); return true; }
|
|
204
|
+
if (msg.type === 'pick' && Number.isInteger(msg.n) && msg.n >= 1) { handlers.preselect?.(msg.n, msg.option); return true; }
|
|
205
|
+
if (msg.type === 'alert_ack') { event = null; handlers.changed?.(); return true; }
|
|
206
|
+
} catch (e) { log(`extension ${name} emit failed: ${e?.message || e}`); }
|
|
207
|
+
return false;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const host = {
|
|
211
|
+
armed: () => loaded.size > 0,
|
|
212
|
+
isLoaded: (name) => loaded.has(name),
|
|
213
|
+
loadedNames: () => [...loaded.keys()],
|
|
214
|
+
where: (name) => loaded.get(name)?.where || null,
|
|
215
|
+
/** emit as one extension (tests and the shim); extensions get their own bound emit in create(). */
|
|
216
|
+
emit: (msg, name) => (name ? emitFor(name)(msg) : [...loaded.keys()].some((n) => emitFor(n)(msg))),
|
|
217
|
+
async load(name) {
|
|
218
|
+
if (loaded.has(name)) return { ok: true, text: `${name} is already loaded.` };
|
|
219
|
+
if (starting.has(name)) return starting.get(name);
|
|
220
|
+
const p = (async () => {
|
|
221
|
+
try {
|
|
222
|
+
const r = await resolveExtension(name, { env, importer, execPath, npmRoot, exists });
|
|
223
|
+
if (!r.ok && r.notFound) return { ok: false, notFound: true, text: `${name} ${notInstalled(name)} (looked in: ${r.tried.join('; ')})`, tried: r.tried };
|
|
224
|
+
if (!r.ok) return { ok: false, text: `${name} could not load: ${r.error}` };
|
|
225
|
+
const ext = normalizeExtension(r.mod, name);
|
|
226
|
+
if (!ext) return { ok: false, text: `${name} could not load: the package has no create().` };
|
|
227
|
+
let inst;
|
|
228
|
+
try { inst = ext.create({ emit: emitFor(name), log, settings: getSettings() }); } catch (e) { return { ok: false, text: `${name} could not start: ${e?.message || e}` }; }
|
|
229
|
+
loaded.set(name, { ext, inst, where: r.where });
|
|
230
|
+
lastKey = null; lastSent = 0; pending = null; event = null; prevLoops.clear(); prevCount = null;
|
|
231
|
+
handlers.changed?.();
|
|
232
|
+
return { ok: true, text: `${ext.name} is on (found: ${r.where}).`, ext };
|
|
233
|
+
} finally { starting.delete(name); }
|
|
234
|
+
})();
|
|
235
|
+
starting.set(name, p);
|
|
236
|
+
return p;
|
|
237
|
+
},
|
|
238
|
+
unload(name) {
|
|
239
|
+
const l = loaded.get(name);
|
|
240
|
+
if (!l) return { ok: true, text: `${name} was not loaded.` };
|
|
241
|
+
loaded.delete(name);
|
|
242
|
+
if (!loaded.size) { pending = null; lastKey = null; if (timer) { clearTimer(timer); timer = null; } }
|
|
243
|
+
try { l.inst?.close?.(); } catch (e) { log(`extension ${name} close failed: ${e?.message || e}`); }
|
|
244
|
+
handlers.changed?.();
|
|
245
|
+
return { ok: true, text: `${name} is off.` };
|
|
246
|
+
},
|
|
247
|
+
unloadAll() { for (const n of [...loaded.keys()]) host.unload(n); },
|
|
248
|
+
/** Slash commands of loaded extensions only. -> [{ name, help, ext }] */
|
|
249
|
+
commands: () => [...loaded.values()].flatMap((l) => l.ext.commands.map((c) => ({ name: c.name, help: c.help || '', ext: l.ext.name }))),
|
|
250
|
+
async runCommand(cmd, args, ctx = {}) {
|
|
251
|
+
for (const l of loaded.values()) {
|
|
252
|
+
const c = l.ext.commands.find((x) => x.name === cmd);
|
|
253
|
+
if (c) { try { return { handled: true, result: await c.run(args, { ...ctx, extension: l.ext.name, loaded: host.loadedNames() }) }; } catch (e) { return { handled: true, error: e?.message || String(e) }; } }
|
|
254
|
+
}
|
|
255
|
+
return { handled: false };
|
|
256
|
+
},
|
|
257
|
+
/** Push the current snapshot inputs: derives an event from changes, throttles to one update per second. */
|
|
258
|
+
push(input) {
|
|
259
|
+
if (!loaded.size) return;
|
|
260
|
+
const items = input?.items || [];
|
|
261
|
+
for (const l of input?.loops || []) {
|
|
262
|
+
const was = prevLoops.get(l.id);
|
|
263
|
+
if (was && was !== l.status && (l.status === 'done' || l.status === 'failed')) event = { kind: 'loop', text: `${l.title || l.id} ${l.status}`, level: l.status === 'failed' ? 'error' : 'ok' };
|
|
264
|
+
prevLoops.set(l.id, l.status);
|
|
265
|
+
}
|
|
266
|
+
if (prevCount != null && items.length > prevCount) event = { kind: 'approval', text: `${items.length} waiting`, level: 'warn' };
|
|
267
|
+
prevCount = items.length;
|
|
268
|
+
const state = buildSnapshot({ ...input, event });
|
|
269
|
+
const wait = THROTTLE_MS - (now() - lastSent);
|
|
270
|
+
if (wait <= 0 && !timer) { pending = null; sendAll(state); return; }
|
|
271
|
+
pending = state;
|
|
272
|
+
if (!timer) timer = setTimer(flush, Math.max(wait, 0));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
return host;
|
|
276
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL-INBOX-BAR (owner 2026-10-26): the owner's Quox inbox in the chat. Pure helpers: the status-area bar text,
|
|
3
|
+
* the arrival line, the /inbox table and the one-item view. No I/O here; app.js owns the calls.
|
|
4
|
+
*/
|
|
5
|
+
const clip = (s, n) => { const t = String(s ?? '').replace(/\s+/g, ' ').trim(); return t.length > n ? `${t.slice(0, Math.max(0, n - 1))}…` : t; };
|
|
6
|
+
const pad = (s, n) => { const t = clip(s, n); return t + ' '.repeat(Math.max(0, n - [...t].length)); };
|
|
7
|
+
const ms = (v) => { const t = Date.parse(String(v ?? '').includes('T') || !v ? v : `${String(v).replace(' ', 'T')}Z`); return Number.isFinite(t) ? t : 0; };
|
|
8
|
+
const CLOSED = new Set(['resolved', 'cancelled', 'expired', 'deleted', 'archived']);
|
|
9
|
+
|
|
10
|
+
/** Newest first. */
|
|
11
|
+
export const newestFirst = (items) => [...(Array.isArray(items) ? items : [])].filter((i) => i && i.id).sort((a, b) => ms(b.created_at ?? b.createdAt) - ms(a.created_at ?? a.createdAt));
|
|
12
|
+
/** Unread = the server's per-user is_read flag is not set, and the item is not closed or archived. */
|
|
13
|
+
export const isUnread = (it) => !it.is_read && !it.archived_at && !CLOSED.has(String(it.status || '').toLowerCase());
|
|
14
|
+
export const unreadItems = (items) => newestFirst(items).filter(isUnread);
|
|
15
|
+
|
|
16
|
+
/** The status-area line, or '' when nothing is unread: `📨 2 new · <newest title>`. */
|
|
17
|
+
export function inboxBarText(items, cols = 80) {
|
|
18
|
+
const un = unreadItems(items);
|
|
19
|
+
if (!un.length) return '';
|
|
20
|
+
const head = `📨 ${un.length} new · `;
|
|
21
|
+
return head + clip(un[0].title || un[0].kind || 'untitled', Math.max(8, cols - 4 - [...head].length));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Unread items not seen on an earlier poll. `seen` is a Set of ids; the first poll seeds it and announces nothing. */
|
|
25
|
+
export function arrivals(items, seen) {
|
|
26
|
+
const un = unreadItems(items);
|
|
27
|
+
const fresh = seen ? un.filter((i) => !seen.has(i.id)) : [];
|
|
28
|
+
return { fresh, ids: new Set((Array.isArray(items) ? items : []).map((i) => i?.id).filter(Boolean)) };
|
|
29
|
+
}
|
|
30
|
+
export const arrivalLine = (it) => `📨 ${clip(it.title || it.kind || 'untitled', 70)} · /inbox 1`;
|
|
31
|
+
|
|
32
|
+
export const fromOf = (it) => String(it.created_by_name || it.payload?.source || it.payload?.approval_context?.source_label || it.source || it.inbox_type || it.kind || 'system');
|
|
33
|
+
export function ageOf(v, now = Date.now()) {
|
|
34
|
+
const t = ms(v); if (!t) return '?';
|
|
35
|
+
const s = Math.max(0, Math.round((now - t) / 1000));
|
|
36
|
+
if (s < 90) return 'now';
|
|
37
|
+
if (s < 5400) return `${Math.round(s / 60)}m`;
|
|
38
|
+
if (s < 129600) return `${Math.round(s / 3600)}h`;
|
|
39
|
+
return `${Math.round(s / 86400)}d`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The latest 10 as a compact table; unread rows carry a dot. -> [{ text, tone }] */
|
|
43
|
+
export function inboxTable(items, now = Date.now(), cols = 100, max = 10) {
|
|
44
|
+
const rows = newestFirst(items).slice(0, max);
|
|
45
|
+
if (!rows.length) return [{ text: 'Your inbox is empty.', tone: 'dim' }];
|
|
46
|
+
const tw = Math.max(16, Math.min(60, cols - 40));
|
|
47
|
+
const out = [{ text: ` ${pad('#', 3)}${pad('when', 6)}${pad('from', 14)}${pad('title', tw)} state`, tone: 'dim' }];
|
|
48
|
+
rows.forEach((it, i) => {
|
|
49
|
+
const un = isUnread(it);
|
|
50
|
+
out.push({ text: `${un ? '●' : ' '} ${pad(i + 1, 3)}${pad(ageOf(it.created_at ?? it.createdAt, now), 6)}${pad(fromOf(it), 14)}${pad(it.title || it.kind || 'untitled', tw)} ${un ? 'unread' : (it.status || 'read')}`, tone: un ? 'accent' : 'dim' });
|
|
51
|
+
});
|
|
52
|
+
const un = unreadItems(items).length;
|
|
53
|
+
out.push({ text: `${un} unread · /inbox N open one · /inbox read N|all · /inbox open N (approvals)`, tone: 'dim' });
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Arg grammar: '' | N | read N|all | open N. -> { kind, n?|all? } | { kind:'usage' } */
|
|
58
|
+
export function parseInbox(arg) {
|
|
59
|
+
const a = String(arg || '').trim().toLowerCase();
|
|
60
|
+
if (!a || a === 'list' || a === 'latest') return { kind: 'list' };
|
|
61
|
+
let m = /^(?:show\s+)?#?(\d+)$/.exec(a); if (m) return { kind: 'show', n: Number(m[1]) };
|
|
62
|
+
m = /^open\s+#?(\d+)$/.exec(a); if (m) return { kind: 'open', n: Number(m[1]) };
|
|
63
|
+
m = /^read\s+(all|#?\d+)$/.exec(a); if (m) return m[1] === 'all' ? { kind: 'read', all: true } : { kind: 'read', n: Number(m[1].replace('#', '')) };
|
|
64
|
+
return { kind: 'usage' };
|
|
65
|
+
}
|
|
66
|
+
export const INBOX_USAGE = 'Usage: /inbox | /inbox N | /inbox read N|all | /inbox open N';
|
|
67
|
+
|
|
68
|
+
/** One item in full. -> [{ text, tone }] */
|
|
69
|
+
export function inboxDetail(it, now = Date.now()) {
|
|
70
|
+
const p = it.payload || {};
|
|
71
|
+
const body = it.body || it.message || p.message || p.body || p.summary || p.text || '';
|
|
72
|
+
const out = [{ text: `${it.title || it.kind || 'untitled'}`, tone: 'accent' },
|
|
73
|
+
{ text: `from ${fromOf(it)} · ${ageOf(it.created_at ?? it.createdAt, now)} ago · ${it.kind || 'item'} · ${it.priority || 'normal'} · ${it.status || '?'}`, tone: 'dim' }];
|
|
74
|
+
if (body) for (const l of String(body).split('\n')) out.push({ text: l, tone: 'text' });
|
|
75
|
+
if (it.dashboard_url) out.push({ text: it.dashboard_url, tone: 'dim' });
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** Approval-type items route to /approvals (the existing view). */
|
|
79
|
+
export const isApprovalItem = (it) => String(it.kind || '').toLowerCase() === 'approval' || Boolean(it.payload?.approval_context) || it.payload?.type === 'decision';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAZY-AI-CLI (owner 2026-10-19): "Quox can depend heavily on your choice or blend of Claude and OpenAI models.
|
|
3
|
+
* We advise installing both Claude Code and Codex." Draw people in step by step: when something needs a vendor CLI
|
|
4
|
+
* that is missing, offer to install it THEN, as a y/N.
|
|
5
|
+
*
|
|
6
|
+
* ONE flow behind `quox provider install` and the TUI offer cards (first-run suggestion + just-in-time), so they
|
|
7
|
+
* cannot drift. The server downloads the vendor's own npm package (never a binary Quox ships), as the Quox service
|
|
8
|
+
* user, into a volume that survives updates, audited start and finish (services/collector/routes/providerCli.js).
|
|
9
|
+
*/
|
|
10
|
+
import { detectCallerSource } from '../core/callerSource.js';
|
|
11
|
+
|
|
12
|
+
export const INSTALL_TOOLS = {
|
|
13
|
+
'claude-code': { id: 'claude-code', name: 'Claude Code', vendor: 'Anthropic', bin: 'claude', after: 'Claude sign-in is available now: sign in from the dashboard Terminal, then pick Claude in /model.' },
|
|
14
|
+
codex: { id: 'codex', name: 'Codex', vendor: 'OpenAI', bin: 'codex', after: 'Codex is ready: sign in to ChatGPT from the dashboard Terminal (Codex), then pick it in /model.' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const SUGGEST_LINE = 'Quox works best with both Claude and OpenAI models. We suggest installing Claude Code and Codex.';
|
|
18
|
+
|
|
19
|
+
const ALIASES = { claude: 'claude-code', 'claude-code': 'claude-code', claudecode: 'claude-code', codex: 'codex', 'openai-codex': 'codex' };
|
|
20
|
+
export const toolFor = (s) => ALIASES[String(s || '').trim().toLowerCase()] || null;
|
|
21
|
+
|
|
22
|
+
const sizeText = (p) => (p?.downloadMb ? `about ${p.downloadMb} MB` : null);
|
|
23
|
+
|
|
24
|
+
export function planLine(p) {
|
|
25
|
+
const t = INSTALL_TOOLS[p?.id] || { name: p?.name || p?.id };
|
|
26
|
+
const size = sizeText(p);
|
|
27
|
+
return `${t.name} ${p?.version || ''} (${t.vendor ? `${t.vendor}'s ` : ''}${p?.package || ''}) from ${p?.source || 'registry.npmjs.org'}${size ? `, ${size} download` : ''}${p?.installedMb ? `, ${p.installedMb} MB on disk` : ''}. It installs on this server as the Quox service user and stays across updates.`.replace(/\s+/g, ' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function offerQuestion(tool, plan) {
|
|
31
|
+
const t = INSTALL_TOOLS[tool];
|
|
32
|
+
const size = sizeText(plan);
|
|
33
|
+
return `${t.name} isn't installed on this server. Install it now? (${size ? `${size} from npm` : 'from npm'})`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The CLIs the server says are missing, in a stable order. */
|
|
37
|
+
export function missingTools(status) {
|
|
38
|
+
if (!status?.ok || !Array.isArray(status.tools)) return [];
|
|
39
|
+
return Object.keys(INSTALL_TOOLS).filter((id) => status.tools.some((t) => t.id === id && t.installed === false));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** An agent (a session, an attached agent, a Claude process) never self-approves an install: it files a card. */
|
|
43
|
+
export function isAgentCaller(env = process.env, detect = detectCallerSource) {
|
|
44
|
+
if (env.QUOX_AGENT_ID || env.QUOX_SESSION_ID || env.QUOX_SESSION) return true;
|
|
45
|
+
let src = null;
|
|
46
|
+
try { src = detect(env); } catch { src = null; }
|
|
47
|
+
if (!src) return false;
|
|
48
|
+
if (['attached_agent', 'held_slot', 'QUOX_STREAM'].includes(src.via)) return true;
|
|
49
|
+
return src.via === 'process' && /^claude\b/i.test(String(src.label || ''));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const POLL_MS = 3000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Start the server install and wait for it. Never throws.
|
|
56
|
+
* @returns {Promise<{ok:boolean, version?:string|null, lines:string[]}>}
|
|
57
|
+
*/
|
|
58
|
+
export async function runInstall(api, tool, { approvalId = null, onProgress = () => {}, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now, timeoutMs = 15 * 60 * 1000 } = {}) {
|
|
59
|
+
const t = INSTALL_TOOLS[tool];
|
|
60
|
+
if (!t) return { ok: false, lines: [`No installable CLI called "${tool}". Use one of: ${Object.keys(INSTALL_TOOLS).join(', ')}`] };
|
|
61
|
+
const start = await api.providerCliInstall(tool, { approvalId });
|
|
62
|
+
if (!start?.ok || !start.job_id) {
|
|
63
|
+
if (start?.status === 404 || start?.status === 405) return { ok: false, lines: [`This Quox server is too old to install ${t.name} for you. Update it first: quox server update`] };
|
|
64
|
+
if (start?.status === 403) return { ok: false, lines: [`✖ ${start.error || 'Only an instance admin can install software on this server.'} Nothing was installed.`] };
|
|
65
|
+
return { ok: false, lines: [`✖ ${t.name} was not installed: ${start?.error || 'the server did not start the install'}`] };
|
|
66
|
+
}
|
|
67
|
+
const began = now();
|
|
68
|
+
let last = began;
|
|
69
|
+
for (;;) {
|
|
70
|
+
await sleep(POLL_MS);
|
|
71
|
+
const j = await api.providerCliJob(start.job_id);
|
|
72
|
+
if (j?.state === 'done' && j.ok) {
|
|
73
|
+
return { ok: true, version: j.version || null, lines: [`✔ ${t.name} ${j.version || ''} is installed on this server.`.replace(/\s+/g, ' ').replace(' .', '.'), t.after] };
|
|
74
|
+
}
|
|
75
|
+
if (j?.state === 'failed') return { ok: false, lines: [`✖ ${t.name} was not installed: ${j.error || 'the install failed'}`] };
|
|
76
|
+
if (now() - began >= timeoutMs) return { ok: false, lines: [`◐ ${t.name} is still running its install on the server. Check again shortly: quox provider list`] };
|
|
77
|
+
if (now() - last >= 15000) { last = now(); onProgress(`◐ installing ${t.name} (${Math.round((now() - began) / 1000)}s)`); }
|
|
78
|
+
}
|
|
79
|
+
}
|
package/lib/quoxon/keys.js
CHANGED
|
@@ -68,7 +68,7 @@ const CSI_KEYS = {
|
|
|
68
68
|
const CSI_LIST = Object.keys(CSI_KEYS).sort((a, b) => b.length - a.length);
|
|
69
69
|
|
|
70
70
|
const CTRL = {
|
|
71
|
-
'\x01': 'home', '\x05': 'end', '\x03': 'ctrl-c', '\x04': 'ctrl-d', '\x07': 'ctrl-g',
|
|
71
|
+
'\x01': 'home', '\x05': 'end', '\x03': 'ctrl-c', '\x04': 'ctrl-d', '\x07': 'ctrl-g', '\x12': 'ctrl-r',
|
|
72
72
|
'\x0b': 'kill-line-forward', '\x0f': 'ctrl-o', '\x13': 'ctrl-s', '\x15': 'kill-line', '\x17': 'delete-word',
|
|
73
73
|
'\x1f': 'undo', '\x19': 'redo', '\x0a': 'newline', '\x7f': 'backspace', '\x08': 'backspace',
|
|
74
74
|
'\r': 'enter', '\t': 'tab', '\x02': 'left', '\x06': 'right'
|
|
@@ -118,7 +118,7 @@ export function parseChunk(chunk, state = initialKeyState(), draft = '') {
|
|
|
118
118
|
const run = /^[^\x00-\x1f\x7f]+/.exec(s);
|
|
119
119
|
if (run) { events.push({ type: 'text', text: run[0] }); s = s.slice(run[0].length); continue; }
|
|
120
120
|
const c = s[0];
|
|
121
|
-
if (CTRL[c]) { events.push({ type: 'key', name: CTRL[c] }); }
|
|
121
|
+
if (CTRL[c]) { events.push(c === '\x01' ? { type: 'key', name: CTRL[c], via: 'ctrl-a' } : { type: 'key', name: CTRL[c] }); } // via: a double Ctrl+A selects all (app.js)
|
|
122
122
|
s = s.slice(1);
|
|
123
123
|
}
|
|
124
124
|
return { events: coalesceBurst(events, draft), state: st };
|