quoxcli 0.2.0 → 0.3.1
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 +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Z2H-R2B: what Quox does when NO AI provider is connected. A turn is answered locally (no server call),
|
|
3
|
+
* with a full friendly card; nothing here names one vendor as the default. "add a host" has a deterministic
|
|
4
|
+
* path that needs no AI.
|
|
5
|
+
*/
|
|
6
|
+
import { CHOICES } from './model.js';
|
|
7
|
+
import { providerState } from './providers.js';
|
|
8
|
+
|
|
9
|
+
export const NO_AI_STATUS = 'no AI connected';
|
|
10
|
+
export const NO_AI_CARD = [
|
|
11
|
+
'No AI is connected yet. Pick one:',
|
|
12
|
+
'/connect openai (recommended), /connect anthropic, /connect ollama <host>, or /connect for the list.'
|
|
13
|
+
];
|
|
14
|
+
export const HOSTS_USAGE = '/hosts add';
|
|
15
|
+
|
|
16
|
+
/** -> true when at least one provider is connected, false when readable and none is, null when nothing could be read. */
|
|
17
|
+
export function anyProviderConnected(vault, caps) {
|
|
18
|
+
let known = false;
|
|
19
|
+
for (const c of CHOICES) {
|
|
20
|
+
const st = providerState({ cap: c.cap || c.id, vault: c.vault }, caps, vault).state;
|
|
21
|
+
if (st === 'connected') return true;
|
|
22
|
+
if (st !== 'unknown') known = true;
|
|
23
|
+
}
|
|
24
|
+
return known ? false : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const asksAddHost = (text) => /^\s*(?:please\s+)?(?:(?:i\s+(?:want|need)\s+to|can\s+you|help\s+me|let'?s)\s+)?(?:add|connect|enrol+|enroll|register|onboard)\s+(?:a|my|another|the)?\s*(?:new\s+)?(?:host|server|machine|node|box)s?\b/i.test(String(text || '').replace(/[.?!]+\s*$/, ''));
|
|
28
|
+
|
|
29
|
+
/** Z2H-R4: "watch my servers" and friends; watching starts with adding a host, which needs no AI. */
|
|
30
|
+
export const asksWatchHosts = (text) => /^\s*(?:please\s+)?(?:(?:i\s+(?:want|need)\s+(?:you\s+)?to|can\s+you|help\s+me)\s+)?(?:watch|monitor|keep\s+an\s+eye\s+on|look\s+after|keep\s+watch\s+(?:on|over))\s+(?:my|our|the)\s+(?:\w+\s+)?(?:servers?|hosts?|machines?|nodes?|boxes|fleet|homelab|infrastructure|infra)\b/i.test(String(text || '').replace(/[.?!]+\s*$/, ''));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The deterministic add-a-host lines. `base` = the server URL when known. `check` = the result of probing
|
|
34
|
+
* `${base}/install` ({ ok, status }) or null when it could not be read. Z2H-3c: a public install answered 503
|
|
35
|
+
* there (no QuoxAgent bundled), so the curl line we printed failed on the user's host; a failed probe now says
|
|
36
|
+
* so plainly instead of handing out a command that cannot work. Unreadable (null) keeps the old lines.
|
|
37
|
+
*/
|
|
38
|
+
export function addHostLines(base, check = null) {
|
|
39
|
+
const b = String(base || '').replace(/\/+$/, '') || '<your-quox-url>';
|
|
40
|
+
if (check && check.ok === false && Number(check.status) >= 400) {
|
|
41
|
+
return [
|
|
42
|
+
`This Quox cannot enrol hosts yet: its host installer is not available (${b}/install answered HTTP ${check.status}).`,
|
|
43
|
+
'The QuoxAgent installer is not bundled in this install, so the add-a-host command would fail on your machine.',
|
|
44
|
+
'Nothing was changed on either machine.'
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
return [
|
|
48
|
+
'To add a host, no AI needed. On the machine you want to add, run:',
|
|
49
|
+
// REL-423: never `curl | sudo bash`: that pipe exits 0 when the download fails (TLS error, HTTP error) and runs
|
|
50
|
+
// whatever partial body arrived. Download to a temp file with curl -f, then run it, so a failure stops loudly.
|
|
51
|
+
// Two lines (shell continuation) so the TUI, which clips long lines, never cuts the command short.
|
|
52
|
+
`( f=$(mktemp) && trap 'rm -f "$f"' EXIT && curl -fsSL ${b}/install -o "$f" && \\`,
|
|
53
|
+
` sudo bash "$f" --host-id <name> )`,
|
|
54
|
+
'Self-signed certificate? Run quox fleet enrol <name>: it prints this with the certificate pinned.',
|
|
55
|
+
'Then check it arrived with: quox fleet hosts'
|
|
56
|
+
];
|
|
57
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E5 "While you were away": a factual digest since a moment, from GET /api/v1/away (collector). Every
|
|
3
|
+
* item carries the source that reported it and its time; nothing is summarised by a model. Sources the
|
|
4
|
+
* server could not read, or that went silent, come back in `blind` and are always named ("Can't see"),
|
|
5
|
+
* so an empty list never reads as an all clear it cannot vouch for.
|
|
6
|
+
*/
|
|
7
|
+
import { seg, fit, hhmm, plural, boxWidth, parseTs } from './model.js';
|
|
8
|
+
|
|
9
|
+
/** Importance: approvals waiting first (they block work), then alerts, failures, then outcomes. */
|
|
10
|
+
export const RANK = {
|
|
11
|
+
approval_waiting: 0, alert: 1, turn_failed: 2, objective_failed: 2, workflow_failed: 2,
|
|
12
|
+
approval_resolved: 3, objective_finished: 4, workflow_finished: 4, decision: 5
|
|
13
|
+
};
|
|
14
|
+
const GLYPH = {
|
|
15
|
+
approval_waiting: ['⚠ ', 'warn'], alert: ['⚠ ', 'warn'], turn_failed: ['✖ ', 'error'], objective_failed: ['✖ ', 'error'],
|
|
16
|
+
workflow_failed: ['✖ ', 'error'], approval_resolved: ['✔ ', 'ok'], objective_finished: ['✔ ', 'ok'],
|
|
17
|
+
workflow_finished: ['✔ ', 'ok'], decision: ['◆ ', 'indigo']
|
|
18
|
+
};
|
|
19
|
+
const rankOf = (it) => RANK[it?.kind] ?? 6;
|
|
20
|
+
|
|
21
|
+
export function rankItems(items) {
|
|
22
|
+
return [...(items || [])].sort((a, b) => rankOf(a) - rankOf(b) || (parseTs(b.ts) ?? 0) - (parseTs(a.ts) ?? 0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** "5h", "45m", "2d" for the away window. */
|
|
26
|
+
export function awayFor(sinceTs, now) {
|
|
27
|
+
const t = parseTs(sinceTs);
|
|
28
|
+
if (t == null) return '';
|
|
29
|
+
const m = Math.max(0, Math.round((now - t) / 60000));
|
|
30
|
+
if (m < 60) return `${m}m`;
|
|
31
|
+
const h = Math.round(m / 60);
|
|
32
|
+
return h < 48 ? `${h}h` : `${Math.round(h / 24)}d`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** d: { since, items, blind, activity, compact?, max?, now } */
|
|
36
|
+
export function awayCard(d, cols) {
|
|
37
|
+
const now = d.now ?? Date.now();
|
|
38
|
+
const w = boxWidth(cols, 96), inner = w - 4;
|
|
39
|
+
const items = rankItems(d.items);
|
|
40
|
+
const max = d.compact ? (d.max ?? 5) : items.length;
|
|
41
|
+
const shown = items.slice(0, max);
|
|
42
|
+
const rows = [];
|
|
43
|
+
const win = awayFor(d.since, now);
|
|
44
|
+
if (!items.length) rows.push([seg(d.standing && d.standing.count > 0 ? 'Nothing new needed you.' : 'Nothing needed you.', 'muted')]);
|
|
45
|
+
for (const it of shown) {
|
|
46
|
+
const [g, role] = GLYPH[it.kind] || ['· ', 'muted'];
|
|
47
|
+
const meta = `${it.source || '?'} · ${hhmm(it.ts) || 'time unknown'}`;
|
|
48
|
+
if (d.compact) {
|
|
49
|
+
rows.push([seg(g, role), seg(fit(it.title, Math.max(8, inner - 2 - meta.length - 2)), 'value'), seg(` ${meta}`, 'muted')]);
|
|
50
|
+
} else {
|
|
51
|
+
rows.push([seg(g, role), seg(fit(it.title, inner - 2), 'value', rankOf(it) === 0)]);
|
|
52
|
+
rows.push([seg(' '), seg(fit(meta, inner - 2), 'muted')]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (items.length > shown.length) rows.push([seg(`+${items.length - shown.length} more · /away`, 'muted')]);
|
|
56
|
+
if (d.standing && d.standing.count > 0) rows.push([seg(fit(`${plural(d.standing.count, 'older item')} still need${d.standing.count === 1 ? 's' : ''} you · quox lucid`, inner), 'muted')]);
|
|
57
|
+
const act = d.activity;
|
|
58
|
+
if (act && act.total > 0 && !d.compact) {
|
|
59
|
+
const top = (act.agents || []).slice(0, 4).map(a => `${a.agent} ${a.count}`).join(', ');
|
|
60
|
+
rows.push([seg(' ')]);
|
|
61
|
+
rows.push([seg(fit(`${act.partial ? 'at least ' : ''}${plural(act.total, 'agent action')}${top ? `: ${top}` : ''}${act.source ? ` (${act.source})` : ''}`, inner), 'muted')]);
|
|
62
|
+
}
|
|
63
|
+
const blind = d.blind || [];
|
|
64
|
+
if (blind.length && d.compact) {
|
|
65
|
+
const what = blind.map(b => (b.reason ? `${b.source} (${b.reason})` : b.source)).join(', ');
|
|
66
|
+
rows.push([seg(fit(`Can't see: ${what}`, inner), 'faint')]);
|
|
67
|
+
} else if (blind.length) {
|
|
68
|
+
rows.push([seg("Can't see:", 'faint')]);
|
|
69
|
+
for (const b of blind) rows.push([seg(' '), seg(fit(`${b.source}${b.reason ? `: ${b.reason}` : ''}`, inner - 2), 'faint')]);
|
|
70
|
+
}
|
|
71
|
+
return { box: { width: w, title: `While you were away${win ? ` (${win})` : ''}`, titleRole: 'value', right: d.compact ? '' : `since ${hhmm(d.since)}`, rightRole: 'muted', border: items.some(i => rankOf(i) === 0) ? 'warn' : 'muted' }, rows };
|
|
72
|
+
}
|
|
@@ -11,6 +11,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { findSkill } from '../skills.js';
|
|
12
12
|
import { packFrom } from './ladder.js';
|
|
13
13
|
import { slotFor } from './agent.js';
|
|
14
|
+
import { listFailure } from '../session.js';
|
|
15
|
+
import { resolveEndpoint } from '../../api/endpoints.js';
|
|
16
|
+
import { fleetFetchers } from './fleet.js';
|
|
17
|
+
import { healthFetchers } from './health.js';
|
|
18
|
+
import { proveFetchers } from './prove.js';
|
|
19
|
+
import { replayFetchers } from './replay.js';
|
|
14
20
|
|
|
15
21
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
22
|
const ASSISTANTS_DIR = path.resolve(HERE, '../../../../src/config/entities/assistants');
|
|
@@ -31,7 +37,7 @@ export function bundledIdentities(dir = ASSISTANTS_DIR) {
|
|
|
31
37
|
return out;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
export function createObjectsApi({ client, orgId = () => null, home = os.homedir(), identities = bundledIdentities }) {
|
|
40
|
+
export function createObjectsApi({ client, orgId = () => null, home = os.homedir(), identities = bundledIdentities, profile = null }) {
|
|
35
41
|
const get = (service, p, query, timeout = 12000) => client.get(p, { service, query, retries: 0, timeout });
|
|
36
42
|
const post = (service, p, body, timeout = 8000) => client.post(p, { service, body, retries: 0, timeout });
|
|
37
43
|
const org = () => { const o = orgId(); return typeof o === 'string' ? o : null; };
|
|
@@ -66,7 +72,11 @@ export function createObjectsApi({ client, orgId = () => null, home = os.homedir
|
|
|
66
72
|
return out;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
const self = {
|
|
76
|
+
...fleetFetchers({ get, org, listOf, why, resolveEndpoint }),
|
|
77
|
+
...healthFetchers({ get, org, listOf, why, resolveEndpoint }),
|
|
78
|
+
...replayFetchers({ get, listOf, why, verifyReceipt: (id, t) => verifyReceipt(id, t), aeeGet: (id) => aeeGet(id) }),
|
|
79
|
+
...proveFetchers({ get, listOf, resolveEndpoint, self: { proof: (t) => self.proof(t) } }),
|
|
70
80
|
/** E1 */
|
|
71
81
|
async skill(name) {
|
|
72
82
|
const all = listOf(await get('collector', '/quoxflow/skills', { orgId: org() || undefined }), 'data', 'skills');
|
|
@@ -110,14 +120,15 @@ export function createObjectsApi({ client, orgId = () => null, home = os.homedir
|
|
|
110
120
|
/** E4 */
|
|
111
121
|
async agent(name) {
|
|
112
122
|
const q = String(name || '').trim().toLowerCase();
|
|
113
|
-
let toolMap = null, toolErr = null;
|
|
114
|
-
try { toolMap = (await client.get('/api/v1/tools/agents', { service: 'collector',
|
|
123
|
+
let toolMap = null, toolErr = null, toolFail = null;
|
|
124
|
+
try { toolMap = (await client.get('/api/v1/tools/agents', { service: 'collector', retries: 0, timeout: 10000 }))?.agentTools || null; } catch (e) { toolErr = why(e); toolFail = e; }
|
|
115
125
|
let orgAgents = [];
|
|
116
126
|
try { if (org()) orgAgents = listOf(await get('auth', `/api/orgs/${org()}/agents`, { includeTools: 'false' }), 'agents', 'data'); } catch { /* optional */ }
|
|
117
127
|
const known = new Set([...Object.keys(toolMap || {}), ...idents().keys(), ...orgAgents.map(a => a.agent_id)]);
|
|
118
128
|
let id = [...known].find(k => k.toLowerCase() === q);
|
|
119
129
|
if (!id) id = [...known].find(k => String(nameOf(k)).toLowerCase() === q) || orgAgents.find(a => String(a.name || '').toLowerCase() === q)?.agent_id;
|
|
120
|
-
if (!id) return { error: `
|
|
130
|
+
if (!id && toolFail) return { error: `Couldn't check agents: ${listFailure(toolFail, profile)}`, tone: 'warn' }; // a failed list is a failure, never "no such agent"
|
|
131
|
+
if (!id) return { error: `No agent called "${name}".`, tone: 'warn' };
|
|
121
132
|
const record = orgAgents.find(a => a.agent_id === id) || null;
|
|
122
133
|
const ident = idents().get(id) || {};
|
|
123
134
|
let slot = null;
|
|
@@ -167,4 +178,5 @@ export function createObjectsApi({ client, orgId = () => null, home = os.homedir
|
|
|
167
178
|
return { data: { items } };
|
|
168
179
|
}
|
|
169
180
|
};
|
|
181
|
+
return self;
|
|
170
182
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /fleet: computers, agents, missions and SubFleets in one compact block, with health. Read-only.
|
|
3
|
+
* Sources (the same routes `quox fleet ...` and `quox objective list` use): GET /api/v1/agents
|
|
4
|
+
* (heartbeats), GET /api/v1/fleet/hosts (Bastion registry), org agents, GET /api/objectives. A source
|
|
5
|
+
* that could not be read is named on the "Can't see" line, never counted as zero. SubFleets have an
|
|
6
|
+
* RFC but no API yet, so that row says so. Moving work is not here: the hint points at plain words.
|
|
7
|
+
*/
|
|
8
|
+
import { seg, fit, fitRow, ago, plural, boxWidth } from './model.js';
|
|
9
|
+
import { fromObjective } from '../roster.js';
|
|
10
|
+
|
|
11
|
+
const OFFLINE = /^(dead|offline|down|disconnected|unreachable)$/;
|
|
12
|
+
const HEALTHY = /^(healthy|ok|good)$/;
|
|
13
|
+
const DEGRADED = /^(warning|warn|degraded|stale)$/;
|
|
14
|
+
const UNHEALTHY = /^(critical|unhealthy|error|failed|fail)$/;
|
|
15
|
+
const FLEET_OVERVIEW_WORDS = new Set(['health', 'status', 'list', 'all', 'overview', 'summary', 'hosts', 'agents', 'quoxagents']);
|
|
16
|
+
|
|
17
|
+
export const ORDER = ['healthy', 'degraded', 'unhealthy', 'offline', 'unknown'];
|
|
18
|
+
const SEVERITY = { unhealthy: 0, offline: 1, degraded: 2, unknown: 3, healthy: 4 };
|
|
19
|
+
|
|
20
|
+
/** A heartbeat row or a Bastion host row -> healthy | degraded | unhealthy | offline | unknown. */
|
|
21
|
+
export function healthOf(row) {
|
|
22
|
+
const s = row?.status;
|
|
23
|
+
const st = String(typeof s === 'object' && s ? s.state ?? '' : s ?? '').toLowerCase();
|
|
24
|
+
const h = String(typeof s === 'object' && s ? s.health ?? '' : '').toLowerCase();
|
|
25
|
+
if (OFFLINE.test(st) || OFFLINE.test(h)) return 'offline';
|
|
26
|
+
if (UNHEALTHY.test(h) || UNHEALTHY.test(st)) return 'unhealthy';
|
|
27
|
+
if (DEGRADED.test(h) || DEGRADED.test(st)) return 'degraded';
|
|
28
|
+
if (HEALTHY.test(h) || HEALTHY.test(st)) return 'healthy';
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const listOk = (s) => (s?.ok ? s.list || [] : null);
|
|
33
|
+
|
|
34
|
+
/** d: { agents, hosts, orgAgents, missions } each { ok, list } | { ok:false, error } -> merged view model. */
|
|
35
|
+
export function collectFleet(d, now = Date.now()) {
|
|
36
|
+
const byId = new Map();
|
|
37
|
+
const cant = [];
|
|
38
|
+
const sources = [['heartbeats', d.agents], ['hosts', d.hosts]];
|
|
39
|
+
for (const [label, src] of sources) {
|
|
40
|
+
if (!src) continue;
|
|
41
|
+
if (!src.ok) { cant.push(`${label} (${src.error || 'failed'})`); continue; }
|
|
42
|
+
for (const r of src.list || []) {
|
|
43
|
+
const id = r.host_id || r.id || r.hostname;
|
|
44
|
+
if (!id) continue;
|
|
45
|
+
const cur = byId.get(id) || { id };
|
|
46
|
+
if (label === 'heartbeats') {
|
|
47
|
+
const s = r.status;
|
|
48
|
+
byId.set(id, { ...cur, health: healthOf(r), version: r.version ?? cur.version, lastSeenS: typeof s === 'object' ? s?.last_seen_seconds : undefined, heartbeat: true });
|
|
49
|
+
} else {
|
|
50
|
+
byId.set(id, { ...cur, ip: r.ip ?? cur.ip, group: r.group ?? cur.group, registryHealth: healthOf(r), health: cur.heartbeat ? cur.health : healthOf(r) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const computers = [...byId.values()].sort((a, b) => SEVERITY[a.health] - SEVERITY[b.health] || a.id.localeCompare(b.id));
|
|
55
|
+
const counts = Object.fromEntries(ORDER.map(k => [k, 0]));
|
|
56
|
+
for (const c of computers) counts[c.health]++;
|
|
57
|
+
const org = listOk(d.orgAgents);
|
|
58
|
+
if (d.orgAgents && !d.orgAgents.ok) cant.push(`agents (${d.orgAgents.error || 'failed'})`);
|
|
59
|
+
const agents = org ? { active: org.filter(a => !a.status || a.status === 'active').length, other: org.filter(a => a.status && a.status !== 'active').length } : null;
|
|
60
|
+
let missions = null;
|
|
61
|
+
if (d.missions && !d.missions.ok) cant.push(`missions (${d.missions.error || 'failed'})`);
|
|
62
|
+
else if (d.missions) {
|
|
63
|
+
const rows = (d.missions.list || []).map(o => fromObjective(o, now)).filter(Boolean);
|
|
64
|
+
missions = { running: rows.filter(r => r.state === 'running').length, waiting: rows.filter(r => r.state === 'waiting').length, loops: rows.filter(r => r.kind === 'loop' && (r.state === 'running' || r.state === 'waiting')).length, rows: rows.filter(r => r.state === 'running' || r.state === 'waiting') };
|
|
65
|
+
}
|
|
66
|
+
return { computers, counts, agents, missions, cant };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ROLE = { healthy: 'ok', degraded: 'warn', unhealthy: 'error', offline: 'error', unknown: 'muted' };
|
|
70
|
+
const HINT = 'ask in plain words to move work';
|
|
71
|
+
|
|
72
|
+
/** d: { agents, hosts, orgAgents, missions, now } -> view { box, rows } */
|
|
73
|
+
export function fleetCard(d, cols) {
|
|
74
|
+
const now = d.now ?? Date.now();
|
|
75
|
+
const f = collectFleet(d, now);
|
|
76
|
+
const w = boxWidth(cols, 78), inner = w - 4;
|
|
77
|
+
const L = (label, parts) => [seg(label.padEnd(12), 'muted'), ...parts];
|
|
78
|
+
const rows = [];
|
|
79
|
+
if (f.computers.length || (d.agents?.ok || d.hosts?.ok)) {
|
|
80
|
+
const bits = [];
|
|
81
|
+
for (const k of ORDER) if (f.counts[k]) { if (bits.length) bits.push(seg(' · ', 'muted')); bits.push(seg(`${f.counts[k]} ${k}`, ROLE[k], k !== 'healthy')); }
|
|
82
|
+
rows.push(L('Computers', [seg(`${f.computers.length} `, 'value', true), ...bits]));
|
|
83
|
+
const bad = f.computers.filter(c => c.health !== 'healthy');
|
|
84
|
+
if (bad.length) rows.push([seg('not healthy'.padEnd(12), 'muted'), seg(fit(bad.map(c => `${c.id} ${c.health}`).join(', '), inner - 12), 'warn')]);
|
|
85
|
+
} else rows.push(L('Computers', [seg('?', 'warn'), seg(' not read', 'muted')]));
|
|
86
|
+
rows.push(f.agents ? L('Agents', [seg(`${f.agents.active} active`, 'value'), ...(f.agents.other ? [seg(` · ${f.agents.other} other`, 'muted')] : [])]) : L('Agents', [seg('?', 'warn')]));
|
|
87
|
+
if (f.missions) {
|
|
88
|
+
const m = f.missions;
|
|
89
|
+
const extra = [m.waiting ? plural(m.waiting, 'waiting', 'waiting') : null, m.loops ? plural(m.loops, 'loop') : null].filter(Boolean);
|
|
90
|
+
rows.push(L('Missions', [seg(`${m.running} running`, 'value'), ...(extra.length ? [seg(` · ${extra.join(' · ')}`, 'muted')] : [])]));
|
|
91
|
+
for (const r of m.rows.slice(0, 3)) rows.push([seg(' ', 'muted'), seg(fit(`${r.name}${r.activity ? ` · ${r.activity}` : ''}`, inner - 2), 'muted')]);
|
|
92
|
+
if (m.rows.length > 3) rows.push([seg(` …and ${m.rows.length - 3} more`, 'muted')]);
|
|
93
|
+
} else rows.push(L('Missions', [seg('?', 'warn')]));
|
|
94
|
+
rows.push(L('SubFleets', [seg('none yet', 'muted'), seg(w < 60 ? '' : ' (designed, no API in this Quox)', 'muted')]));
|
|
95
|
+
if (f.cant.length) rows.push([seg("Can't see ", 'warn'), seg(fit(f.cant.join(', '), inner - 12), 'warn')]);
|
|
96
|
+
rows.push([seg(HINT, 'muted')]);
|
|
97
|
+
return { box: { width: w, title: 'Fleet', titleRole: 'sky', border: 'sky' }, rows: rows.map(r => fitRow(r, inner)) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** d: { host, now } -> view for one computer. */
|
|
101
|
+
export function hostCard(d, cols) {
|
|
102
|
+
const h = d.host;
|
|
103
|
+
const w = boxWidth(cols, 66), inner = w - 4;
|
|
104
|
+
const L = (label, v, role = 'value') => [seg(label.padEnd(8), 'muted'), seg(fit(v, inner - 8), role)];
|
|
105
|
+
const rows = [L('health', h.health, ROLE[h.health])];
|
|
106
|
+
if (h.ip) rows.push(L('ip', h.ip));
|
|
107
|
+
if (h.group) rows.push(L('group', h.group));
|
|
108
|
+
if (h.version) rows.push(L('version', h.version));
|
|
109
|
+
if (typeof h.lastSeenS === 'number') rows.push(L('seen', `${ago(Date.now() - h.lastSeenS * 1000, Date.now()) || '0s'} ago`, 'muted'));
|
|
110
|
+
if (!h.heartbeat) rows.push(L('agent', 'no heartbeat (in the registry only)', 'muted'));
|
|
111
|
+
rows.push([seg(HINT, 'muted')]);
|
|
112
|
+
return { box: { width: w, title: h.id, titleRole: 'sky', right: h.health, rightRole: ROLE[h.health], border: 'sky' }, rows };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Builder registered as BUILDERS.fleet: one host when d.host is set, else the whole fleet. */
|
|
116
|
+
export function fleetView(d, cols) { return d.host ? hostCard(d, cols) : fleetCard(d, cols); }
|
|
117
|
+
|
|
118
|
+
/** Fetchers for createObjectsApi (kept here so fetch.js only spreads them in). */
|
|
119
|
+
export function fleetFetchers({ get, org, listOf, why, resolveEndpoint }) {
|
|
120
|
+
const read = async (fn) => { try { return { ok: true, list: await fn() }; } catch (e) { return { ok: false, error: why(e) }; } };
|
|
121
|
+
return {
|
|
122
|
+
/** Host ids for /fleet <TAB>: [{ value, desc }]; a failed read suggests nothing. */
|
|
123
|
+
async fleetNames() {
|
|
124
|
+
const hb = resolveEndpoint('v1.agents.list');
|
|
125
|
+
const rows = listOf(await get(hb.service, hb.path, undefined, 6000), 'agents');
|
|
126
|
+
return rows.map(r => r.host_id || r.id).filter(Boolean).map(id => ({ value: String(id), desc: healthOf(rows.find(r => (r.host_id || r.id) === id)) }));
|
|
127
|
+
},
|
|
128
|
+
/** E-fleet: name '' = the whole fleet, else one computer by id (case-insensitive, then prefix). */
|
|
129
|
+
async fleet(name) {
|
|
130
|
+
const q = String(name || '').trim().toLowerCase();
|
|
131
|
+
const hb = resolveEndpoint('v1.agents.list'), hs = resolveEndpoint('v1.fleet.hosts.list'), ob = resolveEndpoint('objectives.list');
|
|
132
|
+
const [agents, hosts, orgAgents, missions] = await Promise.all([
|
|
133
|
+
read(async () => listOf(await get(hb.service, hb.path, undefined, 10000), 'agents')),
|
|
134
|
+
read(async () => listOf(await get(hs.service, hs.path, undefined, 10000), 'hosts')),
|
|
135
|
+
org() ? read(async () => { const ep = resolveEndpoint('agents.list', { orgId: org() }); return listOf(await get(ep.service, ep.path, { includeTools: 'false' }, 10000), 'agents', 'data'); }) : Promise.resolve(null),
|
|
136
|
+
read(async () => listOf(await get(ob.service, ob.path, { limit: 100 }, 10000), 'objectives'))
|
|
137
|
+
]);
|
|
138
|
+
const data = { agents, hosts, orgAgents, missions, now: Date.now() };
|
|
139
|
+
if (!q) return { data };
|
|
140
|
+
const f = collectFleet(data, data.now);
|
|
141
|
+
const host = f.computers.find(c => c.id.toLowerCase() === q) || f.computers.find(c => c.id.toLowerCase().startsWith(q));
|
|
142
|
+
if (host) return { data: { host, now: data.now } };
|
|
143
|
+
// owner 2026-10-15 typed "/fleet health": overview words mean the whole fleet (a real host by that name still wins above)
|
|
144
|
+
if (FLEET_OVERVIEW_WORDS.has(q)) return { data };
|
|
145
|
+
if (!agents.ok && !hosts.ok) return { error: `Couldn't check hosts: ${agents.error}.`, tone: 'warn' }; // a failed list is never "no such host"
|
|
146
|
+
return { error: `No host called "${name}". /fleet lists them.`, tone: 'warn' };
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /health (and /core): a compact summary of what the Core dashboard's overview (/dashboard) shows.
|
|
3
|
+
* Read-only, no model call, server-side reads only (the same truth the dashboard reads):
|
|
4
|
+
* services GET collector /api/v1/health/aggregate (the dashboard's service ring)
|
|
5
|
+
* computers GET /api/v1/agents heartbeats + GET /api/v1/fleet/hosts (as /fleet)
|
|
6
|
+
* agents org agents (as /fleet)
|
|
7
|
+
* approvals GET auth /api/orgs/:org/inbox/stats (the dashboard's "Attention needed")
|
|
8
|
+
* missions GET tasks /api/objectives (the dashboard's live objectives)
|
|
9
|
+
* failures GET quoxflow /api/executions?status=failed&since=24h
|
|
10
|
+
* licence GET auth /api/setup/licensing/entitlements
|
|
11
|
+
* A section whose read failed says "unavailable: <reason>"; an error body is never shown as zero.
|
|
12
|
+
* /status stays the session panel; /collector-health still goes to /status.
|
|
13
|
+
*/
|
|
14
|
+
import { seg, fit, fitRow, plural, boxWidth } from './model.js';
|
|
15
|
+
import { collectFleet, fleetFetchers } from './fleet.js';
|
|
16
|
+
|
|
17
|
+
const DAY_MS = 24 * 3600 * 1000;
|
|
18
|
+
const SVC_ROLE = { ok: 'ok', degraded: 'warn', down: 'error' };
|
|
19
|
+
const reasons = (...srcs) => [...new Set(srcs.filter(s => s && !s.ok).map(s => s.error || 'failed'))].join(', ');
|
|
20
|
+
|
|
21
|
+
/** d: { aggregate, agents, hosts, orgAgents, missions, inbox, failures, licence, now } -> per-section model ({ error } when unread). */
|
|
22
|
+
export function collectHealth(d) {
|
|
23
|
+
const now = d.now ?? Date.now();
|
|
24
|
+
const out = {};
|
|
25
|
+
|
|
26
|
+
const a = d.aggregate;
|
|
27
|
+
if (!a?.ok) out.services = { error: a?.error || 'not read' };
|
|
28
|
+
else {
|
|
29
|
+
const all = Object.entries(a.data?.services || {});
|
|
30
|
+
// Z2H run-6: an optional service this install does not ship (collector says not_installed) is neither up nor down.
|
|
31
|
+
const entries = all.filter(([, s]) => s?.status !== 'not_installed');
|
|
32
|
+
out.services = {
|
|
33
|
+
up: entries.filter(([, s]) => s?.status === 'ok').length,
|
|
34
|
+
total: entries.length,
|
|
35
|
+
score: typeof a.data?.score === 'number' ? a.data.score : null,
|
|
36
|
+
bad: entries.filter(([, s]) => s?.status !== 'ok').map(([name, s]) => ({ name, status: s?.status || 'unknown' })),
|
|
37
|
+
notInstalled: all.filter(([, s]) => s?.status === 'not_installed').map(([name]) => name),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const f = collectFleet({ agents: d.agents, hosts: d.hosts, orgAgents: d.orgAgents, missions: d.missions }, now);
|
|
42
|
+
if (d.agents?.ok || d.hosts?.ok) {
|
|
43
|
+
out.computers = { healthy: f.counts.healthy, total: f.computers.length, bad: f.computers.filter(c => c.health !== 'healthy').map(c => ({ id: c.id, health: c.health })), partial: reasons(d.agents, d.hosts) || null };
|
|
44
|
+
} else out.computers = { error: reasons(d.agents, d.hosts) || 'not read' };
|
|
45
|
+
|
|
46
|
+
if (!d.orgAgents) out.agents = { error: 'no org selected' };
|
|
47
|
+
else out.agents = f.agents ? { ...f.agents } : { error: d.orgAgents.error || 'failed' };
|
|
48
|
+
|
|
49
|
+
out.missions = f.missions ? { running: f.missions.running, waiting: f.missions.waiting, loops: f.missions.loops } : { error: d.missions?.error || 'not read' };
|
|
50
|
+
|
|
51
|
+
const ib = d.inbox;
|
|
52
|
+
if (!ib?.ok) out.approvals = { error: ib?.error || 'not read' };
|
|
53
|
+
else {
|
|
54
|
+
const s = ib.data || {};
|
|
55
|
+
out.approvals = { approvals: Number(s.pending_approvals) || 0, active: Number(s.total_active) || 0, urgent: Number(s.urgent) || 0, overdue: Number(s.overdue) || 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fl = d.failures;
|
|
59
|
+
if (!fl?.ok) out.failures = { error: fl?.error || 'not read' };
|
|
60
|
+
else {
|
|
61
|
+
const list = fl.list || [];
|
|
62
|
+
const r = list[0];
|
|
63
|
+
out.failures = { count: typeof fl.total === 'number' ? fl.total : list.length, last: r ? (r.workflowName || r.workflow_name || r.name || r.workflowId || r.workflow_id || r.id || null) : null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const li = d.licence;
|
|
67
|
+
if (!li?.ok) out.licence = { error: li?.error || 'not read' };
|
|
68
|
+
else {
|
|
69
|
+
const L = li.data || {};
|
|
70
|
+
const leases = Array.isArray(L.licenses) ? L.licenses : [];
|
|
71
|
+
const plugins = Array.isArray(L.entitlements) ? L.entitlements.length : 0;
|
|
72
|
+
const errors = Array.isArray(L.entitlement_errors) ? L.entitlement_errors.length : 0;
|
|
73
|
+
if (!leases.length) out.licence = { core: true, plugins, errors };
|
|
74
|
+
else {
|
|
75
|
+
const plans = [...new Set(leases.flatMap(x => Array.isArray(x.plans) ? x.plans : []))];
|
|
76
|
+
const exp = leases.map(x => Date.parse(x.expires_at)).filter(Number.isFinite).sort((x, y) => x - y)[0];
|
|
77
|
+
out.licence = { core: false, plans, plugins, errors, expiresAt: Number.isFinite(exp) ? exp : null, expired: Number.isFinite(exp) && exp < now };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Overall verdict for the title: partial (a section unread) > needs attention > all good. */
|
|
84
|
+
function verdict(h) {
|
|
85
|
+
if (Object.values(h).some(s => s?.error)) return ['partial', 'warn'];
|
|
86
|
+
const attention = h.services.up < h.services.total || h.computers.bad.length || h.failures.count || h.approvals.approvals || h.approvals.urgent || h.approvals.overdue || h.licence.errors || h.licence.expired;
|
|
87
|
+
return attention ? ['needs attention', 'warn'] : ['all good', 'ok'];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const HINT = [seg('/fleet', 'key'), seg(' computers · ', 'label'), seg('/status', 'key'), seg(' this session · ', 'label'), seg('/dashboard', 'key'), seg(' open it', 'label')];
|
|
91
|
+
|
|
92
|
+
/** d -> view { box, rows }. */
|
|
93
|
+
export function healthCard(d, cols) {
|
|
94
|
+
const h = collectHealth(d);
|
|
95
|
+
const w = boxWidth(cols, 78), inner = w - 4;
|
|
96
|
+
const L = (label, parts) => [seg(label.padEnd(11), 'label'), ...parts];
|
|
97
|
+
const na = (s) => [seg('unavailable: ', 'warn'), seg(s.error, 'warn')];
|
|
98
|
+
const sep = () => seg(' · ', 'label');
|
|
99
|
+
const rows = [];
|
|
100
|
+
|
|
101
|
+
const s = h.services;
|
|
102
|
+
if (s.error) rows.push(L('Services', na(s)));
|
|
103
|
+
else {
|
|
104
|
+
const parts = [seg(`${s.up}/${s.total} up`, s.up === s.total ? 'ok' : 'warn', true)];
|
|
105
|
+
for (const b of s.bad) parts.push(sep(), seg(`${b.name} ${b.status}`, SVC_ROLE[b.status] || 'warn'));
|
|
106
|
+
for (const n of s.notInstalled || []) parts.push(sep(), seg(`${n} not installed`, 'value'));
|
|
107
|
+
rows.push(L('Services', parts));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const c = h.computers;
|
|
111
|
+
if (c.error) rows.push(L('Computers', na(c)));
|
|
112
|
+
else if (!c.total) rows.push(L('Computers', [seg('none connected', 'value')]));
|
|
113
|
+
else {
|
|
114
|
+
const parts = [seg(`${c.healthy}/${c.total} healthy`, c.healthy === c.total ? 'ok' : 'warn', true)];
|
|
115
|
+
if (c.bad.length) parts.push(sep(), seg(fit(c.bad.map(x => `${x.id} ${x.health}`).join(', '), inner), c.bad.some(x => x.health === 'offline' || x.health === 'unhealthy') ? 'error' : 'warn'));
|
|
116
|
+
if (c.partial) parts.push(sep(), seg(`partial: ${c.partial}`, 'warn'));
|
|
117
|
+
rows.push(L('Computers', parts));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const ag = h.agents;
|
|
121
|
+
rows.push(L('Agents', ag.error ? na(ag) : [seg(`${ag.active} active`, 'value', true), ...(ag.other ? [sep(), seg(`${ag.other} other`, 'value')] : [])]));
|
|
122
|
+
|
|
123
|
+
const ap = h.approvals;
|
|
124
|
+
if (ap.error) rows.push(L('Approvals', na(ap)));
|
|
125
|
+
else {
|
|
126
|
+
const parts = [seg(ap.approvals ? `${ap.approvals} open` : 'none open', ap.approvals ? 'warn' : 'ok', true)];
|
|
127
|
+
if (ap.active) parts.push(sep(), seg(`${ap.active} in inbox`, 'value'));
|
|
128
|
+
if (ap.urgent) parts.push(sep(), seg(`${ap.urgent} urgent`, 'error'));
|
|
129
|
+
if (ap.overdue) parts.push(sep(), seg(`${ap.overdue} overdue`, 'error'));
|
|
130
|
+
rows.push(L('Approvals', parts));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const m = h.missions;
|
|
134
|
+
if (m.error) rows.push(L('Missions', na(m)));
|
|
135
|
+
else {
|
|
136
|
+
const parts = [seg(`${m.running} running`, 'value', true)];
|
|
137
|
+
if (m.waiting) parts.push(sep(), seg(`${m.waiting} waiting`, 'warn'));
|
|
138
|
+
if (m.loops) parts.push(sep(), seg(plural(m.loops, 'loop'), 'value'));
|
|
139
|
+
rows.push(L('Missions', parts));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fl = h.failures;
|
|
143
|
+
if (fl.error) rows.push(L('Failures', na(fl)));
|
|
144
|
+
else if (!fl.count) rows.push(L('Failures', [seg('none in 24h', 'ok')]));
|
|
145
|
+
else rows.push(L('Failures', [seg(`${plural(fl.count, 'failed run')} in 24h`, 'error', true), ...(fl.last ? [sep(), seg(`last: ${fl.last}`, 'value')] : [])]));
|
|
146
|
+
|
|
147
|
+
const li = h.licence;
|
|
148
|
+
if (li.error) rows.push(L('Licence', na(li)));
|
|
149
|
+
else {
|
|
150
|
+
const parts = li.core ? [seg('Core (free)', 'value', true)] : [seg(li.plans.length ? li.plans.join(', ') : 'licensed', 'value', true)];
|
|
151
|
+
if (li.plugins) parts.push(sep(), seg(plural(li.plugins, 'plugin'), 'value'));
|
|
152
|
+
if (!li.core && li.expiresAt) parts.push(sep(), seg(li.expired ? `expired ${new Date(li.expiresAt).toISOString().slice(0, 10)}` : `until ${new Date(li.expiresAt).toISOString().slice(0, 10)}`, li.expired ? 'error' : 'value'));
|
|
153
|
+
if (li.errors) parts.push(sep(), seg(plural(li.errors, 'licence error'), 'error'));
|
|
154
|
+
rows.push(L('Licence', parts));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
rows.push(HINT);
|
|
158
|
+
const [right, rightRole] = verdict(h);
|
|
159
|
+
return { box: { width: w, title: 'Health', titleRole: 'sky', right, rightRole, border: 'sky' }, rows: rows.map(r => fitRow(r, inner)) };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Fetchers for createObjectsApi. Every read is bounded and every failure carries its reason. */
|
|
163
|
+
export function healthFetchers({ get, org, listOf, why, resolveEndpoint }) {
|
|
164
|
+
const fleet = fleetFetchers({ get, org, listOf, why, resolveEndpoint });
|
|
165
|
+
const read = async (fn) => { try { return { ok: true, ...(await fn()) }; } catch (e) { return { ok: false, error: why(e) }; } };
|
|
166
|
+
const noOrg = { ok: false, error: 'no org selected' };
|
|
167
|
+
return {
|
|
168
|
+
/** -> { data } for the health card (now injectable for tests). */
|
|
169
|
+
async health(now = Date.now()) {
|
|
170
|
+
const ag = resolveEndpoint('v1.health.aggregate.list'), ex = resolveEndpoint('executions.list'), lic = resolveEndpoint('setup.licensing.entitlements.list');
|
|
171
|
+
const o = org();
|
|
172
|
+
const [fl, aggregate, inbox, failures, licence] = await Promise.all([
|
|
173
|
+
fleet.fleet(''),
|
|
174
|
+
read(async () => ({ data: await get(ag.service, ag.path, undefined, 15000) })),
|
|
175
|
+
o ? read(async () => { const ep = resolveEndpoint('inbox.stats.list', { orgId: o }); return { data: await get(ep.service, ep.path, undefined, 8000) }; }) : Promise.resolve(noOrg),
|
|
176
|
+
read(async () => {
|
|
177
|
+
const d = await get(ex.service, ex.path, { status: 'failed', since: new Date(now - DAY_MS).toISOString(), limit: 5 }, 8000);
|
|
178
|
+
const total = d?.pagination?.total;
|
|
179
|
+
return { list: listOf(d, 'data', 'executions'), total: typeof total === 'number' ? total : undefined };
|
|
180
|
+
}),
|
|
181
|
+
read(async () => ({ data: await get(lic.service, lic.path, undefined, 8000) })),
|
|
182
|
+
]);
|
|
183
|
+
return { data: { ...fl.data, aggregate, inbox, failures, licence, now } };
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -11,12 +11,20 @@ import { agentCard } from './agent.js';
|
|
|
11
11
|
import { memoryFacts } from './memory.js';
|
|
12
12
|
import { rulesLadder } from './ladder.js';
|
|
13
13
|
import { proofChain, reconcileView } from './proof.js';
|
|
14
|
+
import { awayCard } from './away.js';
|
|
15
|
+
import { sitrepCard } from './sitrep.js';
|
|
16
|
+
import { fleetView } from './fleet.js';
|
|
17
|
+
import { healthCard } from './health.js';
|
|
18
|
+
import { proveView, lastTurnCalls } from './prove.js';
|
|
19
|
+
import { replayView, parseReplayArg } from './replay.js';
|
|
20
|
+
import { mirrorCard } from './mirror.js';
|
|
21
|
+
import { peekCard } from './peek.js';
|
|
14
22
|
|
|
15
|
-
export { skillCard, stepFlow, crystalCard, agentCard, memoryFacts, rulesLadder, proofChain, reconcileView };
|
|
23
|
+
export { skillCard, stepFlow, crystalCard, agentCard, memoryFacts, rulesLadder, proofChain, reconcileView, awayCard, sitrepCard };
|
|
16
24
|
export { createObjectsApi } from './fetch.js';
|
|
17
25
|
export { decodeRow, arrivalSeconds } from './model.js';
|
|
18
26
|
|
|
19
|
-
export const BUILDERS = { skill: skillCard, workflow: stepFlow, crystal: crystalCard, agent: agentCard, memory: memoryFacts, rules: rulesLadder, proof: proofChain, reconcile: reconcileView };
|
|
27
|
+
export const BUILDERS = { skill: skillCard, workflow: stepFlow, crystal: crystalCard, agent: agentCard, memory: memoryFacts, rules: rulesLadder, proof: proofChain, reconcile: reconcileView, away: awayCard, sitrep: sitrepCard, fleet: fleetView, health: healthCard, prove: proveView, replay: replayView, mirror: mirrorCard, peek: peekCard };
|
|
20
28
|
|
|
21
29
|
/** -> view { box?, rows } for an object item at this width (now injected for tests). */
|
|
22
30
|
export function buildObject(obj, cols, now = Date.now()) {
|
|
@@ -46,7 +54,8 @@ export async function runObjectCommand(cmd, arg, ctx) {
|
|
|
46
54
|
const o = ctx.objects;
|
|
47
55
|
const a = String(arg || '').trim();
|
|
48
56
|
// A bare /skills or /agents is a list, not an object: leave it to its own handler.
|
|
49
|
-
if (!['/workflow', '/crystal', '/memory', '/rules', '/proof', '/reconcile'].includes(cmd) && !((cmd === '/skills' || cmd === '/agents') && a)) return null;
|
|
57
|
+
if (!['/workflow', '/crystal', '/memory', '/rules', '/proof', '/reconcile', '/fleet', '/prove', '/replay', '/health', '/core'].includes(cmd) && !((cmd === '/skills' || cmd === '/agents') && a)) return null;
|
|
58
|
+
if (!o?.health && (cmd === '/health' || cmd === '/core')) return null; // no objects api (demo, local): app.js shows /status instead
|
|
50
59
|
if (!o) return cmd === '/rules' || cmd === '/skills' ? null : { note: 'Object views are not available in this mode.', tone: 'warn' };
|
|
51
60
|
const wrap = async (type, fn) => {
|
|
52
61
|
try {
|
|
@@ -63,6 +72,18 @@ export async function runObjectCommand(cmd, arg, ctx) {
|
|
|
63
72
|
case '/workflow': return a ? wrap('workflow', () => o.workflowRun(a)) : { note: USAGE['/workflow'], tone: 'warn' };
|
|
64
73
|
case '/crystal': return a ? wrap('crystal', () => o.crystal(a)) : { note: USAGE['/crystal'], tone: 'warn' };
|
|
65
74
|
case '/memory': return a ? wrap('memory', () => o.memory(a)) : { note: USAGE['/memory'], tone: 'warn' };
|
|
75
|
+
case '/replay': {
|
|
76
|
+
const { ref, page } = parseReplayArg(a);
|
|
77
|
+
if (!ref && !ctx.conversationId && !toolCalls(ctx.items).length) return { note: 'Nothing to replay yet: no turn in this conversation. /replay <run or conversation id> replays an earlier one.' };
|
|
78
|
+
return wrap('replay', () => o.replay({ ref, page, conversationId: ctx.conversationId || null, calls: lastTurnCalls(ctx.items) }));
|
|
79
|
+
}
|
|
80
|
+
case '/prove': {
|
|
81
|
+
const calls = lastTurnCalls(ctx.items);
|
|
82
|
+
if (!calls.length) return { note: 'No tool calls in the last turn: nothing to prove.' };
|
|
83
|
+
return wrap('prove', () => o.prove(calls));
|
|
84
|
+
}
|
|
85
|
+
case '/fleet': return wrap('fleet', () => o.fleet(a));
|
|
86
|
+
case '/health': case '/core': return wrap('health', () => o.health()); // the Core dashboard summary (objects/health.js)
|
|
66
87
|
case '/rules': {
|
|
67
88
|
return wrap('rules', async () => {
|
|
68
89
|
const r = await o.rules();
|