@ouro.bot/cli 0.1.0-alpha.44 → 0.1.0-alpha.441
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/README.md +127 -19
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
- package/changelog.json +2789 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +140 -0
- package/dist/arc/episodes.js +117 -0
- package/dist/arc/intentions.js +133 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +237 -0
- package/dist/arc/packets.js +193 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +65 -0
- package/dist/heart/active-work.js +832 -0
- package/dist/heart/agent-entry.js +58 -3
- package/dist/heart/attachments/image-normalize.js +194 -0
- package/dist/heart/attachments/materialize.js +97 -0
- package/dist/heart/attachments/originals.js +88 -0
- package/dist/heart/attachments/render.js +29 -0
- package/dist/heart/attachments/sources/adapter.js +2 -0
- package/dist/heart/attachments/sources/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +16 -0
- package/dist/heart/attachments/store.js +103 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +426 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +111 -0
- package/dist/heart/config-registry.js +304 -0
- package/dist/heart/config.js +110 -128
- package/dist/heart/core.js +745 -227
- package/dist/heart/cross-chat-delivery.js +131 -0
- package/dist/heart/daemon/agent-config-check.js +439 -0
- package/dist/heart/daemon/agent-discovery.js +79 -3
- package/dist/heart/daemon/agent-service.js +360 -0
- package/dist/heart/daemon/agentic-repair.js +214 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/cli-defaults.js +626 -0
- package/dist/heart/daemon/cli-exec.js +5646 -0
- package/dist/heart/daemon/cli-help.js +428 -0
- package/dist/heart/daemon/cli-parse.js +1156 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +561 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/connect-bay.js +457 -0
- package/dist/heart/daemon/daemon-cli.js +28 -1582
- package/dist/heart/daemon/daemon-entry.js +356 -3
- package/dist/heart/daemon/daemon-health.js +141 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +175 -12
- package/dist/heart/daemon/daemon-tombstone.js +236 -0
- package/dist/heart/daemon/daemon.js +684 -58
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +427 -0
- package/dist/heart/daemon/health-monitor.js +79 -1
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/human-command-screens.js +236 -0
- package/dist/heart/daemon/human-readiness.js +114 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/interactive-repair.js +307 -0
- package/dist/heart/daemon/launchd.js +46 -9
- package/dist/heart/daemon/log-tailer.js +82 -12
- package/dist/heart/daemon/logs-prune.js +105 -0
- package/dist/heart/daemon/message-router.js +2 -2
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/ouro-bot-entry.js +4 -2
- package/dist/heart/daemon/ouro-entry.js +3 -1
- package/dist/heart/daemon/process-manager.js +214 -0
- package/dist/heart/daemon/provider-discovery.js +137 -0
- package/dist/heart/daemon/provider-ping-progress.js +76 -0
- package/dist/heart/daemon/pulse.js +475 -0
- package/dist/heart/daemon/readiness-repair.js +365 -0
- package/dist/heart/daemon/run-hooks.js +2 -0
- package/dist/heart/daemon/runtime-logging.js +67 -16
- package/dist/heart/daemon/runtime-metadata.js +73 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sense-manager.js +145 -32
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +307 -0
- package/dist/heart/daemon/stale-bundle-prune.js +96 -0
- package/dist/heart/daemon/startup-tui.js +272 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/terminal-ui.js +224 -0
- package/dist/heart/daemon/thoughts.js +510 -0
- package/dist/heart/daemon/up-progress.js +313 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/habits/habit-migration.js +189 -0
- package/dist/heart/habits/habit-parser.js +140 -0
- package/dist/heart/habits/habit-runtime-state.js +100 -0
- package/dist/heart/habits/habit-scheduler.js +372 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +52 -117
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
- package/dist/heart/{daemon → hatch}/specialist-tools.js +35 -12
- package/dist/heart/identity.js +197 -65
- package/dist/heart/kept-notes.js +357 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mcp/mcp-server.js +653 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +59 -0
- package/dist/heart/outlook/outlook-http-hooks.js +64 -0
- package/dist/heart/outlook/outlook-http-response.js +7 -0
- package/dist/heart/outlook/outlook-http-routes.js +232 -0
- package/dist/heart/outlook/outlook-http-static.js +99 -0
- package/dist/heart/outlook/outlook-http-transport.js +116 -0
- package/dist/heart/outlook/outlook-http.js +99 -0
- package/dist/heart/outlook/outlook-read.js +28 -0
- package/dist/heart/outlook/outlook-types.js +27 -0
- package/dist/heart/outlook/outlook-view.js +195 -0
- package/dist/heart/outlook/readers/agent-machine.js +359 -0
- package/dist/heart/outlook/readers/continuity-readers.js +332 -0
- package/dist/heart/outlook/readers/runtime-readers.js +644 -0
- package/dist/heart/outlook/readers/sessions.js +232 -0
- package/dist/heart/outlook/readers/shared.js +111 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-attempt.js +134 -0
- package/dist/heart/provider-binding-resolver.js +239 -0
- package/dist/heart/provider-credentials.js +400 -0
- package/dist/heart/provider-failover.js +266 -0
- package/dist/heart/provider-models.js +81 -0
- package/dist/heart/provider-ping.js +239 -0
- package/dist/heart/provider-state.js +216 -0
- package/dist/heart/provider-visibility.js +186 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +193 -55
- package/dist/heart/providers/azure.js +103 -12
- package/dist/heart/providers/error-classification.js +63 -0
- package/dist/heart/providers/github-copilot.js +145 -0
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +29 -7
- package/dist/heart/providers/openai-codex.js +62 -38
- package/dist/heart/runtime-capability-check.js +170 -0
- package/dist/heart/runtime-credentials.js +260 -0
- package/dist/heart/sense-truth.js +3 -0
- package/dist/heart/session-activity.js +190 -0
- package/dist/heart/session-events.js +855 -0
- package/dist/heart/session-transcript.js +167 -0
- package/dist/heart/start-of-turn-packet.js +345 -0
- package/dist/heart/streaming.js +36 -27
- package/dist/heart/sync.js +332 -0
- package/dist/heart/target-resolution.js +127 -0
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/tool-activity-callbacks.js +36 -0
- package/dist/heart/tool-description.js +135 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +351 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +1 -1
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +425 -0
- package/dist/heart/versioning/ouro-version-manager.js +295 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +5 -1
- package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
- package/dist/mind/bundle-manifest.js +7 -1
- package/dist/mind/context.js +132 -93
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +74 -93
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/friends/channel.js +21 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/resolver.js +38 -1
- package/dist/mind/friends/store-file.js +39 -3
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/note-search.js +268 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/pending.js +66 -7
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +947 -165
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/nerves/cli-logging.js +7 -1
- package/dist/nerves/coverage/audit-rules.js +15 -6
- package/dist/nerves/coverage/audit.js +28 -2
- package/dist/nerves/coverage/cli.js +1 -1
- package/dist/nerves/coverage/contract.js +5 -5
- package/dist/nerves/coverage/file-completeness.js +83 -5
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +224 -4
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/redact.js +79 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/outlook-ui/assets/index-BAcU08c-.css +1 -0
- package/dist/outlook-ui/assets/index-D7l3l4vY.js +61 -0
- package/dist/outlook-ui/index.html +15 -0
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +11 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +733 -0
- package/dist/repertoire/bundle-templates.js +72 -0
- package/dist/repertoire/bw-installer.js +180 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +330 -0
- package/dist/repertoire/coding/feedback.js +197 -30
- package/dist/repertoire/coding/manager.js +158 -9
- package/dist/repertoire/coding/spawner.js +55 -9
- package/dist/repertoire/coding/tools.js +170 -7
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +111 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +371 -0
- package/dist/repertoire/mcp-client.js +255 -0
- package/dist/repertoire/mcp-manager.js +305 -0
- package/dist/repertoire/mcp-tools.js +63 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +15 -24
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tasks/board.js +43 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +37 -4
- package/dist/repertoire/tasks/lifecycle.js +2 -2
- package/dist/repertoire/tasks/parser.js +3 -2
- package/dist/repertoire/tasks/scanner.js +194 -37
- package/dist/repertoire/tasks/transitions.js +16 -78
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-attachments.js +317 -0
- package/dist/repertoire/tools-base.js +42 -690
- package/dist/repertoire/tools-bluebubbles.js +1 -0
- package/dist/repertoire/tools-bridge.js +141 -0
- package/dist/repertoire/tools-bundle.js +984 -0
- package/dist/repertoire/tools-config.js +185 -0
- package/dist/repertoire/tools-continuity.js +248 -0
- package/dist/repertoire/tools-credential.js +361 -0
- package/dist/repertoire/tools-files.js +342 -0
- package/dist/repertoire/tools-flight.js +224 -0
- package/dist/repertoire/tools-flow.js +105 -0
- package/dist/repertoire/tools-github.js +1 -7
- package/dist/repertoire/tools-notes.js +376 -0
- package/dist/repertoire/tools-session.js +739 -0
- package/dist/repertoire/tools-shell.js +120 -0
- package/dist/repertoire/tools-stripe.js +180 -0
- package/dist/repertoire/tools-surface.js +243 -0
- package/dist/repertoire/tools-teams.js +9 -39
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-user-profile.js +144 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools.js +144 -113
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +561 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/attention-queue.js +116 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +260 -9
- package/dist/senses/bluebubbles/entry.js +73 -0
- package/dist/senses/bluebubbles/inbound-log.js +113 -0
- package/dist/senses/bluebubbles/index.js +1620 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +33 -12
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +45 -3
- package/dist/senses/bluebubbles/replay.js +129 -0
- package/dist/senses/bluebubbles/runtime-state.js +109 -0
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +605 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +83 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +240 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +516 -211
- package/dist/senses/commands.js +66 -3
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog-worker.js +102 -19
- package/dist/senses/inner-dialog.js +597 -95
- package/dist/senses/pipeline.js +533 -72
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/shared-turn.js +205 -0
- package/dist/senses/surface-tool.js +68 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +413 -163
- package/dist/senses/trust-gate.js +5 -5
- package/package.json +32 -7
- package/skills/agent-commerce.md +106 -0
- package/skills/browser-navigation.md +117 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -0
- package/skills/configure-dev-tools.md +101 -0
- package/skills/travel-planning.md +138 -0
- package/dist/heart/daemon/ouro-path-installer.js +0 -178
- package/dist/heart/daemon/subagent-installer.js +0 -166
- package/dist/mind/associative-recall.js +0 -209
- package/dist/senses/bluebubbles-entry.js +0 -11
- package/dist/senses/bluebubbles.js +0 -854
- package/dist/senses/debug-activity.js +0 -127
- package/subagents/README.md +0 -86
- package/subagents/work-doer.md +0 -235
- package/subagents/work-merger.md +0 -618
- package/subagents/work-planner.md +0 -382
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
- /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
- /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
- /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
- /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Interactive repair flow for degraded agents detected during `ouro up`.
|
|
4
|
+
*
|
|
5
|
+
* Examines each degraded agent's errorReason and fixHint to detect common
|
|
6
|
+
* issue patterns and prompt the user for repair actions.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.hasRunnableInteractiveRepair = hasRunnableInteractiveRepair;
|
|
10
|
+
exports.isAffirmativeAnswer = isAffirmativeAnswer;
|
|
11
|
+
exports.runInteractiveRepair = runInteractiveRepair;
|
|
12
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
13
|
+
const identity_1 = require("../identity");
|
|
14
|
+
function isCredentialIssue(degraded) {
|
|
15
|
+
const reason = degraded.errorReason.toLowerCase();
|
|
16
|
+
const hint = degraded.fixHint.toLowerCase();
|
|
17
|
+
return reason.includes("credentials") || hint.includes("ouro auth");
|
|
18
|
+
}
|
|
19
|
+
function isVaultUnlockIssue(degraded) {
|
|
20
|
+
const text = `${degraded.errorReason}\n${degraded.fixHint}`.toLowerCase();
|
|
21
|
+
return /ouro vault unlock|credential vault is locked|vault(?: is)? locked/.test(text);
|
|
22
|
+
}
|
|
23
|
+
function isConfigError(degraded) {
|
|
24
|
+
return degraded.fixHint.length > 0 && !isVaultUnlockIssue(degraded) && !isCredentialIssue(degraded);
|
|
25
|
+
}
|
|
26
|
+
function hasRunnableInteractiveRepair(degraded) {
|
|
27
|
+
if (degraded.issue?.actions.some((action) => typedActionToRunnable(degraded, action) !== undefined)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return isVaultUnlockIssue(degraded) || isCredentialIssue(degraded);
|
|
31
|
+
}
|
|
32
|
+
function isAgentProvider(value) {
|
|
33
|
+
return Object.prototype.hasOwnProperty.call(identity_1.PROVIDER_CREDENTIALS, value);
|
|
34
|
+
}
|
|
35
|
+
function extractProviderFromFixHint(fixHint) {
|
|
36
|
+
const provider = fixHint.match(/--provider\s+([a-z0-9-]+)/)?.[1]
|
|
37
|
+
?? fixHint.match(/providers\.([a-z0-9-]+)/)?.[1];
|
|
38
|
+
if (!provider || !isAgentProvider(provider))
|
|
39
|
+
return undefined;
|
|
40
|
+
return provider;
|
|
41
|
+
}
|
|
42
|
+
function escapeRegExp(value) {
|
|
43
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44
|
+
}
|
|
45
|
+
function cleanExtractedCommand(command) {
|
|
46
|
+
const cleaned = command?.trim().replace(/[`'",;:.)]+$/g, "").trim();
|
|
47
|
+
return cleaned && cleaned.length > 0 ? cleaned : undefined;
|
|
48
|
+
}
|
|
49
|
+
function extractRepairCommand(fixHint, commandPrefix) {
|
|
50
|
+
const escapedPrefix = escapeRegExp(commandPrefix);
|
|
51
|
+
const commandBody = `${escapedPrefix}(?=\\s|$)[^\`'"]*`;
|
|
52
|
+
const quoted = fixHint.match(new RegExp(`[\`'"](${commandBody})[\`'"]`, "i"))?.[1];
|
|
53
|
+
const unquoted = fixHint.match(new RegExp(`(${escapedPrefix}(?=\\s|$)[^\\n,;.]+)`, "i"))?.[1];
|
|
54
|
+
return cleanExtractedCommand(quoted) ?? cleanExtractedCommand(unquoted);
|
|
55
|
+
}
|
|
56
|
+
function authCommandFor(degraded) {
|
|
57
|
+
const command = extractRepairCommand(degraded.fixHint, "ouro auth");
|
|
58
|
+
return command && command.length > 0 ? command : `ouro auth --agent ${degraded.agent}`;
|
|
59
|
+
}
|
|
60
|
+
function vaultUnlockCommandFor(degraded) {
|
|
61
|
+
const command = extractRepairCommand(degraded.fixHint, "ouro vault unlock");
|
|
62
|
+
return command && command.length > 0 ? command : `ouro vault unlock --agent ${degraded.agent}`;
|
|
63
|
+
}
|
|
64
|
+
function isAffirmativeAnswer(answer) {
|
|
65
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
66
|
+
}
|
|
67
|
+
function uniqueCommands(commands) {
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const unique = [];
|
|
70
|
+
commands.forEach((command) => {
|
|
71
|
+
if (!command)
|
|
72
|
+
return;
|
|
73
|
+
if (seen.has(command))
|
|
74
|
+
return;
|
|
75
|
+
seen.add(command);
|
|
76
|
+
unique.push(command);
|
|
77
|
+
});
|
|
78
|
+
return unique;
|
|
79
|
+
}
|
|
80
|
+
function fallbackCommandsFor(degraded, primaryCommand) {
|
|
81
|
+
const issueCommands = degraded.issue?.actions.map((action) => action.command) ?? [];
|
|
82
|
+
return uniqueCommands([
|
|
83
|
+
primaryCommand,
|
|
84
|
+
...issueCommands,
|
|
85
|
+
extractRepairCommand(degraded.fixHint, "ouro vault replace"),
|
|
86
|
+
extractRepairCommand(degraded.fixHint, "ouro vault recover"),
|
|
87
|
+
extractRepairCommand(degraded.fixHint, "ouro use"),
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
function renderRepairChoices(prefix, commands) {
|
|
91
|
+
return commands.map((command, index) => ` ${index === 0 ? prefix : "or"}: ${command}`);
|
|
92
|
+
}
|
|
93
|
+
function renderRepairQueueSummaryLines(degraded) {
|
|
94
|
+
const repairable = degraded
|
|
95
|
+
.map((entry) => ({ entry, action: runnableRepairActionFor(entry) }))
|
|
96
|
+
.filter((item) => item.action !== undefined);
|
|
97
|
+
if (repairable.length < 2)
|
|
98
|
+
return [];
|
|
99
|
+
const lines = [
|
|
100
|
+
"Repair queue",
|
|
101
|
+
`${repairable.length} agents need attention before startup can finish.`,
|
|
102
|
+
"",
|
|
103
|
+
];
|
|
104
|
+
repairable.forEach(({ entry, action }, index) => {
|
|
105
|
+
lines.push(`${entry.agent} - ${action.label}`);
|
|
106
|
+
lines.push(` ${action.command}`);
|
|
107
|
+
if (index < repairable.length - 1)
|
|
108
|
+
lines.push("");
|
|
109
|
+
});
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
112
|
+
function renderActionPromptLines(agent, action) {
|
|
113
|
+
const lines = [
|
|
114
|
+
`${agent}`,
|
|
115
|
+
` needs: ${action.label}`,
|
|
116
|
+
` run: ${action.command}`,
|
|
117
|
+
];
|
|
118
|
+
if (action.kind === "vault-unlock") {
|
|
119
|
+
lines.push(" note: use the saved vault unlock secret");
|
|
120
|
+
}
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
function renderDeferredRepair(agent, commands) {
|
|
124
|
+
return [
|
|
125
|
+
`Leaving ${agent} for later.`,
|
|
126
|
+
...renderRepairChoices("next", commands),
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
function renderManualRepairHint(agent, fixHint) {
|
|
130
|
+
return [
|
|
131
|
+
`${agent}`,
|
|
132
|
+
" needs manual attention",
|
|
133
|
+
` next: ${fixHint}`,
|
|
134
|
+
].join("\n");
|
|
135
|
+
}
|
|
136
|
+
function renderUnknownRepair(agent, errorReason) {
|
|
137
|
+
return [
|
|
138
|
+
`${agent}`,
|
|
139
|
+
` ${errorReason}`,
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
function writeDeclinedRepair(degraded, command, deps) {
|
|
143
|
+
deps.writeStdout(renderDeferredRepair(degraded.agent, fallbackCommandsFor(degraded, command)));
|
|
144
|
+
}
|
|
145
|
+
function runnableRepairActionFor(degraded) {
|
|
146
|
+
const typedAction = degraded.issue?.actions
|
|
147
|
+
.map((action) => typedActionToRunnable(degraded, action))
|
|
148
|
+
.find((action) => action !== undefined);
|
|
149
|
+
if (typedAction)
|
|
150
|
+
return typedAction;
|
|
151
|
+
if (isVaultUnlockIssue(degraded)) {
|
|
152
|
+
return { kind: "vault-unlock", label: "vault unlock", command: vaultUnlockCommandFor(degraded) };
|
|
153
|
+
}
|
|
154
|
+
if (isCredentialIssue(degraded)) {
|
|
155
|
+
return {
|
|
156
|
+
kind: "provider-auth",
|
|
157
|
+
label: "provider auth",
|
|
158
|
+
command: authCommandFor(degraded),
|
|
159
|
+
provider: extractProviderFromFixHint(degraded.fixHint),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
function typedActionToRunnable(degraded, action) {
|
|
165
|
+
if (action.executable === false || action.command.includes("<"))
|
|
166
|
+
return undefined;
|
|
167
|
+
if (action.kind === "vault-unlock") {
|
|
168
|
+
return { kind: "vault-unlock", label: "vault unlock", command: action.command };
|
|
169
|
+
}
|
|
170
|
+
if (action.kind === "provider-auth") {
|
|
171
|
+
return {
|
|
172
|
+
kind: "provider-auth",
|
|
173
|
+
label: "provider auth",
|
|
174
|
+
command: action.command || `ouro auth --agent ${degraded.agent}`,
|
|
175
|
+
provider: action.provider,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
function writeRepairQueueSummary(degraded, deps) {
|
|
181
|
+
const lines = renderRepairQueueSummaryLines(degraded);
|
|
182
|
+
if (lines.length > 0)
|
|
183
|
+
deps.writeStdout(lines.join("\n"));
|
|
184
|
+
}
|
|
185
|
+
async function attemptVaultUnlock(entry, action, deps) {
|
|
186
|
+
deps.writeStdout(renderActionPromptLines(entry.agent, action).join("\n"));
|
|
187
|
+
const answer = await deps.promptInput(`Unlock ${entry.agent}'s vault now? [y/N] `);
|
|
188
|
+
if (!isAffirmativeAnswer(answer)) {
|
|
189
|
+
writeDeclinedRepair(entry, action.command, deps);
|
|
190
|
+
return { succeeded: false, attempted: false };
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
if (!deps.runVaultUnlock) {
|
|
194
|
+
deps.writeStdout(renderManualRepairHint(entry.agent, entry.fixHint));
|
|
195
|
+
return { succeeded: false, attempted: false };
|
|
196
|
+
}
|
|
197
|
+
await deps.runVaultUnlock(entry.agent);
|
|
198
|
+
return { succeeded: true, attempted: true };
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
202
|
+
deps.writeStdout(`Vault unlock did not finish for ${entry.agent}.\n ${msg}`);
|
|
203
|
+
(0, runtime_1.emitNervesEvent)({
|
|
204
|
+
level: "error",
|
|
205
|
+
component: "daemon",
|
|
206
|
+
event: "daemon.interactive_repair_vault_unlock_error",
|
|
207
|
+
message: `vault unlock failed for ${entry.agent}`,
|
|
208
|
+
meta: { agent: entry.agent, error: msg },
|
|
209
|
+
});
|
|
210
|
+
return { succeeded: false, attempted: true };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function attemptProviderAuth(entry, action, deps) {
|
|
214
|
+
deps.writeStdout(renderActionPromptLines(entry.agent, action).join("\n"));
|
|
215
|
+
const answer = await deps.promptInput(`Open the auth flow for ${entry.agent} now? [y/N] `);
|
|
216
|
+
if (!isAffirmativeAnswer(answer)) {
|
|
217
|
+
writeDeclinedRepair(entry, action.command, deps);
|
|
218
|
+
return { succeeded: false, attempted: false };
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
if (action.provider) {
|
|
222
|
+
await deps.runAuthFlow(entry.agent, action.provider);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
await deps.runAuthFlow(entry.agent);
|
|
226
|
+
}
|
|
227
|
+
return { succeeded: true, attempted: true };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
231
|
+
deps.writeStdout(`Auth did not finish for ${entry.agent}.\n ${msg}`);
|
|
232
|
+
(0, runtime_1.emitNervesEvent)({
|
|
233
|
+
level: "error",
|
|
234
|
+
component: "daemon",
|
|
235
|
+
event: "daemon.interactive_repair_auth_error",
|
|
236
|
+
message: `auth flow failed for ${entry.agent}`,
|
|
237
|
+
meta: { agent: entry.agent, error: msg },
|
|
238
|
+
});
|
|
239
|
+
return { succeeded: false, attempted: true };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function processEntry(entry, deps) {
|
|
243
|
+
let current = entry;
|
|
244
|
+
while (current) {
|
|
245
|
+
const action = runnableRepairActionFor(current);
|
|
246
|
+
let outcome;
|
|
247
|
+
if (action?.kind === "vault-unlock") {
|
|
248
|
+
outcome = await attemptVaultUnlock(current, action, deps);
|
|
249
|
+
}
|
|
250
|
+
else if (action?.kind === "provider-auth") {
|
|
251
|
+
outcome = await attemptProviderAuth(current, action, deps);
|
|
252
|
+
}
|
|
253
|
+
else if (isConfigError(current)) {
|
|
254
|
+
deps.writeStdout(renderManualRepairHint(current.agent, current.fixHint));
|
|
255
|
+
return { attempted: false };
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
deps.writeStdout(renderUnknownRepair(current.agent, current.errorReason));
|
|
259
|
+
return { attempted: false };
|
|
260
|
+
}
|
|
261
|
+
if (!outcome.succeeded || !deps.recheckAgent) {
|
|
262
|
+
return { attempted: outcome.attempted };
|
|
263
|
+
}
|
|
264
|
+
// Re-evaluate the agent after successful repair
|
|
265
|
+
const recheckResult = await deps.recheckAgent(current.agent);
|
|
266
|
+
if (recheckResult === null) {
|
|
267
|
+
deps.writeStdout(`${current.agent} recovered.`);
|
|
268
|
+
return { attempted: true };
|
|
269
|
+
}
|
|
270
|
+
// Agent still degraded with a new error -- loop to present the new action
|
|
271
|
+
current = recheckResult;
|
|
272
|
+
}
|
|
273
|
+
/* v8 ignore next -- unreachable: loop always returns from within @preserve */
|
|
274
|
+
return { attempted: false };
|
|
275
|
+
}
|
|
276
|
+
async function runInteractiveRepair(degraded, deps) {
|
|
277
|
+
(0, runtime_1.emitNervesEvent)({
|
|
278
|
+
level: "info",
|
|
279
|
+
component: "daemon",
|
|
280
|
+
event: "daemon.interactive_repair_start",
|
|
281
|
+
message: "interactive repair flow started",
|
|
282
|
+
meta: { degradedCount: degraded.length },
|
|
283
|
+
});
|
|
284
|
+
if (degraded.length === 0) {
|
|
285
|
+
return { repairsAttempted: false };
|
|
286
|
+
}
|
|
287
|
+
let repairsAttempted = false;
|
|
288
|
+
if (!deps.skipQueueSummary) {
|
|
289
|
+
writeRepairQueueSummary(degraded, deps);
|
|
290
|
+
}
|
|
291
|
+
for (const entry of degraded) {
|
|
292
|
+
const result = await processEntry(entry, deps);
|
|
293
|
+
if (result.attempted)
|
|
294
|
+
repairsAttempted = true;
|
|
295
|
+
}
|
|
296
|
+
if (repairsAttempted) {
|
|
297
|
+
deps.writeStdout("Repair flow complete.");
|
|
298
|
+
}
|
|
299
|
+
(0, runtime_1.emitNervesEvent)({
|
|
300
|
+
level: "info",
|
|
301
|
+
component: "daemon",
|
|
302
|
+
event: "daemon.interactive_repair_end",
|
|
303
|
+
message: "interactive repair flow completed",
|
|
304
|
+
meta: { repairsAttempted },
|
|
305
|
+
});
|
|
306
|
+
return { repairsAttempted };
|
|
307
|
+
}
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DAEMON_PLIST_LABEL = void 0;
|
|
37
37
|
exports.generateDaemonPlist = generateDaemonPlist;
|
|
38
|
+
exports.writeLaunchAgentPlist = writeLaunchAgentPlist;
|
|
38
39
|
exports.installLaunchAgent = installLaunchAgent;
|
|
39
40
|
exports.uninstallLaunchAgent = uninstallLaunchAgent;
|
|
40
41
|
exports.isDaemonInstalled = isDaemonInstalled;
|
|
@@ -44,6 +45,9 @@ exports.DAEMON_PLIST_LABEL = "bot.ouro.daemon";
|
|
|
44
45
|
function plistFilePath(homeDir) {
|
|
45
46
|
return path.join(homeDir, "Library", "LaunchAgents", `${exports.DAEMON_PLIST_LABEL}.plist`);
|
|
46
47
|
}
|
|
48
|
+
function userLaunchDomain(userUid) {
|
|
49
|
+
return `gui/${userUid}`;
|
|
50
|
+
}
|
|
47
51
|
function generateDaemonPlist(options) {
|
|
48
52
|
(0, runtime_1.emitNervesEvent)({
|
|
49
53
|
component: "daemon",
|
|
@@ -65,15 +69,43 @@ function generateDaemonPlist(options) {
|
|
|
65
69
|
` <string>--socket</string>`,
|
|
66
70
|
` <string>${options.socketPath}</string>`,
|
|
67
71
|
` </array>`,
|
|
72
|
+
` <key>RunAtLoad</key>`,
|
|
73
|
+
` <true/>`,
|
|
68
74
|
` <key>KeepAlive</key>`,
|
|
69
75
|
` <true/>`,
|
|
70
76
|
];
|
|
77
|
+
if (options.envPath) {
|
|
78
|
+
lines.push(` <key>EnvironmentVariables</key>`, ` <dict>`, ` <key>PATH</key>`, ` <string>${options.envPath}</string>`, ` </dict>`);
|
|
79
|
+
}
|
|
71
80
|
if (options.logDir) {
|
|
72
|
-
|
|
81
|
+
// PR 1 decision: we no longer emit `StandardErrorPath` for the daemon.
|
|
82
|
+
// The daemon's structured nerves ndjson pipeline (rotated + gzipped via
|
|
83
|
+
// createNdjsonFileSink) is the source of truth for diagnostics. Writing
|
|
84
|
+
// raw process stderr to an unrotated file grew to 366 MB in the wild;
|
|
85
|
+
// dropping the key lets launchd forward stray stderr to the system log
|
|
86
|
+
// where it gets rotated by the OS.
|
|
87
|
+
lines.push(` <key>StandardOutPath</key>`, ` <string>${path.join(options.logDir, "ouro-daemon-stdout.log")}</string>`);
|
|
73
88
|
}
|
|
74
89
|
lines.push(`</dict>`, `</plist>`, ``);
|
|
75
90
|
return lines.join("\n");
|
|
76
91
|
}
|
|
92
|
+
function writeLaunchAgentPlist(deps, options) {
|
|
93
|
+
const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
|
|
94
|
+
deps.mkdirp(launchAgentsDir);
|
|
95
|
+
if (options.logDir) {
|
|
96
|
+
deps.mkdirp(options.logDir);
|
|
97
|
+
}
|
|
98
|
+
const fullPath = plistFilePath(deps.homeDir);
|
|
99
|
+
const xml = generateDaemonPlist(options);
|
|
100
|
+
deps.writeFile(fullPath, xml);
|
|
101
|
+
(0, runtime_1.emitNervesEvent)({
|
|
102
|
+
component: "daemon",
|
|
103
|
+
event: "daemon.launchd_plist_written",
|
|
104
|
+
message: "daemon launch agent plist written",
|
|
105
|
+
meta: { plistPath: fullPath, entryPath: options.entryPath, socketPath: options.socketPath },
|
|
106
|
+
});
|
|
107
|
+
return fullPath;
|
|
108
|
+
}
|
|
77
109
|
function installLaunchAgent(deps, options) {
|
|
78
110
|
(0, runtime_1.emitNervesEvent)({
|
|
79
111
|
component: "daemon",
|
|
@@ -81,23 +113,27 @@ function installLaunchAgent(deps, options) {
|
|
|
81
113
|
message: "installing launch agent",
|
|
82
114
|
meta: { entryPath: options.entryPath, socketPath: options.socketPath },
|
|
83
115
|
});
|
|
84
|
-
const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
|
|
85
|
-
deps.mkdirp(launchAgentsDir);
|
|
86
116
|
const fullPath = plistFilePath(deps.homeDir);
|
|
117
|
+
const domain = userLaunchDomain(deps.userUid);
|
|
87
118
|
// Unload existing (best effort) for idempotent re-install
|
|
88
119
|
if (deps.existsFile(fullPath)) {
|
|
89
120
|
try {
|
|
90
|
-
deps.exec(`launchctl
|
|
121
|
+
deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
|
|
91
122
|
}
|
|
92
123
|
catch { /* best effort */ }
|
|
93
124
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
125
|
+
writeLaunchAgentPlist(deps, options);
|
|
126
|
+
// Bootstrap the plist so launchd manages crash recovery via KeepAlive.
|
|
127
|
+
// This is safe because ouro up calls this AFTER the daemon is already running,
|
|
128
|
+
// so launchd sees the existing process and just registers for KeepAlive.
|
|
129
|
+
try {
|
|
130
|
+
deps.exec(`launchctl bootstrap ${domain} "${fullPath}"`);
|
|
131
|
+
}
|
|
132
|
+
catch { /* already loaded */ }
|
|
97
133
|
(0, runtime_1.emitNervesEvent)({
|
|
98
134
|
component: "daemon",
|
|
99
135
|
event: "daemon.launchd_installed",
|
|
100
|
-
message: "launch agent installed",
|
|
136
|
+
message: "launch agent installed with KeepAlive",
|
|
101
137
|
meta: { plistPath: fullPath },
|
|
102
138
|
});
|
|
103
139
|
}
|
|
@@ -109,9 +145,10 @@ function uninstallLaunchAgent(deps) {
|
|
|
109
145
|
meta: {},
|
|
110
146
|
});
|
|
111
147
|
const fullPath = plistFilePath(deps.homeDir);
|
|
148
|
+
const domain = userLaunchDomain(deps.userUid);
|
|
112
149
|
if (deps.existsFile(fullPath)) {
|
|
113
150
|
try {
|
|
114
|
-
deps.exec(`launchctl
|
|
151
|
+
deps.exec(`launchctl bootout ${domain} "${fullPath}"`);
|
|
115
152
|
}
|
|
116
153
|
catch { /* best effort */ }
|
|
117
154
|
deps.removeFile(fullPath);
|
|
@@ -37,39 +37,103 @@ exports.discoverLogFiles = discoverLogFiles;
|
|
|
37
37
|
exports.readLastLines = readLastLines;
|
|
38
38
|
exports.formatLogLine = formatLogLine;
|
|
39
39
|
exports.tailLogs = tailLogs;
|
|
40
|
-
const os = __importStar(require("os"));
|
|
41
40
|
const path = __importStar(require("path"));
|
|
41
|
+
const zlib = __importStar(require("zlib"));
|
|
42
42
|
const nerves_1 = require("../../nerves");
|
|
43
43
|
const runtime_1 = require("../../nerves/runtime");
|
|
44
|
+
const identity_1 = require("../identity");
|
|
44
45
|
const LEVEL_COLORS = {
|
|
45
46
|
debug: "\x1b[2m",
|
|
46
47
|
info: "\x1b[36m",
|
|
47
48
|
warn: "\x1b[33m",
|
|
48
49
|
error: "\x1b[31m",
|
|
49
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Parse a log filename into a (streamBase, generationRank) tuple.
|
|
53
|
+
*
|
|
54
|
+
* - `daemon.ndjson` → { streamBase: "daemon", rank: 0 } (active, newest)
|
|
55
|
+
* - `daemon.1.ndjson.gz` → { streamBase: "daemon", rank: 1 }
|
|
56
|
+
* - `daemon.5.ndjson.gz` → { streamBase: "daemon", rank: 5 } (oldest)
|
|
57
|
+
* - Anything else → null (not a log file).
|
|
58
|
+
*
|
|
59
|
+
* Higher rank = older. Used to sort so the oldest generation is read first
|
|
60
|
+
* and the active stream is read last, producing chronological output.
|
|
61
|
+
*/
|
|
62
|
+
function parseLogFilename(name) {
|
|
63
|
+
if (name.endsWith(".ndjson.gz")) {
|
|
64
|
+
// e.g. daemon.1.ndjson.gz → base "daemon.1", strip ".gz" then ".ndjson" then ".<n>"
|
|
65
|
+
const withoutGz = name.slice(0, -".gz".length); // daemon.1.ndjson
|
|
66
|
+
const withoutNdjson = withoutGz.slice(0, -".ndjson".length); // daemon.1
|
|
67
|
+
const genMatch = withoutNdjson.match(/^(.+)\.(\d+)$/);
|
|
68
|
+
if (!genMatch)
|
|
69
|
+
return null;
|
|
70
|
+
return { streamBase: genMatch[1], rank: parseInt(genMatch[2], 10) };
|
|
71
|
+
}
|
|
72
|
+
if (name.endsWith(".ndjson")) {
|
|
73
|
+
const withoutNdjson = name.slice(0, -".ndjson".length);
|
|
74
|
+
// Active file: daemon.ndjson → base "daemon", rank 0
|
|
75
|
+
// Legacy numeric gen: daemon.1.ndjson → base "daemon", rank 1 (treat same as gzipped)
|
|
76
|
+
const legacyMatch = withoutNdjson.match(/^(.+)\.(\d+)$/);
|
|
77
|
+
if (legacyMatch) {
|
|
78
|
+
return { streamBase: legacyMatch[1], rank: parseInt(legacyMatch[2], 10) };
|
|
79
|
+
}
|
|
80
|
+
return { streamBase: withoutNdjson, rank: 0 };
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
50
84
|
function discoverLogFiles(options) {
|
|
51
85
|
/* v8 ignore start -- integration: default DI stubs for real OS @preserve */
|
|
52
|
-
const homeDir = options.homeDir ?? os.homedir();
|
|
53
86
|
const existsSync = options.existsSync ?? (() => false);
|
|
54
87
|
const readdirSync = options.readdirSync ?? (() => []);
|
|
55
88
|
/* v8 ignore stop */
|
|
56
|
-
const logDir =
|
|
57
|
-
|
|
89
|
+
const logDir = options.homeDir
|
|
90
|
+
? path.join(options.homeDir, "AgentBundles", `${options.agentName ?? "slugger"}.ouro`, "state", "daemon", "logs")
|
|
91
|
+
: (0, identity_1.getAgentDaemonLogsDir)(options.agentName);
|
|
92
|
+
const entries = [];
|
|
58
93
|
if (existsSync(logDir)) {
|
|
59
94
|
for (const name of readdirSync(logDir)) {
|
|
60
|
-
|
|
95
|
+
const parsed = parseLogFilename(name);
|
|
96
|
+
if (!parsed)
|
|
61
97
|
continue;
|
|
62
98
|
if (options.agentFilter && !name.includes(options.agentFilter))
|
|
63
99
|
continue;
|
|
64
|
-
|
|
100
|
+
entries.push({ name, parsed });
|
|
65
101
|
}
|
|
66
102
|
}
|
|
67
|
-
|
|
103
|
+
// Sort chronologically: for each stream, oldest generation first, active last.
|
|
104
|
+
// Across streams, sort alphabetically by streamBase so output is stable.
|
|
105
|
+
entries.sort((a, b) => {
|
|
106
|
+
if (a.parsed.streamBase !== b.parsed.streamBase) {
|
|
107
|
+
return a.parsed.streamBase < b.parsed.streamBase ? -1 : 1;
|
|
108
|
+
}
|
|
109
|
+
// Same stream: higher rank = older = read first.
|
|
110
|
+
return b.parsed.rank - a.parsed.rank;
|
|
111
|
+
});
|
|
112
|
+
return entries.map((e) => path.join(logDir, e.name));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Read a log file as a string, transparently handling gzipped rotations.
|
|
116
|
+
*
|
|
117
|
+
* For `.ndjson.gz` files we read the raw bytes via `fs.readFileSync` (ignoring
|
|
118
|
+
* any caller-supplied DI `readFileSync`, since that stub returns a `string`
|
|
119
|
+
* and would corrupt gzip bytes) and then `zlib.gunzipSync` to recover the
|
|
120
|
+
* original text. For plain `.ndjson` files we defer to the DI stub so tests
|
|
121
|
+
* can keep mocking fs.
|
|
122
|
+
*/
|
|
123
|
+
function readNdjsonFileContents(filePath, readFileSync) {
|
|
124
|
+
if (filePath.endsWith(".gz")) {
|
|
125
|
+
// Binary path: tests can mock readFileSync to return a Buffer (typed as
|
|
126
|
+
// string) via `as unknown as string` — we accept both Buffer and Uint8Array.
|
|
127
|
+
const raw = readFileSync(filePath);
|
|
128
|
+
const buf = typeof raw === "string" ? Buffer.from(raw, "binary") : Buffer.from(raw);
|
|
129
|
+
return zlib.gunzipSync(buf).toString("utf-8");
|
|
130
|
+
}
|
|
131
|
+
return readFileSync(filePath, "utf-8");
|
|
68
132
|
}
|
|
69
133
|
function readLastLines(filePath, count, readFileSync) {
|
|
70
134
|
let content;
|
|
71
135
|
try {
|
|
72
|
-
content =
|
|
136
|
+
content = readNdjsonFileContents(filePath, readFileSync);
|
|
73
137
|
}
|
|
74
138
|
catch {
|
|
75
139
|
return [];
|
|
@@ -99,12 +163,17 @@ function tailLogs(options = {}) {
|
|
|
99
163
|
const files = discoverLogFiles(options);
|
|
100
164
|
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.log_tailer_started", message: "log tailer started", meta: { fileCount: files.length, follow: !!options.follow } });
|
|
101
165
|
const fileSizes = new Map();
|
|
102
|
-
// Read initial lines
|
|
166
|
+
// Read initial lines from each discovered file (oldest gz → newest gz → active).
|
|
167
|
+
// Gzipped files are historical and never tailed in follow mode.
|
|
103
168
|
for (const file of files) {
|
|
104
169
|
const lines = readLastLines(file, lineCount, readFileSync);
|
|
105
170
|
for (const line of lines) {
|
|
106
171
|
writer(`${formatLogLine(line)}\n`);
|
|
107
172
|
}
|
|
173
|
+
if (file.endsWith(".gz")) {
|
|
174
|
+
// Historical rotation — skip size tracking, never followed.
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
108
177
|
try {
|
|
109
178
|
const content = readFileSync(file, "utf-8");
|
|
110
179
|
fileSizes.set(file, content.length);
|
|
@@ -113,9 +182,10 @@ function tailLogs(options = {}) {
|
|
|
113
182
|
fileSizes.set(file, 0);
|
|
114
183
|
}
|
|
115
184
|
}
|
|
116
|
-
// Follow mode
|
|
185
|
+
// Follow mode: only the active (non-gzipped) stream is watched.
|
|
117
186
|
if (options.follow && watchFile && unwatchFile) {
|
|
118
|
-
|
|
187
|
+
const activeFiles = files.filter((f) => !f.endsWith(".gz"));
|
|
188
|
+
for (const file of activeFiles) {
|
|
119
189
|
watchFile(file, () => {
|
|
120
190
|
let content;
|
|
121
191
|
try {
|
|
@@ -137,7 +207,7 @@ function tailLogs(options = {}) {
|
|
|
137
207
|
});
|
|
138
208
|
}
|
|
139
209
|
return () => {
|
|
140
|
-
for (const file of
|
|
210
|
+
for (const file of activeFiles) {
|
|
141
211
|
unwatchFile(file);
|
|
142
212
|
}
|
|
143
213
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pruneDaemonLogs = pruneDaemonLogs;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const nerves_1 = require("../../nerves");
|
|
8
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
9
|
+
const identity_1 = require("../identity");
|
|
10
|
+
function pruneDaemonLogs(options = {}) {
|
|
11
|
+
/* v8 ignore next -- defensive: tests always pass logsDir to avoid prod paths @preserve */
|
|
12
|
+
const logsDir = options.logsDir ?? (0, identity_1.getAgentDaemonLogsDir)(options.agentName);
|
|
13
|
+
const maxSizeBytes = options.maxSizeBytes ?? nerves_1.DEFAULT_MAX_LOG_SIZE_BYTES;
|
|
14
|
+
const maxGenerations = options.maxGenerations ?? nerves_1.DEFAULT_MAX_GENERATIONS;
|
|
15
|
+
const traceId = (0, crypto_1.randomUUID)();
|
|
16
|
+
(0, runtime_1.emitNervesEvent)({
|
|
17
|
+
component: "nerves",
|
|
18
|
+
event: "nerves.logs_prune_start",
|
|
19
|
+
trace_id: traceId,
|
|
20
|
+
message: "pruning daemon logs",
|
|
21
|
+
meta: { logsDir, maxSizeBytes, maxGenerations },
|
|
22
|
+
});
|
|
23
|
+
let completed = false;
|
|
24
|
+
try {
|
|
25
|
+
if (!(0, fs_1.existsSync)(logsDir)) {
|
|
26
|
+
completed = true;
|
|
27
|
+
(0, runtime_1.emitNervesEvent)({
|
|
28
|
+
component: "nerves",
|
|
29
|
+
event: "nerves.logs_prune_end",
|
|
30
|
+
trace_id: traceId,
|
|
31
|
+
message: "daemon logs dir does not exist",
|
|
32
|
+
meta: { logsDir, filesCompacted: 0, bytesFreed: 0 },
|
|
33
|
+
});
|
|
34
|
+
return { filesCompacted: 0, bytesFreed: 0 };
|
|
35
|
+
}
|
|
36
|
+
let filesCompacted = 0;
|
|
37
|
+
let bytesFreed = 0;
|
|
38
|
+
// Enumerate and rotate each active .ndjson stream. We explicitly skip
|
|
39
|
+
// .gz and other files — only the active stream can be a rotation
|
|
40
|
+
// candidate. Legacy .N.ndjson uncompressed files are handled inside
|
|
41
|
+
// rotateIfNeeded's generation-shift step, so we skip them here to
|
|
42
|
+
// avoid double-rotating.
|
|
43
|
+
for (const name of (0, fs_1.readdirSync)(logsDir)) {
|
|
44
|
+
if (!name.endsWith(".ndjson"))
|
|
45
|
+
continue;
|
|
46
|
+
// Skip legacy generation files like daemon.1.ndjson; rotateIfNeeded
|
|
47
|
+
// will migrate them on the next rotation of their parent stream.
|
|
48
|
+
if (/\.\d+\.ndjson$/.test(name))
|
|
49
|
+
continue;
|
|
50
|
+
const filePath = (0, path_1.join)(logsDir, name);
|
|
51
|
+
let sizeBefore;
|
|
52
|
+
try {
|
|
53
|
+
sizeBefore = (0, fs_1.statSync)(filePath).size;
|
|
54
|
+
/* v8 ignore start -- defensive: file disappears between readdir and stat @preserve */
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
/* v8 ignore stop */
|
|
60
|
+
if (sizeBefore < maxSizeBytes)
|
|
61
|
+
continue;
|
|
62
|
+
// rotateIfNeeded returns true because we pre-checked sizeBefore >=
|
|
63
|
+
// maxSizeBytes above. The false branch of `if (rotated)` is defensive
|
|
64
|
+
// and unreachable under normal flow; we skip it from coverage so a
|
|
65
|
+
// future refactor that weakens the pre-check still reports correct
|
|
66
|
+
// counts without needing a contrived test.
|
|
67
|
+
const rotated = (0, nerves_1.rotateIfNeeded)(filePath, {
|
|
68
|
+
maxSizeBytes,
|
|
69
|
+
maxGenerations,
|
|
70
|
+
compress: true,
|
|
71
|
+
});
|
|
72
|
+
/* v8 ignore next 3 -- defensive: pre-check guarantees rotated=true @preserve */
|
|
73
|
+
if (!rotated) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
filesCompacted += 1;
|
|
77
|
+
bytesFreed += sizeBefore;
|
|
78
|
+
}
|
|
79
|
+
completed = true;
|
|
80
|
+
(0, runtime_1.emitNervesEvent)({
|
|
81
|
+
component: "nerves",
|
|
82
|
+
event: "nerves.logs_prune_end",
|
|
83
|
+
trace_id: traceId,
|
|
84
|
+
message: "daemon logs pruned",
|
|
85
|
+
meta: { logsDir, filesCompacted, bytesFreed },
|
|
86
|
+
});
|
|
87
|
+
return { filesCompacted, bytesFreed };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
/* v8 ignore next -- defensive: completed=true only reached after try returns @preserve */
|
|
91
|
+
if (!completed) {
|
|
92
|
+
/* v8 ignore next -- defensive: rotation always throws real Errors @preserve */
|
|
93
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
94
|
+
(0, runtime_1.emitNervesEvent)({
|
|
95
|
+
component: "nerves",
|
|
96
|
+
event: "nerves.logs_prune_error",
|
|
97
|
+
trace_id: traceId,
|
|
98
|
+
level: "error",
|
|
99
|
+
message: "daemon logs prune failed",
|
|
100
|
+
meta: { logsDir, error: reason },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|