quoxcli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/admin.js +7 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +2 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agentAttach.js +15 -0
- package/lib/commands/approvals.js +193 -3
- package/lib/commands/ask.js +1 -1
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +15 -0
- package/lib/commands/claude.js +4 -1
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/evidence.js +2 -1
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +78 -5
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +30 -3
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +2 -3
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +29 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/run.js +2 -1
- package/lib/commands/server.js +37 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/skills.js +4 -1
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +27 -1
- package/lib/core/commandRegistrars.js +8 -0
- package/lib/core/config.js +28 -2
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/governedSession.js +135 -5
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/offload.js +37 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +1 -1
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/EXTENSIONS.md +142 -19
- package/lib/quoxon/api.js +185 -34
- package/lib/quoxon/app.js +1032 -152
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +56 -2
- package/lib/quoxon/approvalSentence.js +8 -4
- package/lib/quoxon/approvalsView.js +69 -11
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +7 -0
- package/lib/quoxon/commands.js +40 -10
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +37 -7
- package/lib/quoxon/dragonMoments.js +78 -0
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +43 -7
- package/lib/quoxon/extensions.js +398 -38
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/loopCard.js +5 -1
- package/lib/quoxon/loopCards.js +17 -8
- package/lib/quoxon/loopStream.js +20 -9
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +20 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +22 -0
- package/lib/quoxon/print.js +2 -0
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/render.js +63 -14
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliLeaves.json +63 -1
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +22 -1
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/restart.js +7 -4
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +1 -1
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +36 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +3 -2
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
package/lib/core/quoxglyph.js
CHANGED
|
@@ -30,6 +30,7 @@ export const ICONS = {
|
|
|
30
30
|
quoxseo: '📈 QUOXSEO', reach: '💥 REACH', vulcan: '🌋 VULCAN',
|
|
31
31
|
quoxvoice: '📞 QUOXVOICE', quoxsecurity: '🛡️ QUOXSECURITY', quoxpert: '🎓 QUOXPERT',
|
|
32
32
|
quoxslots: '📍 QUOXSLOTS',
|
|
33
|
+
gripe: '📣 GRIPE',
|
|
33
34
|
quox: 'Ⓠ QUOX'
|
|
34
35
|
};
|
|
35
36
|
export const BUDGETS = { VERB_MAX: 48, RESULT_MAX: 40, LINE_MAX: 120, TREE_MAX: 6, TABLE_MAX: 6 };
|
|
@@ -74,7 +75,7 @@ export function line(surface, verb, opts = {}) {
|
|
|
74
75
|
const col = on();
|
|
75
76
|
const name = brandName(iconFor(surface), col);
|
|
76
77
|
const sep = col ? `${DIM}│${R}` : '│';
|
|
77
|
-
let tail = opts.result ? trunc(opts.result, BUDGETS.RESULT_MAX) : '';
|
|
78
|
+
let tail = opts.result ? trunc(opts.result, opts.max || BUDGETS.RESULT_MAX) : '';
|
|
78
79
|
if (opts.ms !== undefined) {
|
|
79
80
|
const d = col ? `${DIM}${opts.ms}ms${R}` : `${opts.ms}ms`;
|
|
80
81
|
tail = tail ? `${tail} ${col ? DIM + '·' + R : '·'} ${d}` : d;
|
|
@@ -85,6 +86,26 @@ export function line(surface, verb, opts = {}) {
|
|
|
85
86
|
return tail ? `${name} ${sep} ${v} ${sep} ${tail}` : `${name} ${sep} ${v}`;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
const GRIPE_TITLE_MAX = 50;
|
|
90
|
+
const gripeTitle = (t) => trunc(String(t || '').split('\n')[0].trim(), GRIPE_TITLE_MAX);
|
|
91
|
+
|
|
92
|
+
/** GRIPE-1460: `📣 GRIPE │ filed #N owner/repo │ "title" ✔` for an issue URL (the URL itself goes on the next line). */
|
|
93
|
+
export function gripeFiledLine(url, title) {
|
|
94
|
+
const m = /github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)/.exec(String(url || ''));
|
|
95
|
+
const verb = m ? `filed #${m[2]} ${m[1]}` : 'filed issue';
|
|
96
|
+
return line('gripe', verb, { result: `"${gripeTitle(title)}"`, status: 'ok', max: GRIPE_TITLE_MAX + 2 });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** GRIPE-1460: a gripe saved earlier and filed on retry: `📣 GRIPE │ queued gripe filed │ <url> ✔`. */
|
|
100
|
+
export function gripeQueuedFiledLine(url) {
|
|
101
|
+
return line('gripe', 'queued gripe filed', { result: String(url), status: 'ok', max: 200 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** GRIPE-1460: `📣 GRIPE │ file "title" │ ✖ <exact error>`. */
|
|
105
|
+
export function gripeFailedLine(title, error) {
|
|
106
|
+
return line('gripe', `file "${trunc(gripeTitle(title), 30)}"`, { result: `${STATUS.fail.glyph} ${error}`, max: 400 });
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
/** Gradient progress meter: ▰▰▰▱ done/total (or pct when total omitted). */
|
|
89
110
|
export function meter(done, total, width = 8) {
|
|
90
111
|
const col = on();
|
package/lib/core/router.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* output is tested without a router.
|
|
9
9
|
*/
|
|
10
10
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
11
|
+
import { QuoxUserError } from './errors.js';
|
|
11
12
|
|
|
12
13
|
/** verb -> the tool it runs. All read-only (aoclClassification layer 1, autonomy FULL). */
|
|
13
14
|
export const ROUTER_TOOLS = Object.freeze({
|
|
@@ -21,6 +22,18 @@ export const ROUTER_TOOLS = Object.freeze({
|
|
|
21
22
|
inventory: 'mikrotik_discovery_inventory',
|
|
22
23
|
});
|
|
23
24
|
|
|
25
|
+
/** The tool route refused because this identity lacks the router grant: say that, keep the plumbing in detail. */
|
|
26
|
+
function notPermitted(tool, raw, path) {
|
|
27
|
+
const err = new QuoxUserError('This identity is not allowed to read the router: the router tools are not granted to it.', {
|
|
28
|
+
next: 'run it from an identity that has the router tools (`quox config profile list`), or ask an org owner or admin to grant them.',
|
|
29
|
+
code: 'not_permitted',
|
|
30
|
+
});
|
|
31
|
+
err.debug = { tool, route: `POST ${path}`, server_said: raw }; // --json and --verbose only; never the headline
|
|
32
|
+
err.toolError = raw;
|
|
33
|
+
err.tool = tool;
|
|
34
|
+
return err;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
export function makeRouterExec({ client, orgId }) {
|
|
25
38
|
return async function exec(tool, input = {}) {
|
|
26
39
|
const ep = resolveEndpoint('v1.tools.execute');
|
|
@@ -28,6 +41,7 @@ export function makeRouterExec({ client, orgId }) {
|
|
|
28
41
|
if (orgId) body.org_id = orgId;
|
|
29
42
|
const data = await client.post(ep.path, { service: ep.service, body });
|
|
30
43
|
if (data && data.success === false) {
|
|
44
|
+
if (/not granted to agent/i.test(String(data.error || ''))) throw notPermitted(tool, data.error, ep.path);
|
|
31
45
|
const detail = data.detail ? ` (${data.detail})` : '';
|
|
32
46
|
const err = new Error(`${tool} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}${detail}`);
|
|
33
47
|
err.toolError = data.error || 'unknown error';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DS-010 / quox#1335: the CLI half of the human step-up PIN.
|
|
3
|
+
*
|
|
4
|
+
* The SERVER decides what needs a person and answers 403 step_up_required. This helper only reacts:
|
|
5
|
+
* on a terminal it asks for the PIN with hidden input, trades it for a short-lived step-up token
|
|
6
|
+
* (POST /api/auth/me/step-up, the same endpoint the Quox terminal uses) and retries the call ONCE with
|
|
7
|
+
* the token. Rules, all deliberate:
|
|
8
|
+
* - the PIN is read from a hidden prompt only. NEVER from argv, an env var or a file: an agent that can
|
|
9
|
+
* set an env var or a flag could supply it, and then the PIN proves nothing about a person
|
|
10
|
+
* - the PIN is never logged, echoed or put in an error message; the token is never stored
|
|
11
|
+
* - no terminal: refuse with a plain message saying a person must run it interactively
|
|
12
|
+
*/
|
|
13
|
+
import { QuoxUserError } from './errors.js';
|
|
14
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
15
|
+
|
|
16
|
+
export const PIN_RE = /^\d{6}$/;
|
|
17
|
+
export const MAX_PIN_TRIES = 3;
|
|
18
|
+
export const STEP_UP_HEADER = 'x-step-up-token';
|
|
19
|
+
const NO_PIN_NEXT = 'set one in the dashboard: Settings > Account > Approval PIN, then run this again in a terminal.';
|
|
20
|
+
|
|
21
|
+
export function isStepUpRequired(err) {
|
|
22
|
+
return Boolean(err && err.status === 403 && err.body && err.body.error === 'step_up_required');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Real IO: hidden prompt via inquirer; a terminal means a TTY on both stdin and stdout. */
|
|
26
|
+
export function defaultPinIO() {
|
|
27
|
+
return {
|
|
28
|
+
isTTY: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
29
|
+
promptPin: async (message) => {
|
|
30
|
+
const { default: inquirer } = await import('inquirer');
|
|
31
|
+
const a = await inquirer.prompt([{ type: 'password', name: 'pin', message, mask: '*' }]);
|
|
32
|
+
return a.pin;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nonTtyError(what) {
|
|
38
|
+
return new QuoxUserError(`${what} needs your step-up PIN, and there is no terminal to ask for it on.`, {
|
|
39
|
+
next: 'run this command yourself in an interactive terminal; it will ask for the PIN with hidden input. The PIN cannot be passed as a flag or an environment variable.',
|
|
40
|
+
exitCode: 4, code: 'step_up_required',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run `run(extraHeaders)`; if the server says step-up is required, get a token from the person and run it once more.
|
|
46
|
+
* @param {(headers:object)=>Promise<any>} run performs the guarded request, merging `headers` into it
|
|
47
|
+
* @param {{client:any, orgId?:string|null, scope?:string, io?:object, what?:string}} opts
|
|
48
|
+
*/
|
|
49
|
+
export async function withStepUp(run, { client, orgId = null, scope = 'approve', io = defaultPinIO(), what = 'This approval' } = {}) {
|
|
50
|
+
try {
|
|
51
|
+
return await run({});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (!isStepUpRequired(err)) throw err;
|
|
54
|
+
if (err.body?.reason === 'non_human') {
|
|
55
|
+
throw new QuoxUserError(`${what} needs a person. This login is an agent or service identity, which can never approve it.`, {
|
|
56
|
+
next: 'a person must approve it from their own login (the dashboard, or `quox` in their terminal).', exitCode: 4, code: 'step_up_human_only',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (err.body?.pin_set === false) {
|
|
60
|
+
throw new QuoxUserError(`${what} needs your step-up PIN, and you have not set one yet.`, { next: NO_PIN_NEXT, exitCode: 4, code: 'no_pin_set' });
|
|
61
|
+
}
|
|
62
|
+
if (!io.isTTY()) throw nonTtyError(what);
|
|
63
|
+
const token = await collectToken({ client, orgId, scope, io, what });
|
|
64
|
+
return run({ [STEP_UP_HEADER]: token });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function collectToken({ client, orgId, scope, io, what }) {
|
|
69
|
+
const ep = resolveEndpoint('stepup.verify');
|
|
70
|
+
for (let attempt = 1; attempt <= MAX_PIN_TRIES; attempt += 1) {
|
|
71
|
+
let pin = await io.promptPin(`${what} needs your step-up PIN (6 digits, hidden):`);
|
|
72
|
+
if (!PIN_RE.test(String(pin ?? ''))) {
|
|
73
|
+
pin = null;
|
|
74
|
+
if (attempt < MAX_PIN_TRIES) continue;
|
|
75
|
+
throw new QuoxUserError('A step-up PIN is exactly 6 digits. Nothing was approved.', { exitCode: 4, code: 'bad_pin_format' });
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const body = { pin, scopes: [scope] };
|
|
79
|
+
if (orgId) body.org_id = orgId;
|
|
80
|
+
const r = await client.post(ep.path, { service: ep.service, body });
|
|
81
|
+
pin = null;
|
|
82
|
+
if (r && r.step_up_token) return r.step_up_token;
|
|
83
|
+
throw new QuoxUserError('The server did not issue a step-up token. Nothing was approved.', { exitCode: 4, code: 'step_up_failed' });
|
|
84
|
+
} catch (e) {
|
|
85
|
+
pin = null;
|
|
86
|
+
if (e instanceof QuoxUserError) throw e;
|
|
87
|
+
const code = e?.body?.error || e?.code;
|
|
88
|
+
if (e?.status === 409 || code === 'no_pin_set') throw new QuoxUserError('You have not set a step-up PIN yet.', { next: NO_PIN_NEXT, exitCode: 4, code: 'no_pin_set' });
|
|
89
|
+
if (e?.status === 423 || code === 'pin_locked') throw new QuoxUserError('PIN entry is locked after too many wrong tries.', { next: 'wait for the lock to pass, or approve from the dashboard.', exitCode: 4, code: 'pin_locked' });
|
|
90
|
+
if (e?.status === 403 && code === 'step_up_human_only') throw new QuoxUserError(`${what} needs a person; this login is an agent identity.`, { next: 'approve from a human login.', exitCode: 4, code: 'step_up_human_only' });
|
|
91
|
+
if (e?.status === 401 || code === 'wrong_pin') {
|
|
92
|
+
if (attempt >= MAX_PIN_TRIES) throw new QuoxUserError('Wrong PIN. Nothing was approved.', { exitCode: 4, code: 'wrong_pin' });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new QuoxUserError('Wrong PIN. Nothing was approved.', { exitCode: 4, code: 'wrong_pin' });
|
|
99
|
+
}
|
package/lib/core/version.js
CHANGED
|
@@ -18,7 +18,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { dirname, resolve } from 'node:path';
|
|
20
20
|
|
|
21
|
-
export const BASE_VERSION = '0.
|
|
21
|
+
export const BASE_VERSION = '0.5.0';
|
|
22
22
|
|
|
23
23
|
// The CLI source root (…/cli). version.js lives at cli/lib/core/, so two
|
|
24
24
|
// levels up is the tree whose HEAD identifies the running build.
|
package/lib/core/walkthrough.js
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
* card at a time, using the same card and reply rules as the TUI (quoxon/approvalsView.js). Identity is the decision id.
|
|
4
4
|
* io = { readLine(prompt) -> Promise<string|null>, write(line), answer(item, key, note) -> Promise<void> }.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { isDestructive } from '../quoxon/destructive.js';
|
|
7
|
+
import { isDecision, needsPin, approvalAsWalkItem, walkCard, walkReply, walkNotYesNo, walkNoCatch, walkSummary } from '../quoxon/approvalsView.js';
|
|
7
8
|
|
|
8
9
|
export async function runWalkthrough(items, io) {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
// Questions AND policy-held approval cards (kind approval, shaped as yes/no cards); other inbox kinds are not cards.
|
|
11
|
+
const decs = (items || []).filter((it) => isDecision(it) || it?.kind === 'approval').map(approvalAsWalkItem);
|
|
12
|
+
if (!decs.length) { io.write('No question or approval is waiting for you.'); return { answered: [], skipped: [], left: [] }; }
|
|
11
13
|
const st = { answered: [], skipped: [], left: [] };
|
|
12
|
-
|
|
14
|
+
const nApp = decs.filter((d) => d.__approval).length;
|
|
15
|
+
io.write(`${decs.length - nApp ? `${decs.length - nApp} question${decs.length - nApp === 1 ? '' : 's'}` : ''}${decs.length - nApp && nApp ? ' and ' : ''}${nApp ? `${nApp} approval${nApp === 1 ? '' : 's'}` : ''} waiting for you. One at a time: a choice, then an optional note.`);
|
|
13
16
|
for (let i = 0; i < decs.length; i++) {
|
|
14
17
|
const it = decs[i]; const card = walkCard(it, i + 1, decs.length);
|
|
15
18
|
io.write(''); card.lines.forEach((l) => io.write(l));
|
|
@@ -21,7 +24,8 @@ export async function runWalkthrough(items, io) {
|
|
|
21
24
|
if (rep.notYesNo) { io.write(walkNotYesNo(it)); continue; }
|
|
22
25
|
if (rep.defer) { st.skipped.push(it.title || it.id); break; }
|
|
23
26
|
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
|
|
27
|
+
if (needsPin(it) && rep.key !== 'n') { io.write(' High risk: a yes needs your PIN, which this walkthrough cannot take. Run `quox approvals approve <id>` (it asks for the PIN) or answer in `quox` with /approvals walkthrough, or skip it here.'); continue; }
|
|
28
|
+
if (it.__approval && rep.key === 'y' && isDestructive(it)) { io.write(' This deletes something: its name must be typed, which this walkthrough does not take. Run `quox approvals approve <id>` in a terminal, or skip it.'); continue; }
|
|
25
29
|
try { await io.answer(it, rep.key, rep.note || null); }
|
|
26
30
|
catch (e) { io.write(` Not answered (${e.message}). The question stays open.`); continue; }
|
|
27
31
|
const label = it.payload.options.find((o) => o.key === rep.key).label;
|
package/lib/dragon/select.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dragon companions (Lucid, Hyperion, Seer, Sentinel, Drago, Cipher): which one, whether to show it, and where its art lives.
|
|
2
|
+
* Dragon companions (Lucid, Hyperion, Seer, Sentinel, Drago, Cipher, Vulcan, Quoxperticus): which one, whether to show it, and where its art lives.
|
|
3
3
|
* Pure. The art, animation and activity reducer stay in quoxai/dragonlab (integration/companions-v1);
|
|
4
4
|
* Quox only chooses and loads. See docs/dragons/COMPANION-UX.md for the decisions behind each rule.
|
|
5
5
|
*/
|
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
export const VENDORED_DRAGONLAB = fileURLToPath(new URL('../../vendor/dragonlab', import.meta.url));
|
|
11
11
|
|
|
12
12
|
/** Registry agent ids (and display names) to approved dragon. Others, including quox/commanderq, get none. */
|
|
13
|
-
export const AGENT_DRAGON = Object.freeze({ lucid: 'lucid', hyperion: 'hyperion', atlas: 'hyperion', seer: 'seer', sentinel: 'sentinel', warden: 'sentinel', security_lead: 'drago', drago: 'drago', cipher: 'cipher' });
|
|
13
|
+
export const AGENT_DRAGON = Object.freeze({ lucid: 'lucid', hyperion: 'hyperion', atlas: 'hyperion', seer: 'seer', sentinel: 'sentinel', warden: 'sentinel', security_lead: 'drago', drago: 'drago', cipher: 'cipher', vulcan: 'vulcan', forge: 'vulcan', quoxperticus: 'quoxperticus' });
|
|
14
14
|
export const COMPANION_MODES = ['on', 'greeting', 'off'];
|
|
15
15
|
/** One-line change: the owner decides this after his Mac check (docs/dragons/COMPANION-UX.md section 4). */
|
|
16
16
|
export const DEFAULT_COMPANION_MODE = 'on';
|
package/lib/quoxon/EXTENSIONS.md
CHANGED
|
@@ -1,23 +1,146 @@
|
|
|
1
|
-
# quoxon extensions
|
|
1
|
+
# quoxon extensions: contract v1
|
|
2
2
|
|
|
3
|
-
Optional npm packages that add a surface to quoxon (a MIDI pad deck, a status bar). Core ships none and knows none by name.
|
|
3
|
+
Optional npm packages that add a surface to quoxon (a MIDI pad deck, a status bar, a fleet board). Core ships none and knows none by name.
|
|
4
|
+
An extension can OPEN an approval or PRESELECT an answer. It can never resolve one, and it never polls Quox: data is pushed to it.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Loading
|
|
7
|
+
|
|
8
|
+
Nothing loads unless the package is installed AND listed in `~/.quox/extensions.json`
|
|
6
9
|
(`quox ext add|remove|list <pkg>`; `/ext list|on|off <name>` inside quoxon). Install with `npm i -g <pkg>`.
|
|
10
|
+
Set env `QUOX_EXT_LIST=<path>` to use another list file (default `~/.quox/extensions.json`), for `quox ext` and quoxon alike (dev and tests).
|
|
7
11
|
Resolution order: env `QUOX_EXT_<NAME>_MODULE` (name upper-cased, non-alphanumerics to `_`), a local `import(name)`,
|
|
8
|
-
then the global npm root
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
then the global npm root. `/ext list` names where each was found; a miss lists where it looked.
|
|
13
|
+
|
|
14
|
+
**contractVersion** (integer, this document is `1`). The host refuses an extension whose `contractVersion` is above what it supports,
|
|
15
|
+
with a line in `/ext list`: `needs extension contract v2, this quoxon supports v1`. An extension with no `contractVersion` is a
|
|
16
|
+
pre-contract (legacy) extension: it loads, and `emit` may use `n` without `id` (see F2). Declare `contractVersion: 1` to get the strict rules.
|
|
17
|
+
|
|
18
|
+
## Module
|
|
19
|
+
|
|
20
|
+
Default export (or the module itself):
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
interface Extension {
|
|
24
|
+
name: string; version?: string;
|
|
25
|
+
contractVersion?: 1;
|
|
26
|
+
commands?: Command[];
|
|
27
|
+
settings?: SettingDef[];
|
|
28
|
+
create(ctx: { emit(msg: Emit): boolean; log(text: string): void; settings: Record<string, string|number|boolean>; invalidate(): void }): Instance;
|
|
29
|
+
}
|
|
30
|
+
interface Instance {
|
|
31
|
+
update?(state: State): void; // deduped; see Freshness
|
|
32
|
+
close?(): void | Promise<void>; // see Close
|
|
33
|
+
panel?: Panel; // see Panels (one panel per extension)
|
|
34
|
+
onSettings?(next: Record<string, string|number|boolean>): void;
|
|
35
|
+
onEmitResult?(r: { ok: boolean; reason?: string; msg: Emit }): void;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`panel`, `onSettings` and `onEmitResult` live on the object `create()` returns. There is no module-level `onSettings`.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
interface Command {
|
|
45
|
+
name: `/${string}`; help?: string;
|
|
46
|
+
subcommands?: { name: string; help?: string }[]; // feed /help and Tab completion
|
|
47
|
+
run(args: string[], ctx: { raw: string; say(text: string, tone?: 'ok'|'warn'|'accent'|'dim'): void; extension: string; loaded: string[] }): string | { text: string; tone?: string } | Promise<...>;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`args` is the text after the command split on whitespace; `ctx.raw` is the same text unsplit. Commands exist only while the extension is loaded.
|
|
52
|
+
A thrown error is printed as `<cmd>: <message>`, never crashes the window.
|
|
53
|
+
|
|
54
|
+
## State
|
|
55
|
+
|
|
56
|
+
`update(state)` is deduped (a push identical except `at` is dropped) and sent at most once per second.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
interface State {
|
|
60
|
+
at: string; // ISO time of this push
|
|
61
|
+
stale?: true; // present only when the host's last successful server read is over 60 s old
|
|
62
|
+
org: { id: string|null; name: string|null };
|
|
63
|
+
approvals: { count: number; items: { n: number; id: string|null; risk: 'low'|'high'; title: string; agent: string|null; options?: { key: string; label: string }[] }[] };
|
|
64
|
+
health: Record<string, 'ok'|'warn'|'down'>;
|
|
65
|
+
loops: { id: string; title: string; turn: number; max: number; status: string }[];
|
|
66
|
+
fleet: { total: number; online: number; offline: number; warn: number; hosts: { name: string; state: 'ok'|'warn'|'down' }[] }; // hosts max 16, may be empty
|
|
67
|
+
evidence: { chain: 'ok'|'broken'|'unknown'; checked_at: string|null }; // PLACEHOLDER: always 'unknown' until quoxon holds a chain status
|
|
68
|
+
sessions: { active: number };
|
|
69
|
+
event?: { seq: number; id?: string; kind: string; text: string; level: 'info'|'ok'|'warn'|'error' };
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Freshness.** `state.at` is set on every push. While any extension is loaded the host pushes a heartbeat every 15 s, even when nothing
|
|
74
|
+
changed, so `at` keeps moving. `stale: true` means the data may be old (the server was not read for over 60 s): show it as such.
|
|
75
|
+
|
|
76
|
+
**Event ids.** `event.seq` is a positive integer, monotonic within one quoxon window session and never reused; a new event gets the next number,
|
|
77
|
+
the seq changes only when `kind`, `text`, `level` or `id` changes: a repeat of the held event keeps its seq until `alert_ack`. `event.id` appears only when the server supplied one.
|
|
78
|
+
|
|
79
|
+
`fleet` comes from a 30 s poll of the host list while an extension is loaded.
|
|
80
|
+
|
|
81
|
+
## emit
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
type Emit =
|
|
85
|
+
| { type: 'open_approval'; id: string; n?: number }
|
|
86
|
+
| { type: 'pick'; id: string; n?: number; option: string }
|
|
87
|
+
| { type: 'alert_ack' };
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`id` (from `approvals.items[].id`) is REQUIRED for `open_approval` and `pick`. `n` is display-only; when `id` and `n` disagree, `id` wins
|
|
91
|
+
(logged). An unknown, missing or already-resolved `id` is refused through `onEmitResult({ ok:false, reason, msg })`, and is never retargeted
|
|
92
|
+
to another item. `pick` only fills the composer with the answer command: the user presses Enter, high risk still needs the step-up PIN.
|
|
93
|
+
`alert_ack` clears the event. emit returns `true` when accepted and is ignored once the extension is unloaded.
|
|
94
|
+
(Legacy extensions without `contractVersion` may omit `id` and use `n`.)
|
|
95
|
+
|
|
96
|
+
## Panels and tabs
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
interface Panel {
|
|
100
|
+
title: string;
|
|
101
|
+
render(a: { width: number; height: number; focused: boolean; theme: Theme }): string[]; // pure
|
|
102
|
+
onKey?(key: string): boolean; // true = consumed, see Keys
|
|
103
|
+
onFocus?(): void; onBlur?(): void;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
interface Theme { name: 'dark'|'light'|'mono'; color: boolean; unicode: boolean }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The host owns the tab strip, shown only when one or more panels exist. The panel region sits above the composer, height
|
|
112
|
+
`min(12, 40% of terminal rows)`, collapsed until a panel is focused or opened. `/ext tab <name>` focuses (and opens) a panel; `/ext tab off`
|
|
113
|
+
collapses; Ctrl+] cycles focus through the panels; Esc returns keys to the composer (the panel stays open). While focused, keys go to
|
|
114
|
+
`onKey` first; a key it does not consume is dropped. The panel never receives `escape` or `ctrl+]`: the host consumes both.
|
|
115
|
+
|
|
116
|
+
**Keys.** `onKey(key)` gets one of `up down left right enter tab backspace space home end pageup pagedown`, otherwise the single typed
|
|
117
|
+
character (`a`, `?`, `/`), or `ctrl+<c>` / `alt+<c>` for the modifier combinations the terminal reports. Pasted text arrives one
|
|
118
|
+
character at a time.
|
|
119
|
+
|
|
120
|
+
`render` must be pure and fast. The 50 ms budget is wall-clock per render call (`performance.now`). The host clips the result to `height` lines and `width` columns. A render that throws, or takes over 50 ms,
|
|
121
|
+
shows `panel error` in its place and is retried no sooner than 5 s later; it never crashes the window. The panel re-renders on every state
|
|
122
|
+
push and on `invalidate()` (passed to `create`, throttled to 4 per second).
|
|
123
|
+
|
|
124
|
+
## Settings
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
interface SettingDef { key: string; type: 'string'|'number'|'bool'|'enum'; default: string|number|boolean; help: string; choices?: string[] }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Values live in `~/.quox/extensions.json` on the extension's entry as `settings: { <key>: value }`. `create({ settings })` receives the resolved
|
|
131
|
+
object (defaults overlaid with stored values). `/ext set <name> <key> <value>` validates by type (number: `Number()` and it must be a finite number; bool: `true|yes|on|1` or
|
|
132
|
+
`false|no|off|0`, case-insensitive, anything else is rejected with a message; enum: an exact match to one of `choices`; string: any text), stores it, and calls `onSettings(next)` on the running instance.
|
|
133
|
+
`/ext settings <name>` lists each key with its value, default and help. The extension must be loaded and listed.
|
|
134
|
+
|
|
135
|
+
## Close
|
|
136
|
+
|
|
137
|
+
`close()` may return a promise. The host awaits it for at most 500 ms, then drops the instance anyway and logs it. It must be idempotent
|
|
138
|
+
(the host may call it twice) and must free every timer, interval, watcher, device and process listener, because hot reload calls it on each change.
|
|
139
|
+
|
|
140
|
+
## Hot reload
|
|
141
|
+
|
|
142
|
+
While an extension is loaded, quoxon watches its `package.json` (`fs.watch`, 2 s debounce, plus a 30 s stat poll because global installs
|
|
143
|
+
replace directories). When the version or mtime changes it awaits `close()` on the old instance (500 ms cap), drops its commands and panel,
|
|
144
|
+
re-imports the entry with a cache-busting `?v=<version>-<mtime>`, and calls `create()` again, then prints `↻ <name> updated <old> -> <new>`.
|
|
145
|
+
Only the entry module is re-imported. A failed reload leaves the old instance closed, prints the error once, and shows `failed: <error>` in
|
|
146
|
+
`/ext list` until the next change. Watchers and timers are cleared on `/ext off`, unload and exit.
|