quoxcli 0.4.0 → 0.6.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 +110 -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/governance.js +7 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +58 -0
- package/lib/api/endpoints/index.js +4 -0
- package/lib/api/endpoints/lucid.js +17 -0
- package/lib/api/endpoints/mcp.js +35 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/notesq.js +2 -0
- package/lib/api/endpoints/quoxplan.js +51 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/actions.js +51 -13
- 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/contacts.js +2 -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 +7 -0
- package/lib/commands/lucidGoals.js +389 -0
- package/lib/commands/mail.js +427 -25
- package/lib/commands/mcp.js +190 -0
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +69 -144
- package/lib/commands/monitoring.js +68 -0
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +617 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +52 -12
- package/lib/commands/plan.js +487 -52
- 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/volt.js +6 -1
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +880 -0
- package/lib/commands/wazuhIntake.js +358 -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 +9 -0
- package/lib/core/config.js +42 -3
- 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 +120 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +30 -5
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/selfUpdate.js +2 -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 +189 -34
- package/lib/quoxon/app.js +1277 -188
- 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 +72 -12
- package/lib/quoxon/ask.js +4 -2
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +8 -2
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +8 -1
- package/lib/quoxon/commands.js +42 -12
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/delegationDragon.js +49 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +118 -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 +55 -8
- package/lib/quoxon/extensions.js +400 -38
- package/lib/quoxon/foreignText.js +49 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/launcher.js +2 -1
- package/lib/quoxon/loopCard.js +7 -2
- 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 +27 -6
- 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/dockerOffer.js +3 -1
- 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 +68 -16
- package/lib/quoxon/resend.js +14 -0
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +3 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +88 -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 +48 -11
- package/lib/quoxon/slash/broadcastRun.js +24 -7
- package/lib/quoxon/slash/cliLeaves.json +98 -1
- package/lib/quoxon/slash/dashboard.js +2 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/listView.js +2 -1
- package/lib/quoxon/slash/palette.js +63 -1
- package/lib/quoxon/slash/paletteController.js +15 -5
- package/lib/quoxon/slash/restart.js +9 -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 +24 -3
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/slotRun.js +2 -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/webPty.js +15 -0
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +42 -0
- package/lib/render/planBrief.js +614 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +61 -3
- 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/commands/provider.js
CHANGED
|
@@ -58,17 +58,19 @@ export function registerProviderCommands(program, ctx) {
|
|
|
58
58
|
// ── provider list ──────────────────────────────────────────────────────────
|
|
59
59
|
provider
|
|
60
60
|
.command('list')
|
|
61
|
-
.description('Show which AI providers this org can use, and how each is connected')
|
|
62
|
-
.
|
|
61
|
+
.description('Show which AI providers this org can use, and how each is connected (--all adds the other integrations in the vault)')
|
|
62
|
+
.option('--all', 'also list the other integrations in the vault (Slack, GitHub, bastion...), not just AI providers')
|
|
63
|
+
.action(async (opts = {}) => {
|
|
63
64
|
auth.requireAuth();
|
|
64
65
|
const a = api();
|
|
65
66
|
const [vault, caps] = await Promise.all([a.connections(), a.capabilities ? a.capabilities() : null]);
|
|
66
67
|
if (program.opts().output === 'json') {
|
|
67
|
-
const
|
|
68
|
+
const known = new Set(Object.values(PROVIDERS).map((p) => p.type));
|
|
69
|
+
const rows = vault?.ok ? vault.rows.filter((r) => opts.all || known.has(r.integration_type)).map((r) => Object.fromEntries(SAFE_ROW.filter((k) => r[k] !== undefined).map((k) => [k, r[k]]))) : null;
|
|
68
70
|
formatDetail({ connections: rows, ...(vault?.ok ? {} : { error: vault?.error || 'vault not readable' }), capabilities: caps?.ok ? caps.capabilities : null }, { format: 'json' });
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
|
-
say(connectionLines(vault, caps));
|
|
73
|
+
say(connectionLines(vault, caps, { includeOthers: Boolean(opts.all) }));
|
|
72
74
|
});
|
|
73
75
|
|
|
74
76
|
// ── provider connect ───────────────────────────────────────────────────────
|
|
@@ -202,16 +204,35 @@ export function registerProviderCommands(program, ctx) {
|
|
|
202
204
|
const rows = vault.rows.filter((r) => types.has(r.integration_type) && (r.is_default === 1 || r.is_default === true));
|
|
203
205
|
if (!rows.length) throw new Error(p ? `No ${p.id} connection is stored for this org. Connect one: quox provider connect ${p.id}` : 'No provider connection is stored for this org. Connect one: quox provider connect <kind>');
|
|
204
206
|
const orgId = resolveOrgId(auth, program);
|
|
207
|
+
const asJson = program.opts().output === 'json';
|
|
208
|
+
const results = [];
|
|
205
209
|
let failed = 0;
|
|
206
210
|
for (const row of rows) {
|
|
207
|
-
const
|
|
211
|
+
const meta = Object.values(PROVIDERS).find((x) => x.type === row.integration_type) || {};
|
|
212
|
+
const name = meta.name || row.integration_type;
|
|
208
213
|
const ep = resolveEndpoint('vault.test', { orgId, id: row.id });
|
|
209
|
-
let res;
|
|
214
|
+
let res; let reason = null;
|
|
210
215
|
try { res = await client.post(ep.path, { service: ep.service, retries: 0 }); }
|
|
211
|
-
catch (e) {
|
|
212
|
-
|
|
216
|
+
catch (e) {
|
|
217
|
+
res = { success: false, message: e?.message || 'failed' };
|
|
218
|
+
reason = e?.body?.reason || (String(e?.message || '').match(/\(reason: ([^)]+)\)/) || [])[1] || null;
|
|
219
|
+
}
|
|
220
|
+
const ok = res?.success !== false;
|
|
221
|
+
if (!ok) failed++;
|
|
222
|
+
results.push({
|
|
223
|
+
id: row.id, kind: meta.id || row.integration_type, label: row.label || row.id, ok,
|
|
224
|
+
...(ok ? (res?.message ? { message: res.message } : {}) : { error: res?.message || 'no reason given', reason }),
|
|
225
|
+
});
|
|
226
|
+
if (asJson) continue;
|
|
227
|
+
if (!ok) printInfo(`✖ ${name} ${row.label || row.id} failed: ${res?.message || 'no reason given'}`);
|
|
213
228
|
else printInfo(`✔ ${name} ${row.label || row.id} ok${res?.message ? `: ${res.message}` : ''}`);
|
|
214
229
|
}
|
|
230
|
+
if (asJson) formatDetail({
|
|
231
|
+
results,
|
|
232
|
+
summary: { total: results.length, ok: results.length - failed, failed },
|
|
233
|
+
// a top-level error string is what the collector's quox_cli wrapper quotes to the model on a non-zero exit
|
|
234
|
+
...(failed ? { error: results.filter((r) => !r.ok).map((r) => `${r.kind}: ${r.error}`).join('; ') } : {}),
|
|
235
|
+
}, { format: 'json' });
|
|
215
236
|
if (failed) process.exitCode = 1;
|
|
216
237
|
});
|
|
217
238
|
}
|
package/lib/commands/qlarity.js
CHANGED
|
@@ -49,6 +49,25 @@ function annotate(err) {
|
|
|
49
49
|
throw err;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Inspectors whose server-side run() calls assertSelector (qlarity/src/inspectors.js).
|
|
54
|
+
* The registry lives in another repo and `inspectors list` does not expose the
|
|
55
|
+
* flag, so this mirrors it; storage, dom_summary, layout_health and
|
|
56
|
+
* responsive_sweep default or skip the selector and are deliberately absent.
|
|
57
|
+
*/
|
|
58
|
+
export const SELECTOR_INSPECTORS = ['computed_style', 'pseudo_style', 'text_content', 'attribute', 'count', 'bounding_box', 'expect'];
|
|
59
|
+
|
|
60
|
+
/** Keep scheme+host+port only (lowercased); anything unparseable goes through for the server to explain. */
|
|
61
|
+
export function normaliseOrigin(input) {
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(String(input).trim());
|
|
64
|
+
if (!/^https?:$/.test(u.protocol)) return input;
|
|
65
|
+
return u.origin;
|
|
66
|
+
} catch {
|
|
67
|
+
return input;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
// quox#907: a browser login is slower than a read; well above the client's 30s default.
|
|
53
72
|
const LOGIN_TIMEOUT_MS = 120000;
|
|
54
73
|
|
|
@@ -308,14 +327,21 @@ export function registerQlarityCommands(program, ctx) {
|
|
|
308
327
|
al.command('add <origin>')
|
|
309
328
|
.description('Declare an origin this org may inspect, e.g. https://staging.example.com')
|
|
310
329
|
.option('--note <text>', 'why it is allowed, shown alongside it later')
|
|
311
|
-
.action(async (
|
|
330
|
+
.action(async (rawOrigin, opts) => {
|
|
331
|
+
const origin = normaliseOrigin(rawOrigin);
|
|
312
332
|
try {
|
|
333
|
+
if (origin !== rawOrigin) process.stdout.write(`allowlisting ${origin} (scheme+host+port of ${rawOrigin})\n`);
|
|
313
334
|
const d = await call(client, 'qlarity.orgAllowlist.add', {}, { origin, note: opts.note });
|
|
314
335
|
process.stdout.write(`\u{1F50D} QLARITY: added ${d.entry.origin}\n`);
|
|
315
336
|
process.stdout.write(` ${d.origins.length} origin(s) now declared for this org\n`);
|
|
316
337
|
} catch (err) {
|
|
317
338
|
// A refusal here is usually a wildcard or a typo, and the server sends
|
|
318
339
|
// the reason. Surface it instead of a bare 400.
|
|
340
|
+
if (err?.status === 403) {
|
|
341
|
+
process.stderr.write(`Not added: adding an origin is an org-admin action by design (an agent cannot self-authorize egress).\nRetry as an org admin: quox -p <admin-profile> qlarity origins add ${origin}\n`);
|
|
342
|
+
process.exitCode = 4;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
319
345
|
if (err?.status === 400 && err?.message) {
|
|
320
346
|
process.stderr.write(`Not added: ${err.message}\n`);
|
|
321
347
|
process.exitCode = 1;
|
|
@@ -395,6 +421,11 @@ export function registerQlarityCommands(program, ctx) {
|
|
|
395
421
|
.option('--session <id>', 'reuse an existing session (e.g. one left open by `quox qlarity login`) instead of opening a fresh one')
|
|
396
422
|
.option('--json', 'raw JSON output')
|
|
397
423
|
.action(async (url, options) => {
|
|
424
|
+
if (SELECTOR_INSPECTORS.includes(options.inspector) && !options.selector) {
|
|
425
|
+
process.stderr.write(`${options.inspector} needs --selector. Try: quox qlarity read ${url} --inspector ${options.inspector} --selector 'body'\n`);
|
|
426
|
+
process.exitCode = 2;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
398
429
|
let sessionId = null;
|
|
399
430
|
let reused = false;
|
|
400
431
|
try {
|
package/lib/commands/run.js
CHANGED
|
@@ -39,7 +39,8 @@ export function registerRunCommands(program, ctx) {
|
|
|
39
39
|
let items = data.data || data.items || (Array.isArray(data) ? data : []);
|
|
40
40
|
// an older engine that ignores the filter must still never show another workflow's runs
|
|
41
41
|
if (options.workflow) items = items.filter((it) => (it.workflowId ?? it.workflow_id) === options.workflow);
|
|
42
|
-
rows
|
|
42
|
+
// quoxflow rows are camelCase (workflowName, startedAt, completedAt); the table reads snake_case columns.
|
|
43
|
+
rows = items.map((item) => ({ ...item, workflow_name: item.workflow_name ?? item.workflowName, started_at: item.started_at ?? item.startedAt, finished_at: item.finished_at ?? item.completedAt ?? item.finishedAt, source: 'engine' }));
|
|
43
44
|
source = 'engine';
|
|
44
45
|
} catch (err) {
|
|
45
46
|
printWarning(`Engine (quoxflow) execution list failed, falling back to the auth-service store: ${err.message}`);
|
package/lib/commands/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { planUninstall, runUninstall } from '../server/uninstall.js';
|
|
|
24
24
|
import { serverChecks } from '../server/doctor.js';
|
|
25
25
|
import { writeDiagBundle } from '../server/diag.js';
|
|
26
26
|
import { latestSnapshot, listSnapshots } from '../server/snapshot.js';
|
|
27
|
+
import { runInstallFlow } from '../server/installFlow.js';
|
|
27
28
|
import { loadRegistry, saveServer, buildRows, planGo, hereIdentity, registryPath } from '../server/devServers.js';
|
|
28
29
|
|
|
29
30
|
const icon = (s) => (s === 'pass' ? chalk.green('✔') : s === 'warn' ? chalk.yellow('⚠') : s === 'skip' ? chalk.dim('·') : chalk.red('✖'));
|
|
@@ -70,6 +71,42 @@ export function registerServerCommands(program, ctx = {}) {
|
|
|
70
71
|
const server = program.command('server').description('Your self-hosted Quox install: status, update, rollback, uninstall, doctor, diag');
|
|
71
72
|
const withDir = (c) => c.option('--dir <path>', 'the install directory (default: the one Getting started recorded)');
|
|
72
73
|
|
|
74
|
+
server.command('install')
|
|
75
|
+
.description('Install Quox on this machine: checks, download, start, owner account, one-time dashboard link (the same installer Getting started runs)')
|
|
76
|
+
.option('--dir <path>', 'where to install (default: ~/quox-install)')
|
|
77
|
+
.option('--dry-run', 'check this machine, print the plan, change nothing')
|
|
78
|
+
.option('-y, --yes', 'skip the confirmation (required without a terminal)')
|
|
79
|
+
.option('--json', 'one JSON object per line: stage events, then a result')
|
|
80
|
+
.option('--email <address>', 'owner account email (asked when there is a terminal)')
|
|
81
|
+
.option('--password-stdin', 'read the owner password from stdin, one line (never an argument)')
|
|
82
|
+
.option('--print-login-link', 'print the live one-time sign-in link (default: only on an interactive terminal, never in --json)')
|
|
83
|
+
.action(async (o) => {
|
|
84
|
+
const asJson = Boolean(o.json) || json();
|
|
85
|
+
const human = (e) => {
|
|
86
|
+
if (e.type === 'log') { console.log(chalk.dim(` ${e.line}`)); return; }
|
|
87
|
+
if (e.type === 'stage') {
|
|
88
|
+
if (e.status === 'running') console.log(` ${chalk.cyan('◐')} ${e.stage}`);
|
|
89
|
+
else if (e.status === 'ok') console.log(` ${chalk.green('✔')} ${e.stage}${e.detail ? chalk.dim(` ${e.detail}`) : ''}`);
|
|
90
|
+
else if (e.status === 'skip') console.log(` ${chalk.dim('·')} ${e.stage} skipped${e.detail ? `: ${e.detail}` : ''}${e.next ? `. ${e.next}` : ''}`);
|
|
91
|
+
else if (e.status === 'fail') { console.log(chalk.red(` ✖ ${e.stage} failed: ${e.error}`)); console.log(` Next: ${e.next}`); }
|
|
92
|
+
} else if (e.type === 'plan') {
|
|
93
|
+
console.log(chalk.bold(`\n Plan: install Quox into ${e.dir || '~/quox-install'}`));
|
|
94
|
+
for (const c of e.checks) console.log(` ${c.status === 'fail' ? chalk.red('✖') : c.status === 'warn' ? chalk.yellow('⚠') : chalk.green('✔')} ${c.id.padEnd(8)} ${c.body}`);
|
|
95
|
+
console.log(' It will:'); e.steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
|
|
96
|
+
console.log(chalk.dim('\n Dry run: nothing was changed. Run quox server install to install.\n'));
|
|
97
|
+
} else if (e.type === 'result' && e.ok && !e.dryRun) {
|
|
98
|
+
console.log(chalk.green('\n ✔ Your Quox is running.'));
|
|
99
|
+
if (e.link) console.log(` Dashboard sign-in link (single use, 10 minutes): ${e.link}`);
|
|
100
|
+
else console.log(` A one-time sign-in link was issued but not printed (stdout is not a terminal). Get one with: ${e.linkHow}`);
|
|
101
|
+
console.log(chalk.dim(' Your browser will warn about the certificate once: it is the self-signed one this install made.\n'));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const emit = (e) => (asJson ? console.log(JSON.stringify(e)) : human(e));
|
|
105
|
+
const r = await runInstallFlow({ dir: o.dir, dryRun: dry(o), yes: yes(o), email: o.email, passwordStdin: Boolean(o.passwordStdin), printLink: Boolean(o.printLoginLink) || (!asJson && Boolean(process.stdout.isTTY)) }, deps.installFlowDeps || {}, emit);
|
|
106
|
+
if (!r.ok && !r.dryRun) process.exitCode = r.declined ? 1 : 2;
|
|
107
|
+
if (!r.ok && r.dryRun === undefined && r.failedStage && dry(o)) process.exitCode = 2;
|
|
108
|
+
});
|
|
109
|
+
|
|
73
110
|
withDir(server.command('status').description('Version, health per service, disk, ports and the last update (read-only)'))
|
|
74
111
|
.action(async (o) => {
|
|
75
112
|
const i = await install(o);
|
package/lib/commands/setup.js
CHANGED
|
@@ -8,6 +8,9 @@ import { resolveEndpoint } from '../api/endpoints.js';
|
|
|
8
8
|
import { QuoxApiError, QuoxValidationError } from '../core/errors.js';
|
|
9
9
|
import { confirmDestructive } from '../core/confirm.js';
|
|
10
10
|
import { passwordProblem } from '../quoxon/onboarding/finish.js';
|
|
11
|
+
import {
|
|
12
|
+
renderLinkCard, renderLinkedCard, linkedDetails, startWaiting, colorFor, animateFor,
|
|
13
|
+
} from './licenceLinkView.js';
|
|
11
14
|
|
|
12
15
|
// The auth service's rule (services/auth/utils/passwordValidation.js), said up front.
|
|
13
16
|
export const PASSWORD_POLICY = 'Passwords need at least 12 characters, with an uppercase letter, a lowercase letter, a number and a special character (for example - or !).';
|
|
@@ -348,93 +351,142 @@ export function registerSetupCommands(program, ctx) {
|
|
|
348
351
|
interval,
|
|
349
352
|
});
|
|
350
353
|
|
|
354
|
+
// QUOXON (owner 2026-11-05): a framed card with the code, the full
|
|
355
|
+
// link and a countdown, then a live waiting line on a TTY. Not a TTY
|
|
356
|
+
// (CI, logs): plain lines, no ANSI, nothing animated.
|
|
357
|
+
const term = ctx.term || process.stdout;
|
|
358
|
+
const format = program.opts().output;
|
|
359
|
+
const pretty = !json && (format === undefined || format === 'table');
|
|
360
|
+
const color = pretty && colorFor(term);
|
|
361
|
+
const verificationUrl = started.verification_url || 'https://quox.ai/link';
|
|
362
|
+
let waiting = null;
|
|
351
363
|
if (!json) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
364
|
+
if (pretty) {
|
|
365
|
+
console.log(renderLinkCard({
|
|
366
|
+
userCode: started.user_code, url: verificationUrl, remainingMs: expiresIn * 1000, color, cols: term.columns || 80,
|
|
367
|
+
}));
|
|
368
|
+
if (term.isTTY) {
|
|
369
|
+
waiting = startWaiting({ term, deadline, totalMs: expiresIn * 1000, color, animate: animateFor(term) });
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
printInfo(`Open ${verificationUrl} and enter this code:`);
|
|
373
|
+
console.log('');
|
|
374
|
+
console.log(chalk.bold.cyan(` ${started.user_code}`));
|
|
375
|
+
console.log('');
|
|
376
|
+
printInfo(`The code is good for ${Math.round(expiresIn / 60)} minutes. Waiting for approval...`);
|
|
377
|
+
}
|
|
357
378
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
379
|
+
try {
|
|
380
|
+
await pollUntilDone();
|
|
381
|
+
} finally {
|
|
382
|
+
if (waiting) waiting.stop();
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
|
|
386
|
+
async function pollUntilDone() {
|
|
387
|
+
const pollEp = resolveEndpoint('setup.licensing.device.poll');
|
|
388
|
+
let lastState = 'started';
|
|
389
|
+
|
|
390
|
+
while (Date.now() < deadline) {
|
|
391
|
+
await sleepFn(interval * 1000);
|
|
392
|
+
|
|
393
|
+
let result;
|
|
394
|
+
try {
|
|
395
|
+
result = await client.post(pollEp.path, {
|
|
396
|
+
service: pollEp.service,
|
|
397
|
+
body: { device_code: deviceCode, interval },
|
|
398
|
+
});
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// 503 means quox.ai blipped, NOT that the activation died: the code
|
|
401
|
+
// is still live upstream and the customer may still be approving it.
|
|
402
|
+
// Anything else (409 seat_limit_reached, 403, 400) is terminal.
|
|
403
|
+
if (err instanceof QuoxApiError && err.status === 503) {
|
|
404
|
+
if (lastState !== 'unreachable') {
|
|
405
|
+
emit({ state: 'unreachable', message: err.message });
|
|
406
|
+
if (waiting) waiting.note('licence server unreachable, still waiting');
|
|
407
|
+
else if (!json) printInfo('The licence server is unreachable right now. Still waiting...');
|
|
408
|
+
lastState = 'unreachable';
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
380
411
|
}
|
|
381
|
-
|
|
412
|
+
throw enrichLicenseError(err);
|
|
382
413
|
}
|
|
383
|
-
throw enrichLicenseError(err);
|
|
384
|
-
}
|
|
385
414
|
|
|
386
|
-
|
|
415
|
+
const state = result.state || 'pending';
|
|
387
416
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
if (state === 'slow_down') {
|
|
418
|
+
interval = Number(result.interval) > 0 ? Number(result.interval) : interval + 5;
|
|
419
|
+
} else if (Number(result.interval) > 0) {
|
|
420
|
+
interval = Number(result.interval);
|
|
421
|
+
}
|
|
393
422
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
423
|
+
if (state !== lastState) {
|
|
424
|
+
emit({ state, interval, ...(result.error ? { error: result.error } : {}) });
|
|
425
|
+
lastState = state;
|
|
426
|
+
}
|
|
398
427
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
428
|
+
if (state === 'approved') {
|
|
429
|
+
const lease = result.lease || {};
|
|
430
|
+
// QUOXON (owner 2026-11-05): read the licence back through the
|
|
431
|
+
// route `quox licence status` uses, so the run NAMES what was
|
|
432
|
+
// linked (plan, plugins, seats). A failed read never hides the
|
|
433
|
+
// activation: the lease is the fallback, and the failure is said.
|
|
434
|
+
let status = null;
|
|
435
|
+
let readBackError = null;
|
|
436
|
+
if (json || pretty) {
|
|
437
|
+
try {
|
|
438
|
+
const readEp = resolveEndpoint('setup.licensing.entitlements.list');
|
|
439
|
+
status = await client.get(readEp.path, { service: readEp.service });
|
|
440
|
+
} catch (err) {
|
|
441
|
+
readBackError = err && err.message ? err.message : String(err);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const details = linkedDetails(lease, status, result);
|
|
445
|
+
// --json: the pre-existing { state, lease } plus an ADDITIVE `licence`.
|
|
446
|
+
emit({ state: 'approved', lease, licence: details });
|
|
447
|
+
if (waiting) { waiting.stop(); waiting = null; }
|
|
448
|
+
if (pretty) {
|
|
449
|
+
console.log(renderLinkedCard(details, {
|
|
450
|
+
color, cols: term.columns || 80, leaseExpiresAt: lease.expires_at || null,
|
|
451
|
+
profile: program.opts().profile || null, readBackError,
|
|
452
|
+
}));
|
|
453
|
+
} else if (!json) {
|
|
454
|
+
formatDetail({
|
|
455
|
+
license_id: lease.license_id || null,
|
|
456
|
+
entitlements: lease.entitlements || [],
|
|
457
|
+
lease_expires_at: lease.expires_at || null,
|
|
458
|
+
}, { format: program.opts().output, title: 'Licence activated' });
|
|
459
|
+
printSuccess('This instance is activated. No key was ever pasted into it.');
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
409
462
|
}
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
463
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
464
|
+
if (state === 'denied') {
|
|
465
|
+
throw new QuoxApiError(403, 'access_denied',
|
|
466
|
+
'The activation was denied on quox.ai. Nothing was activated on this instance.');
|
|
467
|
+
}
|
|
468
|
+
if (state === 'expired') {
|
|
469
|
+
throw new QuoxApiError(410, 'expired_token',
|
|
470
|
+
'The code expired before it was approved. Run `quox setup license link` again for a fresh one.');
|
|
471
|
+
}
|
|
472
|
+
if (state === 'consumed') {
|
|
473
|
+
throw new QuoxApiError(409, 'consumed',
|
|
474
|
+
'That code was already used. Run `quox setup license link` again for a fresh one.');
|
|
475
|
+
}
|
|
476
|
+
if (state === 'activation_failed') {
|
|
477
|
+
throw new QuoxApiError(502, result.error || 'activation_failed',
|
|
478
|
+
`The approval landed but the licence could not be stored on this instance: ${result.message || result.error || 'unknown error'}. `
|
|
479
|
+
+ 'Run `quox setup license link` again.');
|
|
480
|
+
}
|
|
481
|
+
if (state === 'error') {
|
|
482
|
+
throw new QuoxApiError(502, result.error || 'device_poll_failed',
|
|
483
|
+
result.message || 'Device activation failed.');
|
|
484
|
+
}
|
|
433
485
|
}
|
|
434
|
-
}
|
|
435
486
|
|
|
436
|
-
|
|
437
|
-
|
|
487
|
+
throw new QuoxApiError(410, 'expired_token',
|
|
488
|
+
'The code expired before it was approved. Run `quox setup license link` again for a fresh one.');
|
|
489
|
+
}
|
|
438
490
|
});
|
|
439
491
|
|
|
440
492
|
license
|
package/lib/commands/skills.js
CHANGED
|
@@ -84,6 +84,8 @@ export function registerSkillsCommands(program, ctx) {
|
|
|
84
84
|
for (const it of Array.isArray(items) ? items : []) if (it && it.skill_id) active.set(it.skill_id, it.status || status);
|
|
85
85
|
}
|
|
86
86
|
rows = rows.map((r) => ({ ...r, review_status: active.get(r.id) || null, runnable: !active.has(r.id) }));
|
|
87
|
+
// Runnable first (stable): a skill that can run leads, so the first row is never one skill_view must refuse.
|
|
88
|
+
rows = [...rows.filter((r) => r.runnable), ...rows.filter((r) => !r.runnable)];
|
|
87
89
|
} catch (err) {
|
|
88
90
|
printWarning(`Skill review queue could not be read, so which skills can run is unknown: ${err.message}`);
|
|
89
91
|
}
|
|
@@ -641,7 +643,8 @@ export function registerSkillsCommands(program, ctx) {
|
|
|
641
643
|
try {
|
|
642
644
|
const ep = resolveEndpoint('skills.sealed.create');
|
|
643
645
|
const data = await client.post(ep.path, { service: ep.service, body });
|
|
644
|
-
|
|
646
|
+
// the collector answers { sealed: {...}, evidence }; an older/flat body carries sealedId at the top
|
|
647
|
+
printSuccess(`Sealed. sealedId ${(data && data.sealed && data.sealed.sealedId) || (data && data.sealedId)}`);
|
|
645
648
|
formatDetail(data, { format: program.opts().output, title: 'Sealed skill' });
|
|
646
649
|
printInfo(SEALED_SECTION_G_SENTENCE);
|
|
647
650
|
} catch (err) {
|
package/lib/commands/slot.js
CHANGED
|
@@ -19,6 +19,7 @@ import { dirname, join } from 'node:path';
|
|
|
19
19
|
|
|
20
20
|
import { formatDetail, formatOutput, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
21
21
|
import { events, printEvent } from '../core/quoxglyph.js';
|
|
22
|
+
import { announce, announceOnFail } from '../core/announce.js';
|
|
22
23
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
23
24
|
import { QuoxApiError, QuoxNetworkError } from '../core/errors.js';
|
|
24
25
|
import { resolveOrgId } from './helpers.js';
|
|
@@ -400,6 +401,23 @@ export function registerSlotCommands(program, ctx) {
|
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
// SLOT-FROM: the session name this process holds a slot under (held-slots.json lists it, or QUOX_SLOT
|
|
405
|
+
// names a held slot), sent as holder_session so the server's hold record stamps from_slot even when
|
|
406
|
+
// senderSlot() could not bind a live process. The server re-checks identity + session; never trusted.
|
|
407
|
+
function heldSessionFor(from) {
|
|
408
|
+
try {
|
|
409
|
+
const all = readHeldSlots();
|
|
410
|
+
const want = from ? String(from).toLowerCase() : null;
|
|
411
|
+
const bySlot = process.env.QUOX_SLOT ? all[String(process.env.QUOX_SLOT).trim()] : null;
|
|
412
|
+
if (bySlot && !bySlot.member && bySlot.session && (!want || String(bySlot.session).toLowerCase() === want)) return bySlot.session;
|
|
413
|
+
if (!want) return null;
|
|
414
|
+
const hit = Object.values(all).find((e) => e && !e.member && String(e.session || '').toLowerCase() === want);
|
|
415
|
+
return hit ? hit.session : null;
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
403
421
|
// P10: the collector recognises splinter members only via the ATTACH token.
|
|
404
422
|
// When this Claude session is attached to an agent whose recorded slot is
|
|
405
423
|
// `name`, send that token (same noAuth + Authorization pattern as
|
|
@@ -866,7 +884,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
866
884
|
.option('--session <s>', 'session name to speak as (default: $QUOX_STREAM)')
|
|
867
885
|
.option('--room', 'post as a joined room member (no hold needed); signed "session (person)" by the server')
|
|
868
886
|
.option('--run <command>', 'ask the human to run ONE command in their own terminal (the text is why); the quox TUI shows it in full and runs it only on their y. Never run server-side')
|
|
869
|
-
.action(async (name, textParts, options) => {
|
|
887
|
+
.action(announceOnFail('quoxslots', (name) => `say into ${name}`, async (name, textParts, options) => {
|
|
870
888
|
auth.requireAuth();
|
|
871
889
|
// SLOT-RUN: same rule as the collector (one line, max 2000, no control/bidi characters), checked before sending.
|
|
872
890
|
if (options.run !== undefined) {
|
|
@@ -900,6 +918,8 @@ export function registerSlotCommands(program, ctx) {
|
|
|
900
918
|
// A room post already has its address (the room), so it carries none.
|
|
901
919
|
const fromSlot = options.room ? null : senderSlot(from);
|
|
902
920
|
if (fromSlot) body.from_slot = fromSlot;
|
|
921
|
+
const heldAs = options.room ? null : heldSessionFor(from);
|
|
922
|
+
if (heldAs) body.holder_session = heldAs;
|
|
903
923
|
// A freshly created slot gets its Matrix room a few seconds AFTER
|
|
904
924
|
// checkout returns (provisioning is async), so a say straight after
|
|
905
925
|
// checkout used to fail with 409 "no Matrix room yet" (slots audit
|
|
@@ -952,6 +972,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
952
972
|
throw err;
|
|
953
973
|
}
|
|
954
974
|
}
|
|
975
|
+
announce('quoxslots', `said into ${name}`, { result: data?.presence?.state === 'listening' ? 'delivered, listening' : 'posted' });
|
|
955
976
|
printSuccess(`Said into ${name}.`);
|
|
956
977
|
if (!options.room && !fromSlot) {
|
|
957
978
|
printWarning(`${from || 'This session'} holds no slot, so replies cannot reach it. Claim one: quox slot checkout <name> --create --session ${from || '<session>'}`);
|
|
@@ -962,7 +983,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
962
983
|
reportHumanPresence(data?.human_presence);
|
|
963
984
|
reportBridgeDelivery(data?.bridges);
|
|
964
985
|
if (program.opts().output === 'json') formatDetail(data, { format: 'json' });
|
|
965
|
-
});
|
|
986
|
+
}));
|
|
966
987
|
|
|
967
988
|
// QUOXON-BROADCAST: one call fans a message out to every held slot in the org (or --slots).
|
|
968
989
|
slot
|
|
@@ -972,24 +993,38 @@ export function registerSlotCommands(program, ctx) {
|
|
|
972
993
|
.option('--slots <list>', 'comma-separated slot names instead of every held slot')
|
|
973
994
|
.option('--priority <p>', 'normal or urgent', 'normal')
|
|
974
995
|
.option('--ack', 'ask recipients to acknowledge with /ack <id>')
|
|
996
|
+
.option('--active', 'only sessions listening right now; dormant slots are skipped, not woken')
|
|
997
|
+
.option('--dry-run', 'with --active: print who would be reached and who is skipped dormant, send nothing')
|
|
975
998
|
.option('--session <s>', 'session name to sign as (default: $QUOX_STREAM)')
|
|
976
|
-
.action(async (textParts, options) => {
|
|
999
|
+
.action(announceOnFail('quoxslots', 'broadcast', async (textParts, options) => {
|
|
977
1000
|
auth.requireAuth();
|
|
978
1001
|
const parsed = parseBroadcastArgs([
|
|
979
|
-
options.slots ? `--slots ${options.slots}` : '', `--priority ${options.priority}`, options.ack ? '--ack' : '', textParts.join(' '),
|
|
1002
|
+
options.slots ? `--slots ${options.slots}` : '', `--priority ${options.priority}`, options.ack ? '--ack' : '', options.active ? '--active' : '', options.dryRun ? '--dry-run' : '', textParts.join(' '),
|
|
980
1003
|
].filter(Boolean).join(' '));
|
|
981
1004
|
if (!parsed.ok) throw new Error(parsed.message);
|
|
982
1005
|
const from = options.session || process.env.QUOX_STREAM || null;
|
|
1006
|
+
const bcFromSlot = senderSlot(from);
|
|
1007
|
+
const bcHeldAs = heldSessionFor(from);
|
|
983
1008
|
const ep = resolveEndpoint('v1.slots.broadcast');
|
|
984
1009
|
const data = await client.post(ep.path, {
|
|
985
1010
|
service: ep.service, retries: 0,
|
|
986
|
-
body: { text: parsed.text, priority: parsed.priority, ack: parsed.ack, ...(parsed.slots ? { slots: parsed.slots } : {}), ...(from ? { from } : {}), org_id: resolveOrgId(auth, program) },
|
|
1011
|
+
body: { text: parsed.text, priority: parsed.priority, ack: parsed.ack, ...(parsed.active ? { active: true } : {}), ...(parsed.dryRun ? { dry_run: true } : {}), ...(parsed.slots ? { slots: parsed.slots } : {}), ...(from ? { from } : {}), ...(bcFromSlot ? { from_slot: bcFromSlot } : {}), ...(bcHeldAs ? { holder_session: bcHeldAs } : {}), org_id: resolveOrgId(auth, program) },
|
|
987
1012
|
});
|
|
988
1013
|
if (program.opts().output === 'json') return formatDetail(data, { format: 'json' });
|
|
1014
|
+
if (data?.dry_run) { // BROADCAST-ACTIVE --dry-run: nothing was sent
|
|
1015
|
+
const would = data.would_reach || []; const dorm = data.skipped_dormant || [];
|
|
1016
|
+
announce('quoxslots', 'broadcast to active', { result: `${would.length} would reach \u00B7 ${dorm.length} skipped dormant` });
|
|
1017
|
+
if (would.length) printInfo(` would reach: ${would.join(', ')}`);
|
|
1018
|
+
if (dorm.length) printInfo(` skipped dormant: ${dorm.join(', ')}`);
|
|
1019
|
+
return printInfo('Dry run: nothing was sent.');
|
|
1020
|
+
}
|
|
1021
|
+
if (parsed.active && data?.skipped_dormant) {
|
|
1022
|
+
announce('quoxslots', 'broadcast to active', { result: `${data.counts?.delivered || 0} reached \u00B7 ${data.skipped_dormant.length} skipped dormant` });
|
|
1023
|
+
} else announce('quoxslots', 'broadcast sent', { result: data?.id || data?.broadcast?.id });
|
|
989
1024
|
for (const [line, kind] of broadcastSummary(data)) (kind === 'warn' ? printWarning : printInfo)(line);
|
|
990
1025
|
reportHumanPresence(data?.human_presence);
|
|
991
1026
|
return undefined;
|
|
992
|
-
});
|
|
1027
|
+
}));
|
|
993
1028
|
|
|
994
1029
|
slot
|
|
995
1030
|
.command('broadcast-status')
|
|
@@ -1052,12 +1087,14 @@ export function registerSlotCommands(program, ctx) {
|
|
|
1052
1087
|
}
|
|
1053
1088
|
function reportBridgeDelivery(bridges) {
|
|
1054
1089
|
if (!Array.isArray(bridges) || bridges.length === 0) return;
|
|
1055
|
-
|
|
1056
|
-
|
|
1090
|
+
// Two bridges that both report "still sending" with no platform/target printed two identical "bridge:" lines;
|
|
1091
|
+
// number them so each line says which bridge it is about.
|
|
1092
|
+
bridges.forEach((b, i) => {
|
|
1093
|
+
const where = b.target ? `${b.platform} (${b.target})` : b.platform || (bridges.length > 1 ? `bridge ${i + 1} of ${bridges.length}` : 'bridge');
|
|
1057
1094
|
if (b.ok === true) printInfo(` rendered to ${where}`);
|
|
1058
1095
|
else if (b.ok === null) printInfo(` ${where}: still sending, check \`quox slot read\``);
|
|
1059
1096
|
else printWarning(` NOT rendered to ${where}: ${b.reason}`);
|
|
1060
|
-
}
|
|
1097
|
+
});
|
|
1061
1098
|
}
|
|
1062
1099
|
|
|
1063
1100
|
async function readMessages(name, options) {
|
|
@@ -1134,6 +1171,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
1134
1171
|
let cursor = options.since || null;
|
|
1135
1172
|
let netFailures = 0;
|
|
1136
1173
|
let authSince = null; // start of the current run of consecutive 401/403s
|
|
1174
|
+
let downSince = null; // start of the current outage, for the single resume line
|
|
1137
1175
|
|
|
1138
1176
|
for (;;) {
|
|
1139
1177
|
const query = { org_id: resolveOrgId(auth, program), timeout_ms: 50000 };
|
|
@@ -1145,6 +1183,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
1145
1183
|
let data;
|
|
1146
1184
|
try {
|
|
1147
1185
|
data = await client.get(ep.path, { service: ep.service, ...attachAuthFor(name), query, timeout: 65000 });
|
|
1186
|
+
if (downSince !== null) { printInfo(`reconnected after ${Math.round((Date.now() - downSince) / 1000)}s`); downSince = null; }
|
|
1148
1187
|
netFailures = 0;
|
|
1149
1188
|
authSince = null;
|
|
1150
1189
|
} catch (err) {
|
|
@@ -1182,6 +1221,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
1182
1221
|
authSince = null;
|
|
1183
1222
|
}
|
|
1184
1223
|
netFailures += 1;
|
|
1224
|
+
if (downSince === null) downSince = Date.now();
|
|
1185
1225
|
let delay = Math.min(2000 * 2 ** Math.min(netFailures - 1, 5), 60000);
|
|
1186
1226
|
delay = Math.round(delay * (0.75 + Math.random() * 0.5));
|
|
1187
1227
|
if (Number.isFinite(err.retryAfterMs)) delay = Math.min(Math.max(delay, err.retryAfterMs), 300000);
|