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
package/lib/quoxon/rows.js
CHANGED
|
@@ -21,8 +21,11 @@ import { execFile } from 'node:child_process';
|
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { join, dirname } from 'node:path';
|
|
23
23
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { cellWidth } from './cells.js';
|
|
24
25
|
|
|
25
26
|
export const MAX_ROWS = 3;
|
|
27
|
+
/** Lines drawn above the status bar (a row can bring several with `lines`); the stored-row cap stays MAX_ROWS. */
|
|
28
|
+
export const MAX_LINES = 4;
|
|
26
29
|
export const FETCH_TIMEOUT_MS = 5000;
|
|
27
30
|
/** url rows fetch from busy boxes (the proof board is ~400 KB): 15 s. cmd rows keep FETCH_TIMEOUT_MS. */
|
|
28
31
|
export const URL_TIMEOUT_MS = 15000;
|
|
@@ -220,11 +223,55 @@ const trendText = (t) => {
|
|
|
220
223
|
return '';
|
|
221
224
|
};
|
|
222
225
|
|
|
226
|
+
/* ---- cells (contract v1.1, TUI-QPE 2026-10-19) --------------------------------------------------
|
|
227
|
+
* A row may carry `cells`: [{ parts: [{ text, tone?, bold?, keep?, group? }], keep? }]. Each part has its own colour
|
|
228
|
+
* (a tone), so one row can say "70%" in amber and "▼12 broke" in red. `keep` says what goes first when the window is
|
|
229
|
+
* narrow: the lowest keep is dropped whole (a part, the parts sharing its group, or a cell; never half a word); no keep
|
|
230
|
+
* = never dropped. Rows with the same `grid` share one keep order and line their cells up in columns; `min_cols` hides
|
|
231
|
+
* a row in a narrower window.
|
|
232
|
+
*/
|
|
233
|
+
export const CELL_LIMITS = { cells: 6, parts: 12, text: 80 };
|
|
234
|
+
const keepOf = (k) => (typeof k === 'number' && Number.isFinite(k) ? Math.max(0, Math.min(99, k)) : null);
|
|
235
|
+
|
|
236
|
+
function parseCells(list, warnings) {
|
|
237
|
+
if (!Array.isArray(list)) { warnings.push('"cells" is a list of {parts: [{text, tone}]}; ignored.'); return []; }
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const c of list.slice(0, CELL_LIMITS.cells)) {
|
|
240
|
+
const raw = Array.isArray(c?.parts) ? c.parts : typeof c?.text === 'string' ? [c] : null;
|
|
241
|
+
if (!raw) { warnings.push('A cell needs "parts" (or "text"); skipped.'); continue; }
|
|
242
|
+
const parts = [];
|
|
243
|
+
for (const p of raw.slice(0, CELL_LIMITS.parts)) {
|
|
244
|
+
// Keep edge spaces (a part's own joiner, " · reach 214/266"): clean the middle, not the ends.
|
|
245
|
+
const t = typeof p?.text === 'string' ? p.text : typeof p?.text === 'number' ? fmtNum(p.text) : '';
|
|
246
|
+
const lead = /^\s/.test(t) ? ' ' : ''; const trail = /\s$/.test(t) ? ' ' : '';
|
|
247
|
+
const body = cleanText(t, CELL_LIMITS.text);
|
|
248
|
+
if (!body) continue;
|
|
249
|
+
const part = { text: lead + body + trail };
|
|
250
|
+
if (p.tone !== undefined && p.tone !== null) {
|
|
251
|
+
if (STATUS_ROW_TONES.includes(p.tone)) { if (p.tone !== 'neutral') part.tone = p.tone; }
|
|
252
|
+
else warnings.push(`a part's "tone" is one of ${STATUS_ROW_TONES.join(', ')}; "${cleanText(p.tone, 16)}" shows as neutral.`);
|
|
253
|
+
}
|
|
254
|
+
if (p.bold === true) part.bold = true;
|
|
255
|
+
const k = keepOf(p.keep); if (k !== null) part.keep = k;
|
|
256
|
+
if (typeof p.group === 'string' && /^[A-Za-z0-9_-]{1,16}$/.test(p.group)) part.group = p.group; // parts that drop together
|
|
257
|
+
parts.push(part);
|
|
258
|
+
}
|
|
259
|
+
if (!parts.length) continue;
|
|
260
|
+
const cell = { parts };
|
|
261
|
+
const k = keepOf(c.keep); if (k !== null) cell.keep = k;
|
|
262
|
+
out.push(cell);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** cells -> one plain line (screen context, /row list, a narrow fallback). */
|
|
268
|
+
export const cellsText = (cells) => (cells || []).map((c) => c.parts.map((p) => p.text).join('').trim()).join(' │ ');
|
|
269
|
+
|
|
223
270
|
/**
|
|
224
271
|
* Parsed data -> { seg, warnings } | { error }. Lenient on read (an unknown tone shows neutral and is named
|
|
225
272
|
* in warnings, so `quox rows test` can say so); strict on what it prints (everything passes cleanText).
|
|
226
273
|
*/
|
|
227
|
-
export function parseStatusRow(d) {
|
|
274
|
+
export function parseStatusRow(d, depth = 0) {
|
|
228
275
|
if (!d || typeof d !== 'object' || Array.isArray(d)) return { error: 'A status row is a JSON object: {"text": "..."}.' };
|
|
229
276
|
const warnings = [];
|
|
230
277
|
const seg = {};
|
|
@@ -233,7 +280,25 @@ export function parseStatusRow(d) {
|
|
|
233
280
|
if (typeof d.value === 'number' && Number.isFinite(d.value)) seg.value = fmtNum(d.value);
|
|
234
281
|
else if (typeof d.value === 'string' && cleanText(d.value, 24)) seg.value = cleanText(d.value, 24);
|
|
235
282
|
else if (d.value !== undefined && d.value !== null) warnings.push('"value" is a number or a short string; ignored.');
|
|
283
|
+
if (depth === 0 && d.lines !== undefined && d.lines !== null) { // TUI-QPE 2026-10-20: one source, several lines (v1.1)
|
|
284
|
+
if (!Array.isArray(d.lines)) warnings.push('"lines" is a list of status rows; ignored.');
|
|
285
|
+
else {
|
|
286
|
+
const lines = [];
|
|
287
|
+
for (const x of d.lines.slice(0, MAX_LINES)) { const q = parseStatusRow(x, 1); if (q.error) warnings.push(`a line: ${q.error}`); else { lines.push(q.seg); warnings.push(...q.warnings); } }
|
|
288
|
+
if (lines.length) { seg.lines = lines; if (!seg.text) seg.text = lines.map((l) => l.text).join(' / ').slice(0, 160); }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (d.cells !== undefined && d.cells !== null) { // TUI-QPE: coloured, responsive cells (v1.1)
|
|
292
|
+
const cells = parseCells(d.cells, warnings);
|
|
293
|
+
if (cells.length) { seg.cells = cells; if (!seg.text) seg.text = cellsText(cells); }
|
|
294
|
+
}
|
|
236
295
|
if (!seg.text && !seg.value) return { error: 'Not a status row: it needs "text" (see quox rows contract).' };
|
|
296
|
+
if (d.grid !== undefined && d.grid !== null) {
|
|
297
|
+
if (typeof d.grid === 'string' && /^[A-Za-z0-9_-]{1,16}$/.test(d.grid)) seg.grid = d.grid; else warnings.push('"grid" is a short name (letters, digits, - and _); ignored.');
|
|
298
|
+
}
|
|
299
|
+
if (d.min_cols !== undefined && d.min_cols !== null) {
|
|
300
|
+
if (Number.isInteger(d.min_cols) && d.min_cols >= 20 && d.min_cols <= 400) seg.minCols = d.min_cols; else warnings.push('"min_cols" is a whole number from 20 to 400; ignored.');
|
|
301
|
+
}
|
|
237
302
|
if (d.tone !== undefined && d.tone !== null) {
|
|
238
303
|
if (STATUS_ROW_TONES.includes(d.tone)) { if (d.tone !== 'neutral') seg.tone = d.tone; }
|
|
239
304
|
else warnings.push(`"tone" is one of ${STATUS_ROW_TONES.join(', ')}; "${cleanText(d.tone, 16)}" shows as neutral.`);
|
|
@@ -620,6 +685,12 @@ export function rowParts(st, { now = Date.now(), labelW = 0 } = {}) {
|
|
|
620
685
|
const live = st.status === 'ok' || (st.status === 'error' && d.role !== 'error');
|
|
621
686
|
if (!live || st.status === 'loading') return [[d.text, d.role]];
|
|
622
687
|
const label = row.label || seg?.label || '';
|
|
688
|
+
if (seg?.cells?.length) { // TUI-QPE: one cells row on its own (quox rows test); RowsBar uses layoutRows
|
|
689
|
+
const out = label ? [[label + ' ', 'brand', true]] : [];
|
|
690
|
+
seg.cells.forEach((c, i) => { if (i) out.push([CELL_SEP, 'label']); for (const p of c.parts) out.push(partSeg(p)); });
|
|
691
|
+
if (st.status === 'error') out.push([` · ${ageMark(now - st.at)} old`, 'warn']);
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
623
694
|
const toneR = seg?.tone ? toneRole(seg.tone) : null;
|
|
624
695
|
if (!label && !seg?.tone && !seg?.value && !seg?.trend) return [[d.text, d.role]];
|
|
625
696
|
const out = [];
|
|
@@ -651,6 +722,178 @@ export function clipParts(parts, width) {
|
|
|
651
722
|
return out;
|
|
652
723
|
}
|
|
653
724
|
|
|
725
|
+
/* ---- cells layout (TUI-QPE) ---------------------------------------------------------------- */
|
|
726
|
+
|
|
727
|
+
export const CELL_SEP = ' │ ';
|
|
728
|
+
const W = (t) => cellWidth(String(t));
|
|
729
|
+
const cellW = (c) => c.parts.reduce((a, p) => a + W(p.text), 0);
|
|
730
|
+
const cellsW = (cs) => cs.reduce((a, c, i) => a + cellW(c) + (i ? W(CELL_SEP) : 0), 0);
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* cells -> the cells that fit in `room` columns. Whole items go, lowest `keep` first (a tie drops the rightmost); an
|
|
734
|
+
* item with no keep stays. If even the kept items do not fit, the line ends at a word boundary with … (never mid-word).
|
|
735
|
+
*/
|
|
736
|
+
/** Drop the one lowest-keep item (a tie drops the rightmost; a part takes its group with it). -> new cells, or null. */
|
|
737
|
+
export function dropLowest(cells) {
|
|
738
|
+
let best = null;
|
|
739
|
+
cells.forEach((c, i) => {
|
|
740
|
+
const cand = (keep, pos, j) => { if (keep == null) return; if (!best || keep < best.keep || (keep === best.keep && pos > best.pos)) best = { keep, pos, i, j }; };
|
|
741
|
+
cand(c.keep, i * 1000 + 999, -1);
|
|
742
|
+
c.parts.forEach((p, j) => cand(p.keep, i * 1000 + j, j));
|
|
743
|
+
});
|
|
744
|
+
if (!best) return null;
|
|
745
|
+
const cs = cells.map((c) => ({ ...c, parts: [...c.parts] }));
|
|
746
|
+
if (best.j < 0) cs.splice(best.i, 1);
|
|
747
|
+
else {
|
|
748
|
+
const c = cs[best.i]; const g = c.parts[best.j].group;
|
|
749
|
+
c.parts = lead(g ? c.parts.filter((p) => p.group !== g) : c.parts.filter((_, j) => j !== best.j));
|
|
750
|
+
if (!c.parts.length) cs.splice(best.i, 1);
|
|
751
|
+
}
|
|
752
|
+
return cs;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function fitCells(cells, room) {
|
|
756
|
+
let cs = (cells || []).map((c) => ({ ...c, parts: c.parts.map((p) => ({ ...p })) }));
|
|
757
|
+
while (cellsW(cs) > room) { const next = dropLowest(cs); if (!next) break; cs = next; }
|
|
758
|
+
if (cellsW(cs) <= room) return { cells: cs, clipped: false };
|
|
759
|
+
// Still too wide with only the must-keep items: cut at the last word that fits.
|
|
760
|
+
const out = []; let used = 0; const max = Math.max(1, room - 1);
|
|
761
|
+
for (let i = 0; i < cs.length; i++) {
|
|
762
|
+
const parts = [];
|
|
763
|
+
if (i) { if (used + W(CELL_SEP) > max) break; used += W(CELL_SEP); }
|
|
764
|
+
let full = true;
|
|
765
|
+
for (const p of cs[i].parts) {
|
|
766
|
+
const w = W(p.text);
|
|
767
|
+
if (used + w <= max) { parts.push(p); used += w; continue; }
|
|
768
|
+
const words = p.text.split(/(\s+)/); let t = '';
|
|
769
|
+
for (const wd of words) { if (used + W(t + wd) > max) break; t += wd; }
|
|
770
|
+
t = t.replace(/\s+$/, '');
|
|
771
|
+
if (t.trim()) { parts.push({ ...p, text: t }); used += W(t); } // whole words only, so this is a word boundary
|
|
772
|
+
full = false; break;
|
|
773
|
+
}
|
|
774
|
+
if (parts.length) out.push({ ...cs[i], parts });
|
|
775
|
+
if (!full) break;
|
|
776
|
+
}
|
|
777
|
+
if (!out.length) out.push({ parts: [{ text: '' }] });
|
|
778
|
+
const last = out[out.length - 1].parts; last[last.length - 1] = { ...last[last.length - 1], text: last[last.length - 1].text.replace(/\s+$/, '') };
|
|
779
|
+
last.push({ text: '…' });
|
|
780
|
+
return { cells: out, clipped: true };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** A cell whose first item was dropped must not start with its joiner (" · CLI 578/958"): trim a leading " · ". */
|
|
784
|
+
const lead = (parts) => (parts.length && /^\s*·\s*/.test(parts[0].text) ? [{ ...parts[0], text: parts[0].text.replace(/^\s*·\s*/, '') }, ...parts.slice(1)] : parts);
|
|
785
|
+
|
|
786
|
+
/** One part -> a [text, role, bold] segment (the RowsBar shape). Tones are palette roles, never grey. */
|
|
787
|
+
const partSeg = (p) => [p.text, p.tone ? toneRole(p.tone) : 'prose', Boolean(p.bold)];
|
|
788
|
+
|
|
789
|
+
/** States whose seg has cells and something to show. */
|
|
790
|
+
const hasCells = (st) => Boolean(st?.seg?.cells?.length) && (st.status === 'ok' || (st.status === 'error' && st.text));
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* The rows RowsBar draws at `cols`, in order -> [{ st, parts }]: rows below their min_cols are left out; cells rows
|
|
794
|
+
* are fitted, and rows sharing a `grid` share a lead (label) column and pad their cells so the separators line up.
|
|
795
|
+
* Other rows keep the classic rowParts + clipParts path, unchanged.
|
|
796
|
+
*/
|
|
797
|
+
/**
|
|
798
|
+
* Rows -> one state per drawn line: a row whose seg has `lines` becomes one state per line (ids `<id>.<n>`, the row's
|
|
799
|
+
* label on the first only, the row's grid unless a line names its own). Other rows pass through unchanged.
|
|
800
|
+
*/
|
|
801
|
+
export function expandLines(states) {
|
|
802
|
+
const out = [];
|
|
803
|
+
for (const st of states || []) {
|
|
804
|
+
const ls = st?.seg?.lines;
|
|
805
|
+
if (!ls?.length || !(st.status === 'ok' || (st.status === 'error' && st.text))) { out.push(st); continue; }
|
|
806
|
+
ls.forEach((seg, i) => out.push({ ...st, row: { ...st.row, id: `${st.row.id}.${i + 1}`, label: i === 0 ? st.row.label : '' },
|
|
807
|
+
text: seg.text, seg: { ...seg, grid: seg.grid || st.seg.grid || `#${st.row.id}`, ...(i === 0 ? {} : { label: undefined }) } }));
|
|
808
|
+
}
|
|
809
|
+
return out;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function layoutRows(states, cols, { now = Date.now() } = {}) {
|
|
813
|
+
const shown = expandLines(states).filter((st) => !(st?.seg?.minCols && cols < st.seg.minCols)).slice(0, MAX_LINES);
|
|
814
|
+
const room = Math.max(1, cols - 2);
|
|
815
|
+
const labelW = labelWidth(shown.filter((st) => !hasCells(st)));
|
|
816
|
+
const groups = new Map();
|
|
817
|
+
for (const st of shown) if (hasCells(st)) { const g = st.seg.grid || `#${st.row.id}`; if (!groups.has(g)) groups.set(g, []); groups.get(g).push(st); }
|
|
818
|
+
const done = new Map();
|
|
819
|
+
for (const list of groups.values()) {
|
|
820
|
+
const leadOf = (st) => String(st.row.label || st.seg.label || '');
|
|
821
|
+
const leadW = Math.min(16, Math.max(0, ...list.map((st) => W(leadOf(st)))));
|
|
822
|
+
const leadCols = leadW ? leadW + 1 : 0;
|
|
823
|
+
const roomL = room - leadCols;
|
|
824
|
+
const base = list.map((st) => {
|
|
825
|
+
const cells = st.seg.cells.map((c) => ({ ...c, parts: [...c.parts] }));
|
|
826
|
+
return cells;
|
|
827
|
+
});
|
|
828
|
+
// `keep` is ONE priority across the grid. The layout drops the shortest prefix of the grid-wide order (lowest keep
|
|
829
|
+
// first; a tie drops the rightmost, then the lower row) that lets every row fit with its columns lined up. A wider
|
|
830
|
+
// window can only shorten that prefix, so it never shows less (2026-10-20: a greedy per-row pass did, at 68/69 cols).
|
|
831
|
+
const units = [];
|
|
832
|
+
base.forEach((cells, r) => cells.forEach((c, i) => {
|
|
833
|
+
if (c.keep != null) units.push({ r, i, j: -1, keep: c.keep, pos: i * 1000 + 999 });
|
|
834
|
+
const seenG = new Set();
|
|
835
|
+
c.parts.forEach((p, j) => {
|
|
836
|
+
if (p.keep == null) return;
|
|
837
|
+
if (p.group) { if (seenG.has(p.group)) return; seenG.add(p.group); }
|
|
838
|
+
units.push({ r, i, j, group: p.group, keep: p.keep, pos: i * 1000 + j });
|
|
839
|
+
});
|
|
840
|
+
}));
|
|
841
|
+
units.sort((x, y) => x.keep - y.keep || y.pos - x.pos || y.r - x.r);
|
|
842
|
+
const build = (k) => {
|
|
843
|
+
const gone = units.slice(0, k);
|
|
844
|
+
return base.map((cells, r) => cells.map((c, i) => {
|
|
845
|
+
if (gone.some((u) => u.r === r && u.i === i && u.j < 0)) return null;
|
|
846
|
+
const parts = c.parts.filter((p, j) => !gone.some((u) => u.r === r && u.i === i && u.j >= 0 && (u.group ? p.group === u.group : u.j === j)));
|
|
847
|
+
return parts.length ? { ...c, parts: lead(parts) } : null;
|
|
848
|
+
}).filter(Boolean));
|
|
849
|
+
};
|
|
850
|
+
const colWidths = (rows) => { const w = []; for (const cs of rows) cs.forEach((c, i) => { if (i < cs.length - 1) w[i] = Math.max(w[i] || 0, cellW(c)); }); return w; };
|
|
851
|
+
const alignedW = (cs, w) => cs.reduce((a, c, i) => a + (i < cs.length - 1 ? Math.max(w[i] || 0, cellW(c)) : cellW(c)) + (i ? W(CELL_SEP) : 0), 0);
|
|
852
|
+
let rows = null;
|
|
853
|
+
for (let k = 0; k <= units.length; k++) {
|
|
854
|
+
const cand = build(k);
|
|
855
|
+
// A row whose must-keep items alone are too wide is cut at a word below and left out of the column widths.
|
|
856
|
+
const over = cand.map((cs) => cellsW(cs) > roomL);
|
|
857
|
+
const w = colWidths(cand.filter((_, r) => !over[r]));
|
|
858
|
+
if (cand.every((cs, r) => over[r] ? !units.slice(k).some((u) => u.r === r) : alignedW(cs, w) <= roomL)) { rows = cand; break; }
|
|
859
|
+
}
|
|
860
|
+
rows ??= build(units.length);
|
|
861
|
+
const over = rows.map((cs) => cellsW(cs) > roomL);
|
|
862
|
+
const colW = colWidths(rows.filter((_, r) => !over[r]));
|
|
863
|
+
list.forEach((st, r) => {
|
|
864
|
+
const f = over[r] ? fitCells(rows[r], roomL) : { cells: rows[r], clipped: false };
|
|
865
|
+
const pads = f.cells.map((c, i) => (!f.clipped && i < f.cells.length - 1 ? Math.max(0, (colW[i] || 0) - cellW(c)) : 0));
|
|
866
|
+
const out = [];
|
|
867
|
+
if (leadCols) out.push([leadOf(st) + ' '.repeat(Math.max(0, leadW - W(leadOf(st)))) + ' ', 'brand', true]);
|
|
868
|
+
f.cells.forEach((c, i) => {
|
|
869
|
+
if (i) out.push([CELL_SEP, 'label']);
|
|
870
|
+
for (const p of c.parts) out.push(partSeg(p));
|
|
871
|
+
if (pads[i]) out.push([' '.repeat(pads[i]), 'prose']);
|
|
872
|
+
});
|
|
873
|
+
done.set(st, out);
|
|
874
|
+
});
|
|
875
|
+
// The age of a stale value is said ONCE per grid and only in space the fit left over, never in the fit budget:
|
|
876
|
+
// appended per line it pushed whole items out every minute as the data aged (coordinator 2026-10-20 03:39Z).
|
|
877
|
+
// It goes on the first stale line that has room, else the line with the most room; with no room it is left out.
|
|
878
|
+
const stale = list.filter((st) => st.status === 'error' && st.at != null);
|
|
879
|
+
if (stale.length) {
|
|
880
|
+
const age = ` · ${ageMark(now - Math.min(...stale.map((st) => st.at)))} old`;
|
|
881
|
+
const used = (st) => done.get(st).reduce((a, [t]) => a + W(t), 0);
|
|
882
|
+
const roomOf = (st) => room - used(st);
|
|
883
|
+
const pick = stale.find((st) => roomOf(st) >= W(age)) || [...list].sort((a, b) => roomOf(b) - roomOf(a))[0];
|
|
884
|
+
if (pick && roomOf(pick) >= W(age)) done.get(pick).push([age, 'warn']);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return shown.map((st) => ({ st, parts: done.get(st) || clipParts(rowParts(st, { now, labelW }), room) }));
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/** /qpe rows N: keep the first n rows of a grid (0 hides the grid). Rows not in the grid are untouched. */
|
|
891
|
+
export function limitGrid(states, grid, n) {
|
|
892
|
+
if (n == null || !Number.isInteger(n) || n < 0) return states;
|
|
893
|
+
let seen = 0;
|
|
894
|
+
return (states || []).filter((st) => st?.seg?.grid !== grid || ++seen <= n);
|
|
895
|
+
}
|
|
896
|
+
|
|
654
897
|
/* ---- local-only store for cmd rows -------------------------------------------------------- */
|
|
655
898
|
|
|
656
899
|
export const localRowsFile = () => join(homedir(), '.config', 'quox', 'quoxon-rows.local.json');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// TUI-STATUS: which REGISTERED server this quox runs on (the fleet host registry, `quox fleet hosts`).
|
|
2
|
+
// Pure matcher + a once-per-session resolver. Unregistered or any failure -> null: the status line shows nothing,
|
|
3
|
+
// and never the raw hostname (screenshots).
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
|
|
6
|
+
/** This machine's non-internal IPv4 addresses. */
|
|
7
|
+
export function localIps(ifaces = os.networkInterfaces()) {
|
|
8
|
+
return Object.values(ifaces || {}).flat().filter(a => a && a.family === 'IPv4' && !a.internal).map(a => a.address);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const bare = (s) => String(s || '').toLowerCase().split('.')[0];
|
|
12
|
+
|
|
13
|
+
/** hosts: [{ id, ip, group }] -> { name, role } for the host matching this machine's hostname or IP, else null. */
|
|
14
|
+
export function matchServer(hosts, { hostname, ips = [] } = {}, registry) {
|
|
15
|
+
if (!Array.isArray(hosts)) return null;
|
|
16
|
+
const hn = bare(hostname);
|
|
17
|
+
const mine = new Set(ips);
|
|
18
|
+
const hit = hosts.find(h => h && h.id && hn && bare(h.id) === hn)
|
|
19
|
+
|| hosts.find(h => h && h.id && h.ip && h.ip !== '0.0.0.0' && mine.has(h.ip));
|
|
20
|
+
if (!hit) return null;
|
|
21
|
+
// DEVFLEET: with a registry the role is the server's PURPOSE (e.g. "main dev"), never the Bastion group; no entry -> nothing.
|
|
22
|
+
if (registry !== undefined) { const p = registry?.servers?.[hit.id]?.purpose; return { name: String(hit.id), role: p ? String(p) : null }; }
|
|
23
|
+
return { name: String(hit.id), role: hit.group ? String(hit.group) : null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The profile's instance host when it is NOT this machine (a remote instance), else null. */
|
|
27
|
+
export function remoteHost(url, { hostname, ips = [] } = {}) {
|
|
28
|
+
let h;
|
|
29
|
+
try { h = new URL(url).hostname.toLowerCase(); } catch { return null; }
|
|
30
|
+
if (!h || h === 'localhost' || h === '::1' || h === '[::1]' || h.startsWith('127.') || mine(h, ips) || bare(h) === bare(hostname)) return null;
|
|
31
|
+
return h;
|
|
32
|
+
}
|
|
33
|
+
function mine(h, ips) { return ips.includes(h); }
|
|
34
|
+
|
|
35
|
+
/** -> { name, role, remote } | null. Never throws; `listHosts` is async () => hosts[]. */
|
|
36
|
+
export async function resolveServerChip({ listHosts, hostname = os.hostname(), ips = localIps(), url = null, registry } = {}) {
|
|
37
|
+
try {
|
|
38
|
+
const m = matchServer(await listHosts(), { hostname, ips }, registry);
|
|
39
|
+
if (!m) return null;
|
|
40
|
+
return { ...m, remote: url ? remoteHost(url, { hostname, ips }) : null };
|
|
41
|
+
} catch { return null; }
|
|
42
|
+
}
|
package/lib/quoxon/session.js
CHANGED
|
@@ -141,6 +141,16 @@ export function sayMissText(input, rows, cap = 8) {
|
|
|
141
141
|
].filter(Boolean).join('\n');
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/** SAY-LAST-TARGET: a word that is a URL, a path or an address (has / : or .) is never a slot or session name. */
|
|
145
|
+
export const looksLikeAddress = (w) => /[/:.]/.test(String(w || ''));
|
|
146
|
+
|
|
147
|
+
/** SAY-LAST-TARGET: "/say" with no usable target and nothing recent to offer: who to, and who is reachable. */
|
|
148
|
+
export function sayWhoText(rows, cap = 8) {
|
|
149
|
+
const held = (Array.isArray(rows) ? rows : []).filter(r => r?.name && r.holder_session && r.state !== 'free');
|
|
150
|
+
const reach = held.slice(0, cap).map(r => `${r.name} (${r.holder_session})`).join(', ');
|
|
151
|
+
return ['Who to? /say <slot> <message>', reach ? `Reachable now: ${reach}${held.length > cap ? `, +${held.length - cap} more (/slots)` : ''}` : null].filter(Boolean).join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
144
154
|
const idOf = (a) => String(a?.agent_id ?? a?.id ?? a?.name ?? '');
|
|
145
155
|
const nameOf = (a) => String(a?.name ?? a?.agent_id ?? a?.id ?? '');
|
|
146
156
|
|
|
@@ -25,6 +25,42 @@ function variants(w) {
|
|
|
25
25
|
|
|
26
26
|
const MAX_DEPTH = 4;
|
|
27
27
|
|
|
28
|
+
// CONTACTSQ: `/contact new Cristina Cowles --relationship Wife --mobile +44...` must become a command the verb accepts.
|
|
29
|
+
// Per-leaf flag tables (value flags and bare switches), synonyms people type, and a clear refusal for anything else.
|
|
30
|
+
const CONTACTS_ADD = {
|
|
31
|
+
value: ['phone', 'email', 'kind', 'organisation', 'job-title', 'relationship', 'notes', 'given', 'family', 'middle', 'prefix', 'suffix', 'nickname', 'phonetic-given', 'phonetic-family'],
|
|
32
|
+
bare: ['org-kind'],
|
|
33
|
+
synonyms: { mobile: 'phone', tel: 'phone', telephone: 'phone', cell: 'phone', number: 'phone', 'e-mail': 'email', rel: 'relationship', org: 'organisation', company: 'organisation', title: 'job-title' },
|
|
34
|
+
lower: ['relationship', 'kind'],
|
|
35
|
+
};
|
|
36
|
+
const NORMALISERS = { 'contacts add': (args) => normaliseFlags(args, CONTACTS_ADD, 'quox contacts add') };
|
|
37
|
+
|
|
38
|
+
/** args = [...path, ...rest]. Positional words before the first flag become ONE name; flags are mapped and checked. */
|
|
39
|
+
function normaliseFlags(args, spec, verb) {
|
|
40
|
+
const head = args.slice(0, 2);
|
|
41
|
+
const rest = args.slice(2);
|
|
42
|
+
const name = [];
|
|
43
|
+
const out = [];
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < rest.length && !rest[i].startsWith('-')) name.push(rest[i++]);
|
|
46
|
+
while (i < rest.length) {
|
|
47
|
+
const tok = rest[i++];
|
|
48
|
+
if (!tok.startsWith('--')) return { error: `Unexpected "${tok}" after the flags. Quote values with spaces. ${verb} takes: ${[...spec.value, ...spec.bare].map(f => `--${f}`).join(' ')}` };
|
|
49
|
+
const eq = tok.indexOf('=');
|
|
50
|
+
const raw = (eq > 0 ? tok.slice(2, eq) : tok.slice(2)).toLowerCase();
|
|
51
|
+
const flag = spec.synonyms[raw] || raw;
|
|
52
|
+
if (spec.bare.includes(flag)) { out.push(`--${flag}`); continue; }
|
|
53
|
+
if (!spec.value.includes(flag)) return { error: `--${raw} is not a flag of ${verb}. It takes: ${[...spec.value, ...spec.bare].map(f => `--${f}`).join(' ')}` };
|
|
54
|
+
const vals = eq > 0 ? [tok.slice(eq + 1)] : [];
|
|
55
|
+
while (i < rest.length && !rest[i].startsWith('--')) vals.push(rest[i++]);
|
|
56
|
+
if (!vals.length || !vals.join('')) return { error: `--${raw} needs a value.` };
|
|
57
|
+
let v = vals.join(' ');
|
|
58
|
+
if (spec.lower.includes(flag)) v = v.toLowerCase();
|
|
59
|
+
out.push(`--${flag}`, v);
|
|
60
|
+
}
|
|
61
|
+
return { args: [...head, ...(name.length ? [name.join(' ')] : []), ...out] };
|
|
62
|
+
}
|
|
63
|
+
|
|
28
64
|
/** -> { path, cls, args, display } | null. args is the full argv after `quox` (quoted args preserved). */
|
|
29
65
|
export function resolveCliSlash(cmd, restArg, leaves = LEAVES) {
|
|
30
66
|
const first = String(cmd || '').replace(/^\//, '');
|
|
@@ -44,7 +80,12 @@ export function resolveCliSlash(cmd, restArg, leaves = LEAVES) {
|
|
|
44
80
|
for (const p of walk(n)) {
|
|
45
81
|
const path = p.join(' ');
|
|
46
82
|
if (Object.prototype.hasOwnProperty.call(leaves, path)) {
|
|
47
|
-
|
|
83
|
+
let args = [...p, ...words.slice(n)];
|
|
84
|
+
if (NORMALISERS[path]) {
|
|
85
|
+
const nr = NORMALISERS[path](args);
|
|
86
|
+
if (nr.error) return { path, cls: leaves[path], args, display: `quox ${args.join(' ')}`, error: nr.error };
|
|
87
|
+
args = nr.args;
|
|
88
|
+
}
|
|
48
89
|
return { path, cls: leaves[path], args, display: `quox ${args.map(a => (/[\s"']/.test(a) || a === '' ? JSON.stringify(a) : a)).join(' ')}` };
|
|
49
90
|
}
|
|
50
91
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{"account password":"never",
|
|
2
|
+
"actions add":"write",
|
|
3
|
+
"actions delete":"write",
|
|
4
|
+
"actions done":"write",
|
|
2
5
|
"actions get":"read",
|
|
3
6
|
"actions list":"read",
|
|
7
|
+
"actions move":"write",
|
|
4
8
|
"active":"read",
|
|
5
9
|
"admin agent-identity mint":"never",
|
|
6
10
|
"admin config get":"read",
|
|
@@ -85,6 +89,10 @@
|
|
|
85
89
|
"backup server restore-status":"read",
|
|
86
90
|
"backup server status":"read",
|
|
87
91
|
"backup verify":"write",
|
|
92
|
+
"blog draft list":"read",
|
|
93
|
+
"blog draft mark-published":"write",
|
|
94
|
+
"blog draft show":"read",
|
|
95
|
+
"blog draft submit":"write",
|
|
88
96
|
"brain add":"write",
|
|
89
97
|
"brain brief":"read",
|
|
90
98
|
"brain check":"read",
|
|
@@ -180,8 +188,11 @@
|
|
|
180
188
|
"contacts retention run":"write",
|
|
181
189
|
"contacts retention status":"read",
|
|
182
190
|
"contacts rewrap":"never",
|
|
191
|
+
"contacts scheduled cancel":"write",
|
|
192
|
+
"contacts scheduled list":"read",
|
|
183
193
|
"contacts search":"read",
|
|
184
194
|
"contacts selftest":"write",
|
|
195
|
+
"contacts send":"write",
|
|
185
196
|
"contacts settings":"write",
|
|
186
197
|
"contacts show":"read",
|
|
187
198
|
"contacts strict-names":"write",
|
|
@@ -258,6 +269,8 @@
|
|
|
258
269
|
"engagement jobs":"read",
|
|
259
270
|
"engagement list":"read",
|
|
260
271
|
"engagement propose":"write",
|
|
272
|
+
"engagement reconcile":"write",
|
|
273
|
+
"engagement reconciliations":"write",
|
|
261
274
|
"engagement regrade-finding":"write",
|
|
262
275
|
"engagement report":"read",
|
|
263
276
|
"engagement report-finding":"write",
|
|
@@ -275,6 +288,7 @@
|
|
|
275
288
|
"engagement show":"read",
|
|
276
289
|
"engagement sync-issues":"write",
|
|
277
290
|
"engagement tools":"read",
|
|
291
|
+
"engagement verify":"read",
|
|
278
292
|
"engagement verify-finding":"write",
|
|
279
293
|
"engagement verify-target":"write",
|
|
280
294
|
"entity create":"write",
|
|
@@ -284,6 +298,12 @@
|
|
|
284
298
|
"entity relate":"write",
|
|
285
299
|
"entity search":"read",
|
|
286
300
|
"entity unrelate":"write",
|
|
301
|
+
"evidence bundle":"write",
|
|
302
|
+
"evidence status":"read",
|
|
303
|
+
"evidence verify":"read",
|
|
304
|
+
"ext add":"write",
|
|
305
|
+
"ext list":"read",
|
|
306
|
+
"ext remove":"write",
|
|
287
307
|
"facebook insights":"read",
|
|
288
308
|
"facebook lead-forms":"read",
|
|
289
309
|
"facebook leads":"read",
|
|
@@ -307,6 +327,7 @@
|
|
|
307
327
|
"fleet gpu":"read",
|
|
308
328
|
"fleet host-update":"write",
|
|
309
329
|
"fleet hosts":"read",
|
|
330
|
+
"fleet ping":"write",
|
|
310
331
|
"fleet rehome-agent":"write",
|
|
311
332
|
"fleet status":"read",
|
|
312
333
|
"fleet summary":"read",
|
|
@@ -343,10 +364,12 @@
|
|
|
343
364
|
"hat list":"read",
|
|
344
365
|
"hat reset":"write",
|
|
345
366
|
"hat show":"read",
|
|
367
|
+
"hat upgrade":"never",
|
|
346
368
|
"hat use":"never",
|
|
347
369
|
"health":"read",
|
|
348
370
|
"hyperion deck":"never",
|
|
349
371
|
"hyperion status":"read",
|
|
372
|
+
"image generate":"write",
|
|
350
373
|
"inbox get":"read",
|
|
351
374
|
"inbox list":"read",
|
|
352
375
|
"inbox respond":"write",
|
|
@@ -368,6 +391,32 @@
|
|
|
368
391
|
"inventory":"read",
|
|
369
392
|
"invites accept":"never",
|
|
370
393
|
"invites list":"read",
|
|
394
|
+
"kuma coverage propose":"write",
|
|
395
|
+
"kuma coverage show":"read",
|
|
396
|
+
"kuma diagnose":"write",
|
|
397
|
+
"kuma down":"write",
|
|
398
|
+
"kuma hooks create":"never",
|
|
399
|
+
"kuma hooks delete":"write",
|
|
400
|
+
"kuma hooks list":"read",
|
|
401
|
+
"kuma incidents":"write",
|
|
402
|
+
"kuma maintenance end":"write",
|
|
403
|
+
"kuma maintenance list":"read",
|
|
404
|
+
"kuma maintenance open":"never",
|
|
405
|
+
"kuma monitors add":"write",
|
|
406
|
+
"kuma monitors edit":"write",
|
|
407
|
+
"kuma monitors get":"read",
|
|
408
|
+
"kuma monitors list":"read",
|
|
409
|
+
"kuma monitors pause":"write",
|
|
410
|
+
"kuma monitors remove":"write",
|
|
411
|
+
"kuma monitors resume":"write",
|
|
412
|
+
"kuma refresh":"write",
|
|
413
|
+
"kuma sla list":"read",
|
|
414
|
+
"kuma sla verify":"read",
|
|
415
|
+
"kuma status":"read",
|
|
416
|
+
"kuma status-incident draft":"write",
|
|
417
|
+
"kuma status-incident publish":"write",
|
|
418
|
+
"kuma status-incident resolve":"write",
|
|
419
|
+
"kuma uptime":"write",
|
|
371
420
|
"licence catalogue-sign":"never",
|
|
372
421
|
"licence refresh":"write",
|
|
373
422
|
"licence reissue":"never",
|
|
@@ -385,6 +434,12 @@
|
|
|
385
434
|
"llm-policy test":"write",
|
|
386
435
|
"login":"never",
|
|
387
436
|
"logout":"never",
|
|
437
|
+
"loop core-local add":"write",
|
|
438
|
+
"loop core-local list":"read",
|
|
439
|
+
"loop core-local remove":"write",
|
|
440
|
+
"loop list":"read",
|
|
441
|
+
"loop status":"read",
|
|
442
|
+
"loop watch":"never",
|
|
388
443
|
"lucid ack":"write",
|
|
389
444
|
"lucid area":"read",
|
|
390
445
|
"lucid ask":"read",
|
|
@@ -520,6 +575,7 @@
|
|
|
520
575
|
"objective get":"read",
|
|
521
576
|
"objective halt":"write",
|
|
522
577
|
"objective list":"read",
|
|
578
|
+
"objective retry":"write",
|
|
523
579
|
"objective start":"write",
|
|
524
580
|
"offload release":"write",
|
|
525
581
|
"offload status":"read",
|
|
@@ -608,6 +664,7 @@
|
|
|
608
664
|
"prompt update":"write",
|
|
609
665
|
"prompt versions":"read",
|
|
610
666
|
"provider connect":"write",
|
|
667
|
+
"provider install":"write",
|
|
611
668
|
"provider list":"read",
|
|
612
669
|
"provider test":"read",
|
|
613
670
|
"qlarity allowlist":"write",
|
|
@@ -639,6 +696,9 @@
|
|
|
639
696
|
"quoxchat widgets":"read",
|
|
640
697
|
"reach agent":"read",
|
|
641
698
|
"reach unused":"read",
|
|
699
|
+
"research domains add":"write",
|
|
700
|
+
"research domains list":"read",
|
|
701
|
+
"research domains remove":"write",
|
|
642
702
|
"role agent clear":"write",
|
|
643
703
|
"role agent list":"read",
|
|
644
704
|
"role agent set":"write",
|
|
@@ -698,6 +758,7 @@
|
|
|
698
758
|
"seo gaps":"read",
|
|
699
759
|
"seo health":"read",
|
|
700
760
|
"seo keywords":"read",
|
|
761
|
+
"seo queries":"read",
|
|
701
762
|
"seo remove-site":"write",
|
|
702
763
|
"seo report deliver":"write",
|
|
703
764
|
"seo sites":"read",
|
|
@@ -710,6 +771,12 @@
|
|
|
710
771
|
"server config lan-targets":"write",
|
|
711
772
|
"server diag":"write",
|
|
712
773
|
"server doctor":"read",
|
|
774
|
+
"server go":"never",
|
|
775
|
+
"server list":"read",
|
|
776
|
+
"server mark":"write",
|
|
777
|
+
"server roles list":"read",
|
|
778
|
+
"server roles set":"write",
|
|
779
|
+
"server roles unset":"write",
|
|
713
780
|
"server rollback":"write",
|
|
714
781
|
"server status":"read",
|
|
715
782
|
"server uninstall":"never",
|
|
@@ -730,6 +797,8 @@
|
|
|
730
797
|
"setup license link":"write",
|
|
731
798
|
"setup license status":"read",
|
|
732
799
|
"setup status":"read",
|
|
800
|
+
"shortcut add":"write",
|
|
801
|
+
"shortcut remove":"write",
|
|
733
802
|
"signup":"never",
|
|
734
803
|
"sitrep":"read",
|
|
735
804
|
"skills disclosures":"read",
|
|
@@ -945,4 +1014,10 @@
|
|
|
945
1014
|
"x search":"write",
|
|
946
1015
|
"x status":"read",
|
|
947
1016
|
"x thread":"write",
|
|
948
|
-
"x timeline":"write"
|
|
1017
|
+
"x timeline":"write",
|
|
1018
|
+
"youtube analytics":"write",
|
|
1019
|
+
"youtube channel":"write",
|
|
1020
|
+
"youtube comment":"write",
|
|
1021
|
+
"youtube thumbnail":"write",
|
|
1022
|
+
"youtube upload":"write",
|
|
1023
|
+
"youtube videos":"read"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /evidence: export this conversation's signed evidence bundle (the same call as `quox evidence bundle`) to
|
|
3
|
+
* ~/quox-evidence/<conversation>-<timestamp>.zip and say what is in it. /ward lists the WARD receipts the export found
|
|
4
|
+
* (newest ten, then N more). /envelope, /envelopes and /aee are pointers to /proof (the envelope ids); /volt is the
|
|
5
|
+
* VOLT run, which travels inside the bundle. Pure formatting plus one api call; the lines are { text, tone }.
|
|
6
|
+
*/
|
|
7
|
+
import { wrapHang } from '../objects/model.js';
|
|
8
|
+
export const EVIDENCE_ALIASES = ['envelope', 'envelopes', 'aee', 'volt', 'ward'];
|
|
9
|
+
const SHOWN = 10;
|
|
10
|
+
|
|
11
|
+
export async function runEvidenceCommand({ cmd, api, conversationId, cols = Infinity }) {
|
|
12
|
+
const W = Number.isFinite(cols) ? Math.max(20, cols - 4) : 1e9;
|
|
13
|
+
const wrapT = (l) => wrapHang(l.text, W, 4).map(text => ({ ...l, text }));
|
|
14
|
+
if (!api || typeof api.evidenceExport !== 'function') return [{ text: 'Evidence export is not available in this mode. In a terminal: quox evidence bundle', tone: 'warn' }].flatMap(wrapT);
|
|
15
|
+
if (!conversationId) return [{ text: 'No conversation to export yet: send a message first, or in a terminal run quox evidence bundle <conversation id>.', tone: 'warn' }].flatMap(wrapT);
|
|
16
|
+
let r;
|
|
17
|
+
try { r = await api.evidenceExport(conversationId); } catch (e) {
|
|
18
|
+
return [{ text: `Evidence export failed: ${e?.message || 'failed'}. Nothing was written.`, tone: 'warn' }].flatMap(wrapT);
|
|
19
|
+
}
|
|
20
|
+
const out = [];
|
|
21
|
+
const ok = r.verify && r.verify.ok;
|
|
22
|
+
out.push({ text: `${ok ? '✔' : '✖'} evidence bundle ${ok ? 'written and verified' : 'written but FAILED verification'}: ${r.path}`, tone: ok ? 'ok' : 'warn' });
|
|
23
|
+
out.push({ text: ` ${r.envelopes} envelopes, ${r.receipts} WARD receipts, conversation ${r.corr}`, tone: 'dim' });
|
|
24
|
+
if (cmd === '/ward') {
|
|
25
|
+
out.push({ text: ' WARD receipts for this conversation (the same bundle /evidence writes):', tone: 'dim' });
|
|
26
|
+
const rows = [...(r.receiptRows || [])].sort((a, b) => (b.seq || 0) - (a.seq || 0));
|
|
27
|
+
for (const x of rows.slice(0, SHOWN)) out.push({ text: ` WARD seq ${x.seq} envelope ${x.envelope_id}`, tone: 'dim' });
|
|
28
|
+
if (rows.length > SHOWN) out.push({ text: ` ${rows.length - SHOWN} more in the bundle`, tone: 'dim' });
|
|
29
|
+
if (!rows.length) out.push({ text: ' No WARD receipt was found for this conversation\'s envelopes yet (witnessing can lag a few seconds).', tone: 'warn' });
|
|
30
|
+
}
|
|
31
|
+
if (cmd === '/volt') out.push({ text: ' The VOLT run (if this conversation has one) is volt.json inside the bundle.', tone: 'dim' });
|
|
32
|
+
out.push({ text: ` check it offline: quox evidence verify ${r.path}`, tone: 'dim' });
|
|
33
|
+
return out.flatMap(wrapT);
|
|
34
|
+
}
|
|
@@ -96,7 +96,7 @@ export function filterPalette(items, query) {
|
|
|
96
96
|
}
|
|
97
97
|
if (best == null) return;
|
|
98
98
|
if (exact(it)) best = -1;
|
|
99
|
-
if (it.disabled) best += 0.5; // a
|
|
99
|
+
if (it.disabled) best += best >= 10 ? 1000 : 0.5; // a non-prefix match on an unavailable command ranks below every working match; a prefix keeps its place on a tie
|
|
100
100
|
scored.push({ it, best, idx });
|
|
101
101
|
});
|
|
102
102
|
return scored.sort((a, b) => a.best - b.best || a.idx - b.idx).map(x => x.it);
|