quoxcli 0.0.0-stage → 0.2.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/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +72 -4
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatter for QuoxCLI
|
|
3
|
+
*
|
|
4
|
+
* All commands pass results through this module.
|
|
5
|
+
* Supports: table, json, csv, quiet output modes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import Table from 'cli-table3';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format and print tabular data
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} result - { columns, headers, rows } or array of objects
|
|
15
|
+
* @param {Object} options - { format, title, emptyMessage }
|
|
16
|
+
*/
|
|
17
|
+
export function formatOutput(result, options = {}) {
|
|
18
|
+
const format = options.format || getOutputFormat();
|
|
19
|
+
const rows = result.rows || result;
|
|
20
|
+
const columns = result.columns || (rows.length > 0 ? Object.keys(rows[0]) : []);
|
|
21
|
+
const headers = result.headers || columns;
|
|
22
|
+
|
|
23
|
+
// options.raw: the untransformed API records behind the display rows.
|
|
24
|
+
// --json exists for scripting, and scripts want DATA keys (name, state,
|
|
25
|
+
// holder_session), not the humanised table cells (NAME, HOLDER, "2d") the
|
|
26
|
+
// display rows carry -- `quox --json slot list` emitting NAME/STATE bit a
|
|
27
|
+
// live harness on 2026-08-20. Table/csv/quiet still render the display
|
|
28
|
+
// rows; json prefers raw when the caller provides it.
|
|
29
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
30
|
+
if (format === 'json') {
|
|
31
|
+
console.log(JSON.stringify(Array.isArray(options.raw) ? options.raw : []));
|
|
32
|
+
} else if (format === 'csv') {
|
|
33
|
+
console.log(headers.join(','));
|
|
34
|
+
} else if (format !== 'quiet') {
|
|
35
|
+
console.log(chalk.dim(options.emptyMessage || 'No results.'));
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
switch (format) {
|
|
41
|
+
case 'json':
|
|
42
|
+
console.log(JSON.stringify(Array.isArray(options.raw) ? options.raw : rows, null, 2));
|
|
43
|
+
break;
|
|
44
|
+
case 'csv':
|
|
45
|
+
formatCsv(rows, columns, headers);
|
|
46
|
+
break;
|
|
47
|
+
case 'quiet':
|
|
48
|
+
formatQuiet(rows, columns);
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
formatTable(rows, columns, headers, options);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format and print a single object (detail view)
|
|
57
|
+
*/
|
|
58
|
+
export function formatDetail(obj, options = {}) {
|
|
59
|
+
const format = options.format || getOutputFormat();
|
|
60
|
+
|
|
61
|
+
if (format === 'json') {
|
|
62
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (format === 'quiet') {
|
|
67
|
+
console.log(obj.id || obj.name || '');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Key-value display
|
|
72
|
+
const fields = options.fields || Object.keys(obj);
|
|
73
|
+
const labels = options.labels || {};
|
|
74
|
+
|
|
75
|
+
if (options.title) {
|
|
76
|
+
console.log(chalk.bold(options.title));
|
|
77
|
+
console.log(chalk.dim('─'.repeat(Math.min(options.title.length + 10, 60))));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const maxLabel = Math.max(...fields.map(f => (labels[f] || f).length));
|
|
81
|
+
|
|
82
|
+
for (const field of fields) {
|
|
83
|
+
const label = (labels[field] || field).padEnd(maxLabel + 2);
|
|
84
|
+
const raw = obj[field];
|
|
85
|
+
if (raw !== null && typeof raw === 'object') {
|
|
86
|
+
console.log(` ${chalk.dim(label)}${formatNestedValue(raw)}`);
|
|
87
|
+
} else {
|
|
88
|
+
const value = formatValue(raw);
|
|
89
|
+
console.log(` ${chalk.dim(label)} ${value}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Recursively render a nested object/array value for formatDetail, instead
|
|
96
|
+
* of falling through to formatValue's String(value) -> "[object Object]".
|
|
97
|
+
* Kept local to formatDetail (not folded into formatValue) so formatTable /
|
|
98
|
+
* formatCsv output is unchanged.
|
|
99
|
+
*/
|
|
100
|
+
function formatNestedValue(value, indent = 4) {
|
|
101
|
+
if (value == null) return ` ${chalk.dim('-')}`;
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
if (value.length === 0) return ` ${chalk.dim('[]')}`;
|
|
105
|
+
const allPrimitive = value.every(v => v === null || typeof v !== 'object');
|
|
106
|
+
if (allPrimitive) return ` ${value.map(v => formatValue(v)).join(', ')}`;
|
|
107
|
+
return '\n' + value
|
|
108
|
+
.map(v => `${' '.repeat(indent)}- ${typeof v === 'object' && v !== null ? formatNestedValue(v, indent + 2).trimStart() : formatValue(v)}`)
|
|
109
|
+
.join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const keys = Object.keys(value);
|
|
113
|
+
if (keys.length === 0) return ` ${chalk.dim('{}')}`;
|
|
114
|
+
return '\n' + keys
|
|
115
|
+
.map(k => {
|
|
116
|
+
const v = value[k];
|
|
117
|
+
if (v !== null && typeof v === 'object') {
|
|
118
|
+
return `${' '.repeat(indent)}${chalk.dim(k + ':')}${formatNestedValue(v, indent + 2)}`;
|
|
119
|
+
}
|
|
120
|
+
return `${' '.repeat(indent)}${chalk.dim(k + ':')} ${formatValue(v)}`;
|
|
121
|
+
})
|
|
122
|
+
.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Print a success message
|
|
127
|
+
*/
|
|
128
|
+
export function printSuccess(message) {
|
|
129
|
+
const format = getOutputFormat();
|
|
130
|
+
if (format === 'json') {
|
|
131
|
+
console.log(JSON.stringify({ success: true, message }));
|
|
132
|
+
} else if (format !== 'quiet') {
|
|
133
|
+
console.log(chalk.green(message));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Print a warning message
|
|
139
|
+
*/
|
|
140
|
+
export function printWarning(message) {
|
|
141
|
+
if (getOutputFormat() !== 'quiet') {
|
|
142
|
+
console.error(chalk.yellow(message));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Print info message (suppressed in quiet/json mode)
|
|
148
|
+
*/
|
|
149
|
+
export function printInfo(message) {
|
|
150
|
+
const format = getOutputFormat();
|
|
151
|
+
if (format !== 'quiet' && format !== 'json') {
|
|
152
|
+
console.log(chalk.dim(message));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// -- Internal helpers --
|
|
157
|
+
|
|
158
|
+
function formatTable(rows, columns, headers, options) {
|
|
159
|
+
const table = new Table({
|
|
160
|
+
head: headers.map(h => chalk.bold(h)),
|
|
161
|
+
style: { head: [], border: [] },
|
|
162
|
+
chars: {
|
|
163
|
+
'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
|
|
164
|
+
'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
|
|
165
|
+
'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '',
|
|
166
|
+
'right': '', 'right-mid': '', 'middle': ' ',
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
table.push(columns.map(col => formatValue(row[col])));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(table.toString());
|
|
175
|
+
|
|
176
|
+
// Show count
|
|
177
|
+
if (rows.length > 10) {
|
|
178
|
+
console.log(chalk.dim(`\n${rows.length} items`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatCsv(rows, columns, headers) {
|
|
183
|
+
console.log(headers.map(csvEscape).join(','));
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
console.log(columns.map(col => csvEscape(row[col])).join(','));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatQuiet(rows, columns) {
|
|
190
|
+
// Print first column (usually ID) of each row
|
|
191
|
+
const idCol = columns[0];
|
|
192
|
+
for (const row of rows) {
|
|
193
|
+
console.log(row[idCol] ?? '');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function csvEscape(value) {
|
|
198
|
+
const str = String(value ?? '');
|
|
199
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
200
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
201
|
+
}
|
|
202
|
+
return str;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function formatValue(value) {
|
|
206
|
+
if (value == null) return chalk.dim('-');
|
|
207
|
+
if (typeof value === 'boolean') return value ? chalk.green('yes') : chalk.dim('no');
|
|
208
|
+
|
|
209
|
+
const str = String(value);
|
|
210
|
+
|
|
211
|
+
// Color status values
|
|
212
|
+
const lower = str.toLowerCase();
|
|
213
|
+
if (['active', 'healthy', 'operational', 'approved', 'online', 'success'].includes(lower)) {
|
|
214
|
+
return chalk.green(str);
|
|
215
|
+
}
|
|
216
|
+
if (['degraded', 'warning', 'pending', 'in_progress', 'draft', 'stale'].includes(lower)) {
|
|
217
|
+
return chalk.yellow(str);
|
|
218
|
+
}
|
|
219
|
+
if (['error', 'down', 'failed', 'rejected', 'retired', 'offline', 'suspended', 'dead'].includes(lower)) {
|
|
220
|
+
return chalk.red(str);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Truncate long values in table mode
|
|
224
|
+
if (str.length > 60) return str.slice(0, 57) + '...';
|
|
225
|
+
|
|
226
|
+
return str;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getOutputFormat() {
|
|
230
|
+
if (process.argv.includes('--json')) return 'json';
|
|
231
|
+
if (process.argv.includes('-q') || process.argv.includes('--quiet')) return 'quiet';
|
|
232
|
+
|
|
233
|
+
const idx = process.argv.indexOf('-o');
|
|
234
|
+
if (idx !== -1 && process.argv[idx + 1]) return process.argv[idx + 1];
|
|
235
|
+
const idx2 = process.argv.indexOf('--output');
|
|
236
|
+
if (idx2 !== -1 && process.argv[idx2 + 1]) return process.argv[idx2 + 1];
|
|
237
|
+
|
|
238
|
+
return 'table';
|
|
239
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imported FIRST by the shipped bins (bin/quox.js, bin/quoxon.js), before anything that loads React: ES modules
|
|
3
|
+
* evaluate in import order, and React picks its build from NODE_ENV when it is first loaded. The development
|
|
4
|
+
* build roughly doubles CPU per animation frame (measured: ~51% vs 12-30% at 12 fps, 100x40). An explicit
|
|
5
|
+
* NODE_ENV (a developer's, or a test runner's) is always kept.
|
|
6
|
+
*/
|
|
7
|
+
if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production';
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @quox/glyph — the canonical JS implementation of the QuoxGlyph terminal
|
|
3
|
+
* output language (DEVSUITE-GLYPH). Spec: quox-skills
|
|
4
|
+
* quoxify/references/quoxglyph-spec.md. This package IS the spec's constants:
|
|
5
|
+
* ports must consume it (or test against it), never re-type the values.
|
|
6
|
+
*
|
|
7
|
+
* Grammar: <icon> NAME │ <verb object> │ <result> · <duration> [status]
|
|
8
|
+
* Colour: truecolor, degrades to plain when !isTTY or NO_COLOR.
|
|
9
|
+
* Brand gradient #38bdf8 -> #a855f7; rainbow RESERVED for completed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const GRAD = [[56, 189, 248], [168, 85, 247]];
|
|
13
|
+
export const RAINBOW = [
|
|
14
|
+
[255, 59, 48], [255, 149, 0], [255, 204, 0], [52, 199, 89],
|
|
15
|
+
[0, 199, 190], [10, 132, 255], [94, 92, 230], [191, 90, 242]
|
|
16
|
+
];
|
|
17
|
+
export const STATUS = {
|
|
18
|
+
ok: { glyph: '✔', rgb: [48, 209, 88] },
|
|
19
|
+
fail: { glyph: '✖', rgb: [255, 69, 58] },
|
|
20
|
+
busy: { glyph: '◐', rgb: [56, 189, 248] },
|
|
21
|
+
warn: { glyph: '⚠', rgb: [255, 214, 10] },
|
|
22
|
+
block: { glyph: '⛔', rgb: null }
|
|
23
|
+
};
|
|
24
|
+
export const ICONS = {
|
|
25
|
+
repobrain: '🧠 REPOBRAIN', clarify: '🔍 CLARIFY', qlarity: '🔍 QLARITY',
|
|
26
|
+
quoxplan: '🧭 QUOXPLAN', doneengine: '🏁 DONEENGINE', quoxbuild: '🔨 QUOXBUILD',
|
|
27
|
+
quoxsignal: '📡 QUOXSIGNAL', bastion: '🛰️ BASTION', precompact: '🧿 PRECOMPACT',
|
|
28
|
+
brain2: '🌌 QORTEX',
|
|
29
|
+
watchtower: '📨 WATCHTOWER', metron: '⏳ METRON',
|
|
30
|
+
quoxseo: '📈 QUOXSEO', reach: '💥 REACH', vulcan: '🌋 VULCAN',
|
|
31
|
+
quoxvoice: '📞 QUOXVOICE', quoxsecurity: '🛡️ QUOXSECURITY', quoxpert: '🎓 QUOXPERT',
|
|
32
|
+
quoxslots: '📍 QUOXSLOTS',
|
|
33
|
+
quox: 'Ⓠ QUOX'
|
|
34
|
+
};
|
|
35
|
+
export const BUDGETS = { VERB_MAX: 48, RESULT_MAX: 40, LINE_MAX: 120, TREE_MAX: 6, TABLE_MAX: 6 };
|
|
36
|
+
export const PUA_Q = ''; // real horned Q, served by QuoxSymbols font
|
|
37
|
+
|
|
38
|
+
const R = '\x1b[0m', DIM = '\x1b[2m', BOLD = '\x1b[1m';
|
|
39
|
+
const fg = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
|
|
40
|
+
|
|
41
|
+
let forced = null;
|
|
42
|
+
/** Force colour on/off (null = auto: isTTY && !NO_COLOR). */
|
|
43
|
+
export function setColor(v) { forced = v; }
|
|
44
|
+
function on() {
|
|
45
|
+
if (forced !== null) return forced;
|
|
46
|
+
const env = process.env.QUOX_COLOR;
|
|
47
|
+
if (env === '0') return false;
|
|
48
|
+
if (env === '1' || env === '2') return true;
|
|
49
|
+
return Boolean(process.stdout && process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function iconFor(surface) {
|
|
53
|
+
let name = ICONS[surface] || `Ⓠ ${String(surface).toUpperCase()}`;
|
|
54
|
+
if (process.env.QUOX_BRAND_FONT === '1') name = name.replace('Ⓠ', PUA_Q);
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** "<icon> NAME" with NAME painted in the brand gradient, bold, so a Quox
|
|
59
|
+
* line stands out in tool output (owner 2026-10-05). Plain when colour is off. */
|
|
60
|
+
export function brandName(label, col = on()) {
|
|
61
|
+
if (!col) return label;
|
|
62
|
+
const i = label.indexOf(' ');
|
|
63
|
+
const icon = i > 0 ? label.slice(0, i) : '';
|
|
64
|
+
const chars = [...(i > 0 ? label.slice(i + 1) : label)];
|
|
65
|
+
const last = Math.max(chars.length - 1, 1);
|
|
66
|
+
const painted = chars.map((c, k) => fg(GRAD[0].map((v, j) => Math.round(v + (GRAD[1][j] - v) * k / last))) + c).join('');
|
|
67
|
+
return `${icon ? icon + ' ' : ''}${BOLD}${painted}${R}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const trunc = (s, max) => ([...s].length > max ? [...s].slice(0, max - 1).join('') + '…' : s);
|
|
71
|
+
|
|
72
|
+
/** The grammar event line. opts: {result, ms, status} */
|
|
73
|
+
export function line(surface, verb, opts = {}) {
|
|
74
|
+
const col = on();
|
|
75
|
+
const name = brandName(iconFor(surface), col);
|
|
76
|
+
const sep = col ? `${DIM}│${R}` : '│';
|
|
77
|
+
let tail = opts.result ? trunc(opts.result, BUDGETS.RESULT_MAX) : '';
|
|
78
|
+
if (opts.ms !== undefined) {
|
|
79
|
+
const d = col ? `${DIM}${opts.ms}ms${R}` : `${opts.ms}ms`;
|
|
80
|
+
tail = tail ? `${tail} ${col ? DIM + '·' + R : '·'} ${d}` : d;
|
|
81
|
+
}
|
|
82
|
+
const s = opts.status && STATUS[opts.status];
|
|
83
|
+
if (s) tail += ' ' + (col && s.rgb ? `${fg(s.rgb)}${s.glyph}${R}` : s.glyph);
|
|
84
|
+
const v = trunc(verb, BUDGETS.VERB_MAX);
|
|
85
|
+
return tail ? `${name} ${sep} ${v} ${sep} ${tail}` : `${name} ${sep} ${v}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Gradient progress meter: ▰▰▰▱ done/total (or pct when total omitted). */
|
|
89
|
+
export function meter(done, total, width = 8) {
|
|
90
|
+
const col = on();
|
|
91
|
+
const pct = total === undefined || total === null;
|
|
92
|
+
const ratio = pct ? Math.min(100, Math.max(0, done)) / 100 : (total > 0 ? done / total : 0);
|
|
93
|
+
const filled = Math.round(ratio * width);
|
|
94
|
+
const last = Math.max(1, width - 1);
|
|
95
|
+
let bar = '';
|
|
96
|
+
for (let i = 0; i < filled; i++) {
|
|
97
|
+
if (col) {
|
|
98
|
+
const t = i / last;
|
|
99
|
+
bar += fg(GRAD[0].map((v, k) => Math.round(v + (GRAD[1][k] - v) * t)));
|
|
100
|
+
}
|
|
101
|
+
bar += '▰';
|
|
102
|
+
}
|
|
103
|
+
if (col && filled) bar += R;
|
|
104
|
+
bar += (col ? DIM : '') + '▱'.repeat(Math.max(0, width - filled)) + (col ? R : '');
|
|
105
|
+
return pct ? `${bar} ${done}%` : `${bar} ${done}/${total}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Rainbow text. RESERVED for the `completed` lifecycle event. */
|
|
109
|
+
export function celebrate(text) {
|
|
110
|
+
if (!on()) return text;
|
|
111
|
+
const chars = [...text];
|
|
112
|
+
const last = Math.max(1, chars.length - 1);
|
|
113
|
+
return chars.map((ch, i) => {
|
|
114
|
+
const t = (i / last) * (RAINBOW.length - 1);
|
|
115
|
+
const seg = Math.min(RAINBOW.length - 2, Math.floor(t));
|
|
116
|
+
const local = t - seg;
|
|
117
|
+
return fg(RAINBOW[seg].map((v, k) => Math.round(v + (RAINBOW[seg + 1][k] - v) * local))) + ch;
|
|
118
|
+
}).join('') + R;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** WARD receipt footer. */
|
|
122
|
+
export function receipt(hash, ts) {
|
|
123
|
+
const col = on();
|
|
124
|
+
const chain = col ? `${fg([168, 85, 247])}⛓${R}` : '⛓';
|
|
125
|
+
const label = col ? `${DIM}ward:${R}` : 'ward:';
|
|
126
|
+
const t = ts ? (col ? ` ${DIM}· witnessed ${ts}${R}` : ` · witnessed ${ts}`) : '';
|
|
127
|
+
return `${chain} ${label}${String(hash).slice(0, 12)}${t}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Lifecycle events (spec section 4b): the contract every product emits.
|
|
132
|
+
* Each returns a rendered line; the caller prints it.
|
|
133
|
+
*/
|
|
134
|
+
export const events = {
|
|
135
|
+
created: (surface, thing, id) => line(surface, `${thing} created`, { result: id, status: 'ok' }),
|
|
136
|
+
updated: (surface, thing, id, delta) =>
|
|
137
|
+
line(surface, `${thing} updated`, { result: delta ? `${id} ${delta}` : id, status: 'ok' }),
|
|
138
|
+
progressed: (surface, step, id, done, total) =>
|
|
139
|
+
line(surface, step, { result: `${id} ${meter(done, total)}` }),
|
|
140
|
+
completed: (surface, thing, text) => line(surface, thing, { result: celebrate(text) }),
|
|
141
|
+
assigned: (surface, thing, holder) => line(surface, `${thing} assigned`, { result: holder, status: 'busy' }),
|
|
142
|
+
released: (surface, thing, id) => line(surface, `${thing} released`, { result: id, status: 'ok' }),
|
|
143
|
+
failed: (surface, thing, why) => line(surface, `${thing} failed`, { result: why, status: 'fail' }),
|
|
144
|
+
warned: (surface, thing, risk) => line(surface, thing, { result: risk, status: 'warn' })
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// ---- quoxcli integration (not part of the @quox/glyph copy above) ----
|
|
148
|
+
// VENDORED from quox-plugin-sdk packages/quox-glyph/index.js until the
|
|
149
|
+
// owner-gated npm publish lands; the lockstep test in quoxglyph.test.js
|
|
150
|
+
// fails if this copy drifts from the package.
|
|
151
|
+
import { getOutputFormat } from './output.js';
|
|
152
|
+
|
|
153
|
+
/** Print a rendered glyph line, respecting --output json/quiet. */
|
|
154
|
+
export function printEvent(rendered) {
|
|
155
|
+
const format = getOutputFormat();
|
|
156
|
+
if (format === 'json' || format === 'quiet') return;
|
|
157
|
+
console.log(` ${rendered}`);
|
|
158
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resident alarm — QUOXPERTICUS-REVISIT P5 "warden watchdog + loud alarm".
|
|
3
|
+
*
|
|
4
|
+
* While the warden pumps (residentPump.js) instead of answering, it also
|
|
5
|
+
* watches the resident it wards: is the tmux pane still there, has a
|
|
6
|
+
* permission dialog been stuck too long to ever clear itself, has the
|
|
7
|
+
* provider started refusing turns. Three independent conditions, each with
|
|
8
|
+
* its own escalating alert schedule (10m, 30m, then hourly) so a wedged
|
|
9
|
+
* resident is loud rather than silently absent, and each condition clears
|
|
10
|
+
* itself the moment its detector stops firing (no human-ack mechanism yet:
|
|
11
|
+
* clearing = the underlying thing is gone).
|
|
12
|
+
*
|
|
13
|
+
* Every I/O dependency (tmux session list, pane read, release/redeploy,
|
|
14
|
+
* the alert transport, the turn-receipt store, the clock) is injected, same
|
|
15
|
+
* contract as supervisorEngine.js and residentPump.js, so this runs under
|
|
16
|
+
* vitest with no real tmux host and no real Telegram bridge.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ERROR_NOTICE_WINDOW_MS } from './supervisorEngine.js';
|
|
20
|
+
|
|
21
|
+
// Gaps BETWEEN successive alerts for the SAME still-active condition:
|
|
22
|
+
// immediate on first detection (see shouldSendAlarm), then +10m, then +30m,
|
|
23
|
+
// then hourly forever until the condition clears.
|
|
24
|
+
export const ALARM_SCHEDULE_MS = [10 * 60 * 1000, 30 * 60 * 1000, ERROR_NOTICE_WINDOW_MS];
|
|
25
|
+
|
|
26
|
+
// A stuck permission dialog only becomes an alarm condition once it has sat
|
|
27
|
+
// unresolved this long. Below this age it is exactly what residentPump.js
|
|
28
|
+
// already logs: held input, not yet an incident.
|
|
29
|
+
export const STUCK_DIALOG_ALARM_THRESHOLD_MS = 10 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
// Pane-text scan patterns for a provider/credits failure. Deliberately
|
|
32
|
+
// simple substring/regex matches (this is pane TEXT, not a structured
|
|
33
|
+
// provider error object the way detectTurnError in supervisorEngine.js
|
|
34
|
+
// parses a headless turn's JSON output) — same spirit, different signal.
|
|
35
|
+
export const PROVIDER_ISSUE_PATTERNS = [
|
|
36
|
+
{ pattern: 'out of usage credits', regex: /out of usage credits/i },
|
|
37
|
+
{ pattern: 'usage limit', regex: /usage limit/i },
|
|
38
|
+
{ pattern: 'provider error', regex: /provider error/i },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function scanPaneForProviderIssue(paneText) {
|
|
42
|
+
const text = String(paneText || '');
|
|
43
|
+
for (const { pattern, regex } of PROVIDER_ISSUE_PATTERNS) {
|
|
44
|
+
if (regex.test(text)) return { matched: true, pattern };
|
|
45
|
+
}
|
|
46
|
+
return { matched: false, pattern: null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createInitialAlarmCondition() {
|
|
50
|
+
return { active: false, first_detected_at: null, last_alerted_at: null, alert_count: 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createInitialAlarmState() {
|
|
54
|
+
return {
|
|
55
|
+
pane_dead_attempted: false,
|
|
56
|
+
pane_dead_redeploy_failed: false,
|
|
57
|
+
conditions: {
|
|
58
|
+
pane_dead: createInitialAlarmCondition(),
|
|
59
|
+
stuck_dialog: createInitialAlarmCondition(),
|
|
60
|
+
provider_error: createInitialAlarmCondition(),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Advance one condition's state from a single boolean detection this pass.
|
|
67
|
+
* A detection while already active is a no-op (first_detected_at sticks to
|
|
68
|
+
* the original onset). No detection while active is a full reset: clearing
|
|
69
|
+
* a condition means the next detection is treated as a fresh onset, with
|
|
70
|
+
* its own fresh alert schedule.
|
|
71
|
+
*/
|
|
72
|
+
export function updateAlarmCondition(conditionState, detected, now) {
|
|
73
|
+
const current = conditionState || createInitialAlarmCondition();
|
|
74
|
+
if (detected) {
|
|
75
|
+
if (current.active) return current;
|
|
76
|
+
return { ...current, active: true, first_detected_at: now };
|
|
77
|
+
}
|
|
78
|
+
if (current.active) return createInitialAlarmCondition();
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether condition `conditionState` should alert right now. The first
|
|
84
|
+
* alert for a freshly-active condition fires immediately (no delay: a dead
|
|
85
|
+
* pane or a provider outage is news the moment it is confirmed). Every
|
|
86
|
+
* alert after that waits at least the schedule's gap for however many
|
|
87
|
+
* alerts have already fired, capped at the schedule's last (hourly) entry.
|
|
88
|
+
*/
|
|
89
|
+
export function shouldSendAlarm(conditionState, now, schedule = ALARM_SCHEDULE_MS) {
|
|
90
|
+
if (!conditionState || !conditionState.active) return false;
|
|
91
|
+
if (conditionState.last_alerted_at == null) return true;
|
|
92
|
+
const idx = Math.min(Math.max((conditionState.alert_count || 0) - 1, 0), schedule.length - 1);
|
|
93
|
+
const gap = schedule[idx];
|
|
94
|
+
return (now - conditionState.last_alerted_at) >= gap;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function recordAlarmSent(conditionState, now) {
|
|
98
|
+
return { ...conditionState, last_alerted_at: now, alert_count: (conditionState.alert_count || 0) + 1 };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Send one alarm through both transports in order, per spec: tg-send.sh
|
|
103
|
+
* first (the owner's phone), `quox slot say` second as the fallback when
|
|
104
|
+
* tg-send.sh is unavailable or fails — never silent, and the absence of the
|
|
105
|
+
* script is itself treated as "alarm-transport-unavailable", not a reason
|
|
106
|
+
* to skip the slot notice.
|
|
107
|
+
*/
|
|
108
|
+
export async function sendAlarm({ tgSend, postSlotNotice, text, sessionName = 'WARDEN', log = () => {} }) {
|
|
109
|
+
let tgSent = false;
|
|
110
|
+
if (tgSend) {
|
|
111
|
+
try {
|
|
112
|
+
await tgSend(sessionName, text);
|
|
113
|
+
tgSent = true;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
log('warn', `alarm: tg-send.sh failed, falling back to slot say: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
log('warn', 'alarm: alarm-transport-unavailable (tg-send.sh not found), falling back to slot say');
|
|
119
|
+
}
|
|
120
|
+
if (!tgSent) {
|
|
121
|
+
try {
|
|
122
|
+
await postSlotNotice(text);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
log('error', `alarm: slot say fallback also failed: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { tg_sent: tgSent };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run one warden watchdog pass.
|
|
132
|
+
*
|
|
133
|
+
* Injected dependencies:
|
|
134
|
+
* listSessions() -> array of the host's tmux sessions ({name, ...})
|
|
135
|
+
* readPane() -> current pane text (only called while the pane
|
|
136
|
+
* is confirmed alive)
|
|
137
|
+
* releaseSlot() -> release the resident's slot (best-effort)
|
|
138
|
+
* redeploy() -> run the redeploy path (reuses `quox dev
|
|
139
|
+
* resident deploy`; throws on failure)
|
|
140
|
+
* claimFallback() -> attempt to claim the slot as this warden's
|
|
141
|
+
* own fallback voice (only called when redeploy
|
|
142
|
+
* fails); returns {claimed: bool}
|
|
143
|
+
* postSlotNotice(text) -> slot `say`
|
|
144
|
+
* recordTurn(fields) -> turn receipt (outcome/error_note only;
|
|
145
|
+
* NEVER a new outcome value — see
|
|
146
|
+
* devSupervisorTurnsStore.js OUTCOMES)
|
|
147
|
+
* tgSend(session, text) -> shells out to tg-send.sh; absent/failing is
|
|
148
|
+
* handled by sendAlarm above, never thrown here
|
|
149
|
+
* stuckDialogSinceMs -> residentPump.js's state.stuck_dialog_since
|
|
150
|
+
* for this slot (single source of truth for
|
|
151
|
+
* dialog detection; this module only ages it)
|
|
152
|
+
*/
|
|
153
|
+
export async function runWardenPass({
|
|
154
|
+
hostId,
|
|
155
|
+
slot,
|
|
156
|
+
sessionName,
|
|
157
|
+
listSessions,
|
|
158
|
+
readPane,
|
|
159
|
+
releaseSlot,
|
|
160
|
+
redeploy,
|
|
161
|
+
claimFallback,
|
|
162
|
+
postSlotNotice,
|
|
163
|
+
recordTurn,
|
|
164
|
+
tgSend,
|
|
165
|
+
stuckDialogSinceMs = null,
|
|
166
|
+
state,
|
|
167
|
+
log = () => {},
|
|
168
|
+
now = () => Date.now(),
|
|
169
|
+
schedule = ALARM_SCHEDULE_MS,
|
|
170
|
+
stuckDialogThresholdMs = STUCK_DIALOG_ALARM_THRESHOLD_MS,
|
|
171
|
+
}) {
|
|
172
|
+
const base = state || createInitialAlarmState();
|
|
173
|
+
const nextState = {
|
|
174
|
+
pane_dead_attempted: base.pane_dead_attempted ?? false,
|
|
175
|
+
pane_dead_redeploy_failed: base.pane_dead_redeploy_failed ?? false,
|
|
176
|
+
conditions: {
|
|
177
|
+
pane_dead: base.conditions?.pane_dead || createInitialAlarmCondition(),
|
|
178
|
+
stuck_dialog: base.conditions?.stuck_dialog || createInitialAlarmCondition(),
|
|
179
|
+
provider_error: base.conditions?.provider_error || createInitialAlarmCondition(),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const nowMs = now();
|
|
184
|
+
const report = { pane_alive: null, redeployed: false, redeploy_error: null, fallback_claimed: false, alerts_sent: [] };
|
|
185
|
+
|
|
186
|
+
const sessions = await listSessions();
|
|
187
|
+
const paneAlive = !!(sessions || []).find((s) => s.name === sessionName);
|
|
188
|
+
report.pane_alive = paneAlive;
|
|
189
|
+
|
|
190
|
+
let paneText = null;
|
|
191
|
+
|
|
192
|
+
if (!paneAlive) {
|
|
193
|
+
if (!nextState.pane_dead_attempted) {
|
|
194
|
+
nextState.pane_dead_attempted = true;
|
|
195
|
+
log('warn', `warden: resident session ${sessionName} on ${hostId} is not present (pane dead); releasing the slot and redeploying.`);
|
|
196
|
+
try {
|
|
197
|
+
await releaseSlot();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log('warn', `warden: releasing ${slot} failed (continuing): ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
await redeploy();
|
|
203
|
+
report.redeployed = true;
|
|
204
|
+
nextState.pane_dead_redeploy_failed = false;
|
|
205
|
+
nextState.conditions.pane_dead = createInitialAlarmCondition();
|
|
206
|
+
try {
|
|
207
|
+
await postSlotNotice(`Resident on ${hostId} lost its pane; redeployed automatically.`);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
log('warn', `warden: redeploy notice failed to post: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await recordTurn({ outcome: 'error', error_note: 'pane_dead_auto_redeployed' });
|
|
213
|
+
} catch (err) {
|
|
214
|
+
log('warn', `warden: redeploy receipt failed to post: ${err.message}`);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
report.redeploy_error = err.message;
|
|
218
|
+
nextState.pane_dead_redeploy_failed = true;
|
|
219
|
+
log('error', `warden: redeploy failed for ${hostId}: ${err.message}`);
|
|
220
|
+
try {
|
|
221
|
+
await postSlotNotice(`Resident on ${hostId} lost its pane and automatic redeploy FAILED (${err.message}). A human should check this host.`);
|
|
222
|
+
} catch (err2) {
|
|
223
|
+
log('warn', `warden: failure notice failed to post: ${err2.message}`);
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const fb = await claimFallback();
|
|
227
|
+
report.fallback_claimed = !!(fb && fb.claimed);
|
|
228
|
+
} catch (err2) {
|
|
229
|
+
log('warn', `warden: fallback claim attempt failed: ${err2.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (nextState.pane_dead_redeploy_failed) {
|
|
234
|
+
nextState.conditions.pane_dead = updateAlarmCondition(nextState.conditions.pane_dead, true, nowMs);
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
if (nextState.pane_dead_attempted || nextState.conditions.pane_dead.active) {
|
|
238
|
+
log('info', `warden: resident session ${sessionName} on ${hostId} is present again; pane-dead condition cleared.`);
|
|
239
|
+
}
|
|
240
|
+
nextState.pane_dead_attempted = false;
|
|
241
|
+
nextState.pane_dead_redeploy_failed = false;
|
|
242
|
+
nextState.conditions.pane_dead = updateAlarmCondition(nextState.conditions.pane_dead, false, nowMs);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
paneText = await readPane();
|
|
246
|
+
} catch (err) {
|
|
247
|
+
log('warn', `warden: pane read failed on ${hostId} (skipping provider-error scan this pass): ${err.message}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Stuck dialog: this module never detects the dialog itself, it only ages
|
|
252
|
+
// what residentPump.js already found (single source of truth).
|
|
253
|
+
const dialogStuck = stuckDialogSinceMs != null && (nowMs - stuckDialogSinceMs) >= stuckDialogThresholdMs;
|
|
254
|
+
nextState.conditions.stuck_dialog = updateAlarmCondition(nextState.conditions.stuck_dialog, dialogStuck, nowMs);
|
|
255
|
+
|
|
256
|
+
if (paneText != null) {
|
|
257
|
+
const scan = scanPaneForProviderIssue(paneText);
|
|
258
|
+
nextState.conditions.provider_error = updateAlarmCondition(nextState.conditions.provider_error, scan.matched, nowMs);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const conditionAlerts = [
|
|
262
|
+
['pane_dead', `Resident on ${hostId}: pane is dead and automatic redeploy failed. A human should check this host.`],
|
|
263
|
+
['stuck_dialog', `Resident on ${hostId}: a permission dialog has been stuck for over ${Math.round(stuckDialogThresholdMs / 60000)} minutes and will never resolve itself; a human must approve or cancel it.`],
|
|
264
|
+
['provider_error', `Resident on ${hostId}: a provider/credits error was detected in its pane. A human should check its account/billing status.`],
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
for (const [key, message] of conditionAlerts) {
|
|
268
|
+
const condition = nextState.conditions[key];
|
|
269
|
+
if (shouldSendAlarm(condition, nowMs, schedule)) {
|
|
270
|
+
const sendResult = await sendAlarm({ tgSend, postSlotNotice, text: message, sessionName: `WARDEN-${hostId}`, log });
|
|
271
|
+
nextState.conditions[key] = recordAlarmSent(condition, nowMs);
|
|
272
|
+
try {
|
|
273
|
+
await recordTurn({ outcome: 'error', error_note: `alarm_${key}` });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
log('warn', `warden: alarm receipt failed for ${key}: ${err.message}`);
|
|
276
|
+
}
|
|
277
|
+
report.alerts_sent.push({ condition: key, tg_sent: sendResult.tg_sent });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { state: nextState, ...report };
|
|
282
|
+
}
|