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,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bare-`quox` splash (DEVSUITE-GLYPH follow-on): the horned Q in
|
|
3
|
+
* truecolor half-blocks with the brand gradient, plus a one-glance verb map.
|
|
4
|
+
* Bitmap generated from QuoxSymbols quoxQ (fontforge export at 26px); the
|
|
5
|
+
* gradient stops mirror @quox/glyph. Colour gating matches the renderer:
|
|
6
|
+
* QUOX_COLOR=0 off / 1|2 force on, else tty && !NO_COLOR.
|
|
7
|
+
*/
|
|
8
|
+
import { GRAD } from './quoxglyph.js';
|
|
9
|
+
|
|
10
|
+
const Q_BITMAP = [
|
|
11
|
+
' # ',
|
|
12
|
+
' # # ',
|
|
13
|
+
' ## # ',
|
|
14
|
+
' ## ## ',
|
|
15
|
+
' ######### ',
|
|
16
|
+
' ########### ',
|
|
17
|
+
' ### ### ',
|
|
18
|
+
'### ### ',
|
|
19
|
+
'### ### ',
|
|
20
|
+
'### ### ',
|
|
21
|
+
'### ### ',
|
|
22
|
+
'### ### ',
|
|
23
|
+
'### ### ### ',
|
|
24
|
+
'### ###### ',
|
|
25
|
+
' ### ##### ',
|
|
26
|
+
' ############ ',
|
|
27
|
+
' ###########',
|
|
28
|
+
' ### ',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function colorOn() {
|
|
32
|
+
const env = process.env.QUOX_COLOR;
|
|
33
|
+
if (env === '0') return false;
|
|
34
|
+
if (env === '1' || env === '2') return true;
|
|
35
|
+
return Boolean(process.stdout && process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const R = '\x1b[0m';
|
|
39
|
+
const DIM = '\x1b[2m';
|
|
40
|
+
const BOLD = '\x1b[1m';
|
|
41
|
+
|
|
42
|
+
function gradFg(t) {
|
|
43
|
+
const [a, b] = GRAD;
|
|
44
|
+
const c = a.map((v, k) => Math.round(v + (b[k] - v) * t));
|
|
45
|
+
return `\x1b[38;2;${c[0]};${c[1]};${c[2]}m`;
|
|
46
|
+
}
|
|
47
|
+
function gradBg(t) {
|
|
48
|
+
const [a, b] = GRAD;
|
|
49
|
+
const c = a.map((v, k) => Math.round(v + (b[k] - v) * t));
|
|
50
|
+
return `\x1b[48;2;${c[0]};${c[1]};${c[2]}m`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Render the Q as half-block rows (2 bitmap rows per terminal row). */
|
|
54
|
+
export function qArt(col) {
|
|
55
|
+
const cols = Q_BITMAP[0].length;
|
|
56
|
+
const out = [];
|
|
57
|
+
for (let y = 0; y < Q_BITMAP.length; y += 2) {
|
|
58
|
+
const top = Q_BITMAP[y];
|
|
59
|
+
const bot = Q_BITMAP[y + 1] || ' '.repeat(cols);
|
|
60
|
+
let line = '';
|
|
61
|
+
for (let x = 0; x < cols; x++) {
|
|
62
|
+
const t = x / (cols - 1);
|
|
63
|
+
const hasTop = top[x] === '#';
|
|
64
|
+
const hasBot = bot[x] === '#';
|
|
65
|
+
if (!col) {
|
|
66
|
+
line += hasTop && hasBot ? '█' : hasTop ? '▀' : hasBot ? '▄' : ' ';
|
|
67
|
+
} else if (hasTop && hasBot) {
|
|
68
|
+
line += `${gradFg(t)}${gradBg(t)}▀${R}`;
|
|
69
|
+
} else if (hasTop) {
|
|
70
|
+
line += `${gradFg(t)}▀${R}`;
|
|
71
|
+
} else if (hasBot) {
|
|
72
|
+
line += `${gradFg(t)}▄${R}`;
|
|
73
|
+
} else {
|
|
74
|
+
line += ' ';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
out.push(line);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One frame of the intro sweep: the Q is revealed left to right with a bright
|
|
84
|
+
* leading edge; progress >= 1 returns exactly qArt(col). Always the same
|
|
85
|
+
* number of rows and columns, so the live region never changes height.
|
|
86
|
+
*/
|
|
87
|
+
export function qArtFrame(progress, col) {
|
|
88
|
+
if (progress >= 1 || !col) return qArt(col);
|
|
89
|
+
const cols = Q_BITMAP[0].length;
|
|
90
|
+
const head = progress * (cols + 3) - 1.5;
|
|
91
|
+
const px = (x, on) => (on && x <= head ? 1 : 0);
|
|
92
|
+
const glow = (x, t) => {
|
|
93
|
+
const d = head - x; // 0 at the leading edge, grows behind it
|
|
94
|
+
const k = d < 3 ? 1 - d / 3 : 0;
|
|
95
|
+
const [a, b] = GRAD;
|
|
96
|
+
const base = a.map((v, j) => v + (b[j] - v) * t);
|
|
97
|
+
const c = base.map(v => Math.round(v + (255 - v) * 0.7 * k));
|
|
98
|
+
return `\x1b[38;2;${c[0]};${c[1]};${c[2]}m`;
|
|
99
|
+
};
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let y = 0; y < Q_BITMAP.length; y += 2) {
|
|
102
|
+
const top = Q_BITMAP[y];
|
|
103
|
+
const bot = Q_BITMAP[y + 1] || ' '.repeat(cols);
|
|
104
|
+
let line = '';
|
|
105
|
+
for (let x = 0; x < cols; x++) {
|
|
106
|
+
const t = x / (cols - 1);
|
|
107
|
+
const hasTop = px(x, top[x] === '#');
|
|
108
|
+
const hasBot = px(x, bot[x] === '#');
|
|
109
|
+
if (!hasTop && !hasBot) line += ' ';
|
|
110
|
+
else if (hasTop && hasBot) line += `${glow(x, t)}${gradBg(t)}▀${R}`;
|
|
111
|
+
else line += `${glow(x, t)}${hasTop ? '▀' : '▄'}${R}`;
|
|
112
|
+
}
|
|
113
|
+
out.push(line);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function printBanner(version, { loggedIn = true } = {}) {
|
|
119
|
+
const col = colorOn();
|
|
120
|
+
const art = qArt(col);
|
|
121
|
+
const b = (s) => (col ? `${BOLD}${s}${R}` : s);
|
|
122
|
+
const d = (s) => (col ? `${DIM}${s}${R}` : s);
|
|
123
|
+
const text = [
|
|
124
|
+
'',
|
|
125
|
+
`${b('Quox')} ${d('— the governed agent platform')}${version ? d(` v${version}`) : ''}`,
|
|
126
|
+
d('every action witnessed · every agent governed'),
|
|
127
|
+
'',
|
|
128
|
+
// quox#624: a logged-out first run starts at login, not at the auth wall.
|
|
129
|
+
...(loggedIn ? [] : [`🔑 ${b('quox login')} ${d('start here: sign in to your instance')}`]),
|
|
130
|
+
`🧭 ${b('quox plan focus')} ${d('what matters now')}`,
|
|
131
|
+
`🛰️ ${b('quox fleet status')} ${d('the estate at a glance')}`,
|
|
132
|
+
`📡 ${b('quox workflow list')} ${d('governed automations')}`,
|
|
133
|
+
`⛓ ${b('quox volt verify')} ${d('the evidence chain')}`,
|
|
134
|
+
'',
|
|
135
|
+
d('quox --help for everything'),
|
|
136
|
+
];
|
|
137
|
+
const lines = [];
|
|
138
|
+
const rows = Math.max(art.length, text.length);
|
|
139
|
+
for (let i = 0; i < rows; i++) {
|
|
140
|
+
lines.push(` ${art[i] || ' '.repeat(Q_BITMAP[0].length)} ${text[i] || ''}`);
|
|
141
|
+
}
|
|
142
|
+
process.stdout.write('\n' + lines.join('\n') + '\n\n');
|
|
143
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE-BRAIN CX4 (attach-time prime) and CX3 (handoff-to-note) helpers.
|
|
3
|
+
*
|
|
4
|
+
* Both are best effort by design: neither may ever fail an attach or a detach.
|
|
5
|
+
* Endpoints reused: brain.context.pack (`quox brain context`) and
|
|
6
|
+
* brain.capture.create (`quox brain add` / `decide`).
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
10
|
+
|
|
11
|
+
export const PRIME_TIMEOUT_MS = 5000;
|
|
12
|
+
export const PRIME_TOKEN_BUDGET = 1200;
|
|
13
|
+
export const PRIME_SNIPPET_CHARS = 300;
|
|
14
|
+
|
|
15
|
+
function gitOut(args) {
|
|
16
|
+
try { return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return ''; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Does this attach kit's resolved tool list carry the brain_context tool? */
|
|
20
|
+
export function kitHasBrainContext(kit) {
|
|
21
|
+
return Array.isArray(kit?.tools) && kit.tools.includes('brain_context');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Query text from the working context: repo, branch, advisory stream, last 3 commit subjects. */
|
|
25
|
+
export function buildPrimeQuery({ git = gitOut, env = process.env } = {}) {
|
|
26
|
+
const top = git(['rev-parse', '--show-toplevel']);
|
|
27
|
+
const repo = top ? top.split('/').pop() : '';
|
|
28
|
+
const branch = git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
29
|
+
const subjects = git(['log', '-3', '--pretty=%s']).split('\n').map((s) => s.trim()).filter(Boolean);
|
|
30
|
+
const parts = [];
|
|
31
|
+
if (repo) parts.push(`repo ${repo}`);
|
|
32
|
+
if (branch && branch !== 'HEAD') parts.push(`branch ${branch}`);
|
|
33
|
+
if (env.QUOX_STREAM) parts.push(`stream ${env.QUOX_STREAM} (advisory)`);
|
|
34
|
+
if (subjects.length) parts.push(`recent work: ${subjects.join('; ')}`);
|
|
35
|
+
return parts.join(', ');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function withTimeout(promise, ms) {
|
|
39
|
+
let timer;
|
|
40
|
+
const t = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`timed out after ${ms / 1000}s`)), ms); });
|
|
41
|
+
return Promise.race([promise, t]).finally(() => clearTimeout(timer));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returns the "## Brain primer" markdown section. Never throws. */
|
|
45
|
+
export async function buildBrainPrimer(client, { query, timeoutMs = PRIME_TIMEOUT_MS } = {}) {
|
|
46
|
+
const head = '## Brain primer';
|
|
47
|
+
try {
|
|
48
|
+
const q = query ?? buildPrimeQuery();
|
|
49
|
+
if (!q) return `${head}\n\nNo primer: the brain returned nothing relevant\n`;
|
|
50
|
+
const ep = resolveEndpoint('brain.context.pack');
|
|
51
|
+
const data = await withTimeout(
|
|
52
|
+
Promise.resolve().then(() => client.post(ep.path, { service: ep.service, body: { query: q, tokenBudget: PRIME_TOKEN_BUDGET } })),
|
|
53
|
+
timeoutMs,
|
|
54
|
+
);
|
|
55
|
+
const passages = Array.isArray(data?.passages) ? data.passages : [];
|
|
56
|
+
const insufficient = data?.insufficient === true || data?.relevance?.insufficient === true || passages.length === 0;
|
|
57
|
+
if (insufficient) return `${head}\n\nNo primer: the brain returned nothing relevant\n`;
|
|
58
|
+
const lines = passages.map((p) => {
|
|
59
|
+
const c = p.citation || {};
|
|
60
|
+
const text = String(p.text || '').replace(/\s+/g, ' ').trim().slice(0, PRIME_SNIPPET_CHARS);
|
|
61
|
+
return `- ${c.title || 'Untitled'} (${p.noteId || c.noteId || '?'}): ${text}`;
|
|
62
|
+
});
|
|
63
|
+
return `${head}\n\nQuery (stream is advisory): ${q}\n\n${lines.join('\n')}\n`;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return `${head}\n\nNo primer: brain unreachable (${err?.message || 'unknown error'})\n`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Save a handoff to the knowledge brain as a private note. Returns { ok, noteId?, error? }; never throws. */
|
|
70
|
+
export async function saveHandoffNote(client, { agentId, content, prov = {}, sessionName, now = new Date() }) {
|
|
71
|
+
try {
|
|
72
|
+
const ep = resolveEndpoint('brain.capture.create');
|
|
73
|
+
const iso = now.toISOString();
|
|
74
|
+
const provLines = [
|
|
75
|
+
`Agent: ${agentId}`,
|
|
76
|
+
prov.repo ? `Repo: ${prov.repo}` : null,
|
|
77
|
+
prov.commit ? `Commit: ${prov.commit}` : null,
|
|
78
|
+
sessionName ? `Session: ${sessionName}` : null,
|
|
79
|
+
`At: ${iso}`,
|
|
80
|
+
].filter(Boolean);
|
|
81
|
+
const text = `${content}\n\n## Provenance\n\n${provLines.map((l) => `- ${l}`).join('\n')}\n`;
|
|
82
|
+
const data = await client.post(ep.path, {
|
|
83
|
+
service: ep.service,
|
|
84
|
+
body: { title: `Handoff: ${agentId} ${iso}`, text, tags: ['handoff', agentId], scope: 'user' },
|
|
85
|
+
});
|
|
86
|
+
return { ok: true, noteId: data?.noteId };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { ok: false, error: err?.message || 'unknown error' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL-CONTEXT (AQ-9A phase 6): work out WHICH session is calling when
|
|
3
|
+
* $QUOX_SESSION is unset, so an approval card says "BRAIN2-AUDIT (CLI)"
|
|
4
|
+
* instead of a bare user id. Display-only: the server never authorizes on it.
|
|
5
|
+
*
|
|
6
|
+
* Precedence:
|
|
7
|
+
* 1. QUOX_SESSION
|
|
8
|
+
* 2. QUOX_STREAM (advisory)
|
|
9
|
+
* 3. the agent attached by this Claude process tree
|
|
10
|
+
* (~/.quox/attach/sessions/claude-<pid>/<agent>/)
|
|
11
|
+
* 4. the slot held by this process tree (held-slots.json sessionKey claude-<pid>)
|
|
12
|
+
* 5. last resort: "<parent process> in ~/<cwd>"
|
|
13
|
+
*
|
|
14
|
+
* Never reads token contents and never emits secrets: only directory names,
|
|
15
|
+
* the kit's display_name, the slot's session label and the cwd.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join, basename } from 'node:path';
|
|
20
|
+
|
|
21
|
+
/** Tools the collector always holds for a human when an agent proposes them. */
|
|
22
|
+
export const APPROVAL_GATED_TOOLS = new Set([
|
|
23
|
+
'loop_halt', 'slot_assign', 'bob_dispatch_build', 'email_send', 'send_notification',
|
|
24
|
+
'remote_ssh', 'ssh_exec', 'db_query', 'alerts_manage', 'backup_run',
|
|
25
|
+
'keeper_backup_create', 'deploy_template', 'gemini_generate', 'perplexity_research',
|
|
26
|
+
'system_admin', 'proxmox_vm_action', 'calendar_event_create',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const MISSING_REASON_WARNING =
|
|
30
|
+
'approval requests without --reason show \'none given\' to the human; pass --reason "why"';
|
|
31
|
+
|
|
32
|
+
/** One-line stderr warning when a gated tool is run without --reason, else null. */
|
|
33
|
+
export function missingReasonWarning(toolName, reason) {
|
|
34
|
+
if (!APPROVAL_GATED_TOOLS.has(String(toolName || ''))) return null;
|
|
35
|
+
if (typeof reason === 'string' && reason.trim()) return null;
|
|
36
|
+
return `warning: ${MISSING_REASON_WARNING}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cleanLabel(v, n = 48) {
|
|
40
|
+
const s = String(v == null ? '' : v).replace(/[^\w .:@+\-/~]/g, '').trim().slice(0, n);
|
|
41
|
+
return s || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const defaultDeps = {
|
|
45
|
+
readName: (pid) => { try { return readFileSync(`/proc/${pid}/comm`, 'utf-8').trim(); } catch { return null; } },
|
|
46
|
+
readPpid: (pid) => {
|
|
47
|
+
try {
|
|
48
|
+
const s = readFileSync(`/proc/${pid}/stat`, 'utf-8');
|
|
49
|
+
const rest = s.slice(s.lastIndexOf(')') + 2).trim().split(/\s+/);
|
|
50
|
+
const p = parseInt(rest[1], 10);
|
|
51
|
+
return Number.isFinite(p) ? p : null;
|
|
52
|
+
} catch { return null; }
|
|
53
|
+
},
|
|
54
|
+
attachSessionsRoot: () => join(homedir(), '.quox', 'attach', 'sessions'),
|
|
55
|
+
heldSlotsPath: () => join(homedir(), '.quox', 'held-slots.json'),
|
|
56
|
+
readdir: (d) => { try { return readdirSync(d, { withFileTypes: true }); } catch { return []; } },
|
|
57
|
+
readJson: (p) => { try { return existsSync(p) ? JSON.parse(readFileSync(p, 'utf-8')) : null; } catch { return null; } },
|
|
58
|
+
exists: existsSync,
|
|
59
|
+
cwd: () => { try { return process.cwd(); } catch { return ''; } },
|
|
60
|
+
home: () => homedir(),
|
|
61
|
+
pid: () => process.pid,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function findClaudeAncestor(deps, startPid) {
|
|
65
|
+
let pid = startPid;
|
|
66
|
+
for (let hop = 0; hop < 24; hop += 1) {
|
|
67
|
+
if (deps.readName(pid) === 'claude') return pid;
|
|
68
|
+
const pp = deps.readPpid(pid);
|
|
69
|
+
if (!pp || pp === pid || pid === 1) break;
|
|
70
|
+
pid = pp;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function attachedAgentLabel(deps, claudePid) {
|
|
76
|
+
const dir = join(deps.attachSessionsRoot(), `claude-${claudePid}`);
|
|
77
|
+
const agents = deps.readdir(dir).filter((e) => e.isDirectory() && deps.exists(join(dir, e.name, 'token')));
|
|
78
|
+
if (agents.length === 0) return null;
|
|
79
|
+
const name = agents[0].name;
|
|
80
|
+
const kit = deps.readJson(join(dir, name, 'kit.json'));
|
|
81
|
+
return cleanLabel(kit && kit.display_name) || cleanLabel(name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function heldSlotLabel(deps, claudePid) {
|
|
85
|
+
const all = deps.readJson(deps.heldSlotsPath());
|
|
86
|
+
if (!all || typeof all !== 'object') return null;
|
|
87
|
+
for (const [slot, e] of Object.entries(all)) {
|
|
88
|
+
if (e && e.sessionKey === `claude-${claudePid}`) return cleanLabel(e.session) || cleanLabel(slot);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cwdLabel(deps) {
|
|
94
|
+
const cwd = deps.cwd();
|
|
95
|
+
if (!cwd) return null;
|
|
96
|
+
const home = deps.home();
|
|
97
|
+
if (home && cwd === home) return '~';
|
|
98
|
+
if (home && cwd.startsWith(`${home}/`)) return `~/${cwd.slice(home.length + 1)}`;
|
|
99
|
+
return basename(cwd) || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @returns {{ label: string|null, via: string }} */
|
|
103
|
+
export function detectCallerSource(env = process.env, overrides = {}) {
|
|
104
|
+
const deps = { ...defaultDeps, ...overrides };
|
|
105
|
+
const fromSession = cleanLabel(env.QUOX_SESSION);
|
|
106
|
+
if (fromSession) return { label: fromSession, via: 'QUOX_SESSION' };
|
|
107
|
+
const fromStream = cleanLabel(env.QUOX_STREAM);
|
|
108
|
+
if (fromStream) return { label: fromStream, via: 'QUOX_STREAM' };
|
|
109
|
+
|
|
110
|
+
let claudePid = null;
|
|
111
|
+
try { claudePid = findClaudeAncestor(deps, deps.pid()); } catch { claudePid = null; }
|
|
112
|
+
if (claudePid) {
|
|
113
|
+
const agent = attachedAgentLabel(deps, claudePid);
|
|
114
|
+
if (agent) return { label: agent, via: 'attached_agent' };
|
|
115
|
+
const slot = heldSlotLabel(deps, claudePid);
|
|
116
|
+
if (slot) return { label: slot, via: 'held_slot' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let parent = null;
|
|
120
|
+
try {
|
|
121
|
+
const pp = deps.readPpid(deps.pid());
|
|
122
|
+
parent = pp ? deps.readName(pp) : null;
|
|
123
|
+
} catch { parent = null; }
|
|
124
|
+
const who = claudePid ? 'claude' : cleanLabel(parent, 24);
|
|
125
|
+
const where = cwdLabel(deps);
|
|
126
|
+
if (who && where) return { label: cleanLabel(`${who} in ${where}`), via: 'process' };
|
|
127
|
+
if (who) return { label: who, via: 'process' };
|
|
128
|
+
return { label: null, via: 'none' };
|
|
129
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox claude ls`: one row per quox-* tmux session. Pure parse/build/render
|
|
3
|
+
* functions take injected tmux output, held-slots JSON and a clock so they
|
|
4
|
+
* unit-test without tmux. SLOT comes from ~/.quox/held-slots.json (holder
|
|
5
|
+
* session name), AGENT from the attach kit of the claude pid that slot entry
|
|
6
|
+
* records (~/.quox/attach/sessions/claude-<pid>/<agent>/). Both are '-' when
|
|
7
|
+
* not derivable (a session that holds no slot has no recorded pid).
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync as realSpawnSync } from 'node:child_process';
|
|
10
|
+
import { PREFIX, programOf } from './governedSession.js';
|
|
11
|
+
import { slotsBySession, readAttachSafe, readHeldSlotsSafe } from './liveSessionsReport.js';
|
|
12
|
+
import { line } from './quoxglyph.js';
|
|
13
|
+
|
|
14
|
+
export const LS_FORMAT = '#{session_name}\t#{session_created}\t#{session_attached}\t#{session_activity}\t#{session_windows}';
|
|
15
|
+
|
|
16
|
+
export function formatAge(sec) {
|
|
17
|
+
const s = Math.max(0, Math.floor(sec));
|
|
18
|
+
if (s < 60) return `${s}s`;
|
|
19
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
20
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`;
|
|
21
|
+
return `${Math.floor(s / 86400)}d`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const toDate = (v) => new Date(Number(v) * 1000);
|
|
25
|
+
|
|
26
|
+
export function parseQuoxSessions(out) {
|
|
27
|
+
return String(out || '').split('\n').map((l) => l.split('\t'))
|
|
28
|
+
.filter((c) => c.length >= 5 && c[0].startsWith(PREFIX) && c[0].length > PREFIX.length)
|
|
29
|
+
.map((c) => ({
|
|
30
|
+
name: c[0].slice(PREFIX.length),
|
|
31
|
+
created: toDate(c[1]),
|
|
32
|
+
clients: Number(c[2]) || 0,
|
|
33
|
+
activity: toDate(c[3]),
|
|
34
|
+
windows: Number(c[4]) || 0,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildRows({ tmuxOut, held, now = new Date(), attachLookup = readAttachSafe }) {
|
|
39
|
+
const bySession = slotsBySession(held);
|
|
40
|
+
return parseQuoxSessions(tmuxOut)
|
|
41
|
+
.sort((a, b) => b.activity - a.activity)
|
|
42
|
+
.map((s) => {
|
|
43
|
+
// The slot ledger records the bare session name (what --resume NAME used).
|
|
44
|
+
const hit = bySession[s.name] || bySession[s.name.replace(/^(codex-|gemini-|tui-)/, '')] || null;
|
|
45
|
+
const att = hit ? attachLookup(hit.pid) : null;
|
|
46
|
+
const age = (d) => formatAge((now - d) / 1000);
|
|
47
|
+
return {
|
|
48
|
+
name: s.name,
|
|
49
|
+
state: s.clients > 0 ? 'attached' : 'detached',
|
|
50
|
+
clients: s.clients,
|
|
51
|
+
created: s.created.toISOString(),
|
|
52
|
+
created_age: age(s.created),
|
|
53
|
+
last_active: s.activity.toISOString(),
|
|
54
|
+
active_age: age(s.activity),
|
|
55
|
+
slot: hit ? hit.slot : null,
|
|
56
|
+
agent: att ? att.agent : null,
|
|
57
|
+
backend: programOf(s.name),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const clip = (v, n) => {
|
|
63
|
+
const a = [...String(v)];
|
|
64
|
+
return a.length > n ? a.slice(0, n - 1).join('') + '…' : String(v);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function renderTable(rows, { color = false } = {}) {
|
|
68
|
+
const W = { NAME: 20, STATE: 9, CLIENTS: 8, CREATED: 8, 'LAST ACTIVE': 12, SLOT: 8, AGENT: 8, BACKEND: 7 };
|
|
69
|
+
const cols = Object.keys(W);
|
|
70
|
+
const cell = (v, k, last) => (last ? clip(v, W[k]) : clip(v, W[k] - 1).padEnd(W[k]));
|
|
71
|
+
const fmt = (vals) => cols.map((k, i) => cell(vals[i], k, i === cols.length - 1)).join('');
|
|
72
|
+
const head = line('quox', 'sessions', { result: `${rows.length} running` });
|
|
73
|
+
const body = rows.map((r) => fmt([r.name, r.state, r.clients, r.created_age, r.active_age, r.slot || '-', r.agent || '-', r.backend]));
|
|
74
|
+
return [head, fmt(cols), ...body].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function runClaudeLs({ json = false } = {}, over = {}) {
|
|
78
|
+
const d = {
|
|
79
|
+
spawnSync: realSpawnSync, now: () => new Date(), write: (s) => process.stdout.write(s),
|
|
80
|
+
held: undefined, attachLookup: readAttachSafe, ...over,
|
|
81
|
+
};
|
|
82
|
+
const r = d.spawnSync('tmux', ['list-sessions', '-F', LS_FORMAT], { encoding: 'utf8' });
|
|
83
|
+
if (r && r.error && r.error.code === 'ENOENT') {
|
|
84
|
+
d.write('tmux is not installed (apt install tmux / brew install tmux).\n');
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
// A non-zero exit means no server is running (nothing to list), not an error.
|
|
88
|
+
const out = r && r.status === 0 ? r.stdout : '';
|
|
89
|
+
const rows = buildRows({ tmuxOut: out, held: d.held ?? readHeldSlotsSafe(), now: d.now(), attachLookup: d.attachLookup });
|
|
90
|
+
if (json) { d.write(JSON.stringify(rows, null, 2) + '\n'); return 0; }
|
|
91
|
+
if (!rows.length) { d.write('No quox sessions running.\n'); return 0; }
|
|
92
|
+
d.write(renderTable(rows) + '\n');
|
|
93
|
+
return 0;
|
|
94
|
+
}
|