@ouro.bot/cli 0.1.0-alpha.42 → 0.1.0-alpha.421
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 +118 -15
- 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 +2637 -9
- 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 +424 -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 +801 -217
- package/dist/heart/cross-chat-delivery.js +131 -0
- package/dist/heart/daemon/agent-config-check.js +419 -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 +605 -0
- package/dist/heart/daemon/cli-exec.js +4302 -0
- package/dist/heart/daemon/cli-help.js +413 -0
- package/dist/heart/daemon/cli-parse.js +1151 -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/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 +171 -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/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/pulse.js +475 -0
- package/dist/heart/daemon/readiness-repair.js +250 -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 +259 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/thoughts.js +510 -0
- package/dist/heart/daemon/up-progress.js +218 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/habits/habit-migration.js +181 -0
- package/dist/heart/habits/habit-parser.js +140 -0
- package/dist/heart/habits/habit-scheduler.js +371 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +53 -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 +161 -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 +660 -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 +133 -0
- package/dist/heart/provider-binding-resolver.js +239 -0
- package/dist/heart/provider-credentials.js +389 -0
- package/dist/heart/provider-failover.js +266 -0
- package/dist/heart/provider-models.js +81 -0
- package/dist/heart/provider-ping.js +237 -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-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 +301 -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 +3 -1
- package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
- package/dist/mind/bundle-manifest.js +7 -1
- package/dist/mind/context.js +134 -87
- 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/first-impressions.js +14 -1
- 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 +948 -168
- 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 +702 -0
- package/dist/repertoire/bundle-templates.js +72 -0
- package/dist/repertoire/bw-installer.js +79 -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 +26 -1
- 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 -687
- 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 +421 -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 +526 -211
- package/dist/senses/commands.js +66 -3
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog-worker.js +112 -19
- package/dist/senses/inner-dialog.js +596 -94
- package/dist/senses/pipeline.js +539 -61
- 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 +569 -237
- package/dist/senses/trust-gate.js +5 -5
- package/package.json +29 -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 -134
- package/dist/mind/associative-recall.js +0 -209
- package/dist/senses/bluebubbles-entry.js +0 -11
- package/dist/senses/bluebubbles.js +0 -832
- package/dist/senses/debug-activity.js +0 -127
- package/subagents/README.md +0 -60
- 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,4302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI command execution — the core runOuroCli function and its helpers.
|
|
4
|
+
*
|
|
5
|
+
* This is the junction box: it routes parsed commands to the appropriate
|
|
6
|
+
* handler (local execution, daemon socket, or interactive flow).
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.mergeStartupStability = mergeStartupStability;
|
|
43
|
+
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
44
|
+
exports.listGithubCopilotModels = listGithubCopilotModels;
|
|
45
|
+
exports.checkManualCloneBundles = checkManualCloneBundles;
|
|
46
|
+
exports.runOuroCli = runOuroCli;
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const crypto_1 = require("crypto");
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const semver = __importStar(require("semver"));
|
|
53
|
+
const identity_1 = require("../identity");
|
|
54
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
55
|
+
const store_file_1 = require("../../mind/friends/store-file");
|
|
56
|
+
const runtime_metadata_1 = require("./runtime-metadata");
|
|
57
|
+
const runtime_mode_1 = require("./runtime-mode");
|
|
58
|
+
const platform_1 = require("../platform");
|
|
59
|
+
const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
|
|
60
|
+
const update_hooks_1 = require("../versioning/update-hooks");
|
|
61
|
+
const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
62
|
+
const agent_config_v2_1 = require("./hooks/agent-config-v2");
|
|
63
|
+
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
64
|
+
const tasks_1 = require("../../repertoire/tasks");
|
|
65
|
+
const credential_access_1 = require("../../repertoire/credential-access");
|
|
66
|
+
const vault_setup_1 = require("../../repertoire/vault-setup");
|
|
67
|
+
const vault_unlock_1 = require("../../repertoire/vault-unlock");
|
|
68
|
+
const thoughts_1 = require("./thoughts");
|
|
69
|
+
const launchd_1 = require("./launchd");
|
|
70
|
+
const auth_flow_1 = require("../auth/auth-flow");
|
|
71
|
+
const provider_credentials_1 = require("../provider-credentials");
|
|
72
|
+
const runtime_credentials_1 = require("../runtime-credentials");
|
|
73
|
+
const provider_binding_resolver_1 = require("../provider-binding-resolver");
|
|
74
|
+
const provider_state_1 = require("../provider-state");
|
|
75
|
+
const machine_identity_1 = require("../machine-identity");
|
|
76
|
+
const provider_models_1 = require("../provider-models");
|
|
77
|
+
const ouro_version_manager_1 = require("../versioning/ouro-version-manager");
|
|
78
|
+
const sync_1 = require("../sync");
|
|
79
|
+
const cli_parse_1 = require("./cli-parse");
|
|
80
|
+
const cli_parse_2 = require("./cli-parse");
|
|
81
|
+
const cli_help_1 = require("./cli-help");
|
|
82
|
+
const cli_render_1 = require("./cli-render");
|
|
83
|
+
const cli_defaults_1 = require("./cli-defaults");
|
|
84
|
+
const agent_config_check_1 = require("./agent-config-check");
|
|
85
|
+
const doctor_1 = require("./doctor");
|
|
86
|
+
const cli_render_doctor_1 = require("./cli-render-doctor");
|
|
87
|
+
const interactive_repair_1 = require("./interactive-repair");
|
|
88
|
+
const agentic_repair_1 = require("./agentic-repair");
|
|
89
|
+
const readiness_repair_1 = require("./readiness-repair");
|
|
90
|
+
const startup_tui_1 = require("./startup-tui");
|
|
91
|
+
const stale_bundle_prune_1 = require("./stale-bundle-prune");
|
|
92
|
+
const up_progress_1 = require("./up-progress");
|
|
93
|
+
const provider_ping_1 = require("../provider-ping");
|
|
94
|
+
const agent_discovery_1 = require("./agent-discovery");
|
|
95
|
+
// ── ensureDaemonRunning ──
|
|
96
|
+
const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 10_000;
|
|
97
|
+
const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
|
|
98
|
+
const DEFAULT_DAEMON_STARTUP_STABILITY_WINDOW_MS = 1_500;
|
|
99
|
+
const DEFAULT_DAEMON_STARTUP_RETRY_LIMIT = 1;
|
|
100
|
+
const DEFAULT_DAEMON_STARTUP_LOG_LINES = 10;
|
|
101
|
+
async function checkAgentProviders(deps, agentsOverride, onProgress) {
|
|
102
|
+
const agents = agentsOverride ?? await listCliAgents(deps);
|
|
103
|
+
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
104
|
+
const degraded = [];
|
|
105
|
+
for (const agent of [...new Set(agents)]) {
|
|
106
|
+
try {
|
|
107
|
+
onProgress?.(`${agent}: checking providers...`);
|
|
108
|
+
const result = await checkAgentProviderHealth(agent, bundlesRoot, deps, onProgress);
|
|
109
|
+
if (result.ok)
|
|
110
|
+
continue;
|
|
111
|
+
const errorReason = result.error ?? "agent provider health check failed";
|
|
112
|
+
const fixHint = result.fix ?? "";
|
|
113
|
+
degraded.push({ agent, errorReason, fixHint, ...(result.issue ? { issue: result.issue } : {}) });
|
|
114
|
+
(0, runtime_1.emitNervesEvent)({
|
|
115
|
+
level: "error",
|
|
116
|
+
component: "daemon",
|
|
117
|
+
event: "daemon.agent_config_invalid",
|
|
118
|
+
message: errorReason,
|
|
119
|
+
meta: { agent, fixProvided: fixHint.length > 0, source: "already-running-provider-check" },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const errorReason = error instanceof Error ? error.message : String(error);
|
|
124
|
+
degraded.push({
|
|
125
|
+
agent,
|
|
126
|
+
errorReason,
|
|
127
|
+
fixHint: "Run 'ouro doctor' for diagnostics, then retry 'ouro up'.",
|
|
128
|
+
});
|
|
129
|
+
(0, runtime_1.emitNervesEvent)({
|
|
130
|
+
level: "error",
|
|
131
|
+
component: "daemon",
|
|
132
|
+
event: "daemon.agent_config_invalid",
|
|
133
|
+
message: errorReason,
|
|
134
|
+
meta: { agent, fixProvided: true, source: "already-running-provider-check" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return degraded;
|
|
139
|
+
}
|
|
140
|
+
async function checkAgentProviderHealth(agentName, bundlesRoot, deps, onProgress) {
|
|
141
|
+
const liveDeps = {};
|
|
142
|
+
if (deps.homeDir)
|
|
143
|
+
liveDeps.homeDir = deps.homeDir;
|
|
144
|
+
if (onProgress)
|
|
145
|
+
liveDeps.onProgress = onProgress;
|
|
146
|
+
if (liveDeps.homeDir || liveDeps.onProgress) {
|
|
147
|
+
return (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agentName, bundlesRoot, liveDeps);
|
|
148
|
+
}
|
|
149
|
+
return (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agentName, bundlesRoot);
|
|
150
|
+
}
|
|
151
|
+
async function listCliAgents(deps) {
|
|
152
|
+
if (deps.listDiscoveredAgents) {
|
|
153
|
+
return Promise.resolve(deps.listDiscoveredAgents());
|
|
154
|
+
}
|
|
155
|
+
if (deps.bundlesRoot) {
|
|
156
|
+
return (0, agent_discovery_1.listEnabledBundleAgents)({ bundlesRoot: deps.bundlesRoot });
|
|
157
|
+
}
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
function managedAgentsSignature(agentNames) {
|
|
161
|
+
const unique = [...new Set(agentNames.map((agent) => agent.trim()).filter((agent) => agent.length > 0))].sort();
|
|
162
|
+
return unique.length > 0 ? unique.join(",") : "(none)";
|
|
163
|
+
}
|
|
164
|
+
async function checkAlreadyRunningAgentProviders(deps, onProgress) {
|
|
165
|
+
return checkAgentProviders(deps, undefined, onProgress);
|
|
166
|
+
}
|
|
167
|
+
function readinessIssueFromDegraded(entry) {
|
|
168
|
+
return entry.issue ?? (0, readiness_repair_1.genericReadinessIssue)({
|
|
169
|
+
summary: `${entry.agent}: ${entry.errorReason}`,
|
|
170
|
+
...(entry.fixHint ? { fix: entry.fixHint } : {}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function writeProviderRepairSummary(deps, title, degraded) {
|
|
174
|
+
const blocks = degraded.map((entry) => (0, readiness_repair_1.renderReadinessIssueNextSteps)(readinessIssueFromDegraded(entry)).join("\n"));
|
|
175
|
+
deps.writeStdout([title, ...blocks].join("\n\n"));
|
|
176
|
+
}
|
|
177
|
+
function providerRepairCountSummary(count) {
|
|
178
|
+
if (count === 0)
|
|
179
|
+
return "ok";
|
|
180
|
+
return `${count} ${count === 1 ? "needs" : "need"} attention`;
|
|
181
|
+
}
|
|
182
|
+
function createHumanCommandProgress(deps, commandName) {
|
|
183
|
+
return new up_progress_1.CommandProgress({
|
|
184
|
+
write: deps.writeRaw ?? deps.writeStdout,
|
|
185
|
+
isTTY: deps.isTTY ?? false,
|
|
186
|
+
now: deps.now ?? (() => Date.now()),
|
|
187
|
+
autoRender: true,
|
|
188
|
+
eventScope: "command",
|
|
189
|
+
commandName,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function runCommandProgressPhase(progress, label, run, detail) {
|
|
193
|
+
progress.startPhase(label);
|
|
194
|
+
try {
|
|
195
|
+
const result = await Promise.resolve(run());
|
|
196
|
+
progress.completePhase(label, detail(result));
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
progress.completePhase(label, "failed");
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function daemonProgressSummary(result) {
|
|
205
|
+
if (result.verifyStartupStatus === false)
|
|
206
|
+
return "not answering yet";
|
|
207
|
+
if (result.alreadyRunning)
|
|
208
|
+
return "already running";
|
|
209
|
+
if (result.message.includes("restarted"))
|
|
210
|
+
return "restarted and ready";
|
|
211
|
+
return "ready";
|
|
212
|
+
}
|
|
213
|
+
async function reportPostRepairProviderHealth(deps, repairedAgents, onProgress) {
|
|
214
|
+
const remainingDegraded = await checkAgentProviders(deps, repairedAgents, onProgress);
|
|
215
|
+
(0, runtime_1.emitNervesEvent)({
|
|
216
|
+
level: remainingDegraded.length > 0 ? "warn" : "info",
|
|
217
|
+
component: "daemon",
|
|
218
|
+
event: "daemon.post_repair_provider_check",
|
|
219
|
+
message: remainingDegraded.length > 0
|
|
220
|
+
? "post-repair provider health check still degraded"
|
|
221
|
+
: "post-repair provider health check recovered",
|
|
222
|
+
meta: { degradedCount: remainingDegraded.length, repairedAgents },
|
|
223
|
+
});
|
|
224
|
+
if (remainingDegraded.length === 0) {
|
|
225
|
+
deps.writeStdout("All set. Provider checks recovered after repair.");
|
|
226
|
+
return remainingDegraded;
|
|
227
|
+
}
|
|
228
|
+
writeProviderRepairSummary(deps, "Still needs attention", remainingDegraded);
|
|
229
|
+
deps.writeStdout("Run `ouro up` again after these are fixed.");
|
|
230
|
+
return remainingDegraded;
|
|
231
|
+
}
|
|
232
|
+
async function checkProviderHealthBeforeChat(agentName, deps) {
|
|
233
|
+
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
234
|
+
const result = await checkAgentProviderHealth(agentName, bundlesRoot, deps);
|
|
235
|
+
if (!result.ok) {
|
|
236
|
+
const output = `${result.error}\n${result.fix ? ` fix: ${result.fix}` : ""}`;
|
|
237
|
+
deps.writeStdout(output);
|
|
238
|
+
return { ok: false, output };
|
|
239
|
+
}
|
|
240
|
+
return { ok: true };
|
|
241
|
+
}
|
|
242
|
+
function mergeStartupStability(stability, extraDegraded) {
|
|
243
|
+
if (extraDegraded.length === 0)
|
|
244
|
+
return stability;
|
|
245
|
+
const degradedByAgent = new Map();
|
|
246
|
+
for (const entry of stability?.degraded ?? [])
|
|
247
|
+
degradedByAgent.set(entry.agent, entry);
|
|
248
|
+
for (const entry of extraDegraded)
|
|
249
|
+
degradedByAgent.set(entry.agent, entry);
|
|
250
|
+
const degraded = [...degradedByAgent.values()];
|
|
251
|
+
const stable = [];
|
|
252
|
+
for (const agent of stability?.stable ?? []) {
|
|
253
|
+
if (!degradedByAgent.has(agent))
|
|
254
|
+
stable.push(agent);
|
|
255
|
+
}
|
|
256
|
+
return { stable, degraded };
|
|
257
|
+
}
|
|
258
|
+
async function ensureDaemonRunning(deps, options = {}) {
|
|
259
|
+
const readLatestDaemonStartupEvent = () => {
|
|
260
|
+
try {
|
|
261
|
+
// The daemon writes structured events to daemon.ndjson in the first
|
|
262
|
+
// agent bundle's state/daemon/logs/ directory. Read the last line to
|
|
263
|
+
// surface what it's currently doing (e.g., "starting auto-start agents").
|
|
264
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
265
|
+
if (!fs.existsSync(bundlesRoot))
|
|
266
|
+
return null;
|
|
267
|
+
const agents = fs.readdirSync(bundlesRoot).filter((d) => d.endsWith(".ouro"));
|
|
268
|
+
for (const agent of agents) {
|
|
269
|
+
const logPath = path.join(bundlesRoot, agent, "state", "daemon", "logs", "daemon.ndjson");
|
|
270
|
+
if (!fs.existsSync(logPath))
|
|
271
|
+
continue;
|
|
272
|
+
const stat = fs.statSync(logPath);
|
|
273
|
+
if (stat.size === 0)
|
|
274
|
+
continue;
|
|
275
|
+
// Only read logs from the last 30 seconds (daemon just started)
|
|
276
|
+
const mtime = stat.mtimeMs;
|
|
277
|
+
if (Date.now() - mtime > 30_000)
|
|
278
|
+
continue;
|
|
279
|
+
const buf = Buffer.alloc(4096);
|
|
280
|
+
const fd = fs.openSync(logPath, "r");
|
|
281
|
+
let bytesRead = 0;
|
|
282
|
+
try {
|
|
283
|
+
const readFrom = Math.max(0, stat.size - 4096);
|
|
284
|
+
bytesRead = fs.readSync(fd, buf, 0, 4096, readFrom);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
fs.closeSync(fd);
|
|
288
|
+
}
|
|
289
|
+
const lines = buf.subarray(0, bytesRead).toString("utf-8").trim().split("\n").filter(Boolean);
|
|
290
|
+
const last = lines[lines.length - 1];
|
|
291
|
+
if (!last)
|
|
292
|
+
continue;
|
|
293
|
+
const parsed = JSON.parse(last);
|
|
294
|
+
return parsed.message ?? null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Best effort only.
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
};
|
|
302
|
+
const alive = options.initialAlive ?? await deps.checkSocketAlive(deps.socketPath);
|
|
303
|
+
if (alive) {
|
|
304
|
+
const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
305
|
+
const localManagedAgents = managedAgentsSignature((0, agent_discovery_1.listEnabledBundleAgents)({
|
|
306
|
+
bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
|
|
307
|
+
}));
|
|
308
|
+
let runningRuntimePromise = null;
|
|
309
|
+
const fetchRunningRuntimeMetadata = async () => {
|
|
310
|
+
runningRuntimePromise ??= (async () => {
|
|
311
|
+
const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
|
|
312
|
+
const payload = (0, cli_render_1.parseStatusPayload)(status.data);
|
|
313
|
+
return {
|
|
314
|
+
version: payload?.overview.version ?? "unknown",
|
|
315
|
+
lastUpdated: payload?.overview.lastUpdated ?? "unknown",
|
|
316
|
+
repoRoot: payload?.overview.repoRoot ?? "unknown",
|
|
317
|
+
configFingerprint: payload?.overview.configFingerprint ?? "unknown",
|
|
318
|
+
managedAgents: payload && payload.workers.length > 0
|
|
319
|
+
? managedAgentsSignature(payload.workers.map((worker) => worker.agent))
|
|
320
|
+
: "unknown",
|
|
321
|
+
};
|
|
322
|
+
})();
|
|
323
|
+
return runningRuntimePromise;
|
|
324
|
+
};
|
|
325
|
+
const runtimeResult = await (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
|
|
326
|
+
socketPath: deps.socketPath,
|
|
327
|
+
localVersion: localRuntime.version,
|
|
328
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
329
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
330
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
331
|
+
localManagedAgents,
|
|
332
|
+
fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
|
|
333
|
+
fetchRunningRuntimeMetadata,
|
|
334
|
+
stopDaemon: async () => {
|
|
335
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
336
|
+
},
|
|
337
|
+
cleanupStaleSocket: deps.cleanupStaleSocket,
|
|
338
|
+
startDaemonProcess: deps.startDaemonProcess,
|
|
339
|
+
checkSocketAlive: deps.checkSocketAlive,
|
|
340
|
+
});
|
|
341
|
+
if (!runtimeResult.verifyStartupStatus) {
|
|
342
|
+
return runtimeResult;
|
|
343
|
+
}
|
|
344
|
+
const stability = await (0, startup_tui_1.pollDaemonStartup)({
|
|
345
|
+
sendCommand: deps.sendCommand,
|
|
346
|
+
socketPath: deps.socketPath,
|
|
347
|
+
daemonPid: runtimeResult.startedPid ?? null,
|
|
348
|
+
/* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
|
|
349
|
+
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
350
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
351
|
+
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
352
|
+
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
353
|
+
now: deps.now ?? (() => Date.now()),
|
|
354
|
+
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
355
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
356
|
+
/* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
|
|
357
|
+
readLatestDaemonEvent: readLatestDaemonStartupEvent,
|
|
358
|
+
/* v8 ignore stop */
|
|
359
|
+
onProgress: deps.reportDaemonStartupPhase,
|
|
360
|
+
render: !deps.reportDaemonStartupPhase,
|
|
361
|
+
});
|
|
362
|
+
return {
|
|
363
|
+
alreadyRunning: runtimeResult.alreadyRunning,
|
|
364
|
+
message: runtimeResult.message,
|
|
365
|
+
stability,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const retryLimit = deps.startupRetryLimit ?? DEFAULT_DAEMON_STARTUP_RETRY_LIMIT;
|
|
369
|
+
let lastFailure = {
|
|
370
|
+
reason: "daemon failed before the startup monitor recorded a failure",
|
|
371
|
+
retryable: false,
|
|
372
|
+
};
|
|
373
|
+
let lastPid = null;
|
|
374
|
+
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
375
|
+
deps.reportDaemonStartupPhase?.("launching daemon process");
|
|
376
|
+
deps.reportDaemonStartupPhase?.("waiting for daemon socket");
|
|
377
|
+
deps.cleanupStaleSocket(deps.socketPath);
|
|
378
|
+
const bootStartedAtMs = (deps.now ?? Date.now)();
|
|
379
|
+
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
380
|
+
lastPid = started.pid ?? null;
|
|
381
|
+
const startupFailure = await waitForDaemonStartup(deps, {
|
|
382
|
+
bootStartedAtMs,
|
|
383
|
+
pid: lastPid,
|
|
384
|
+
});
|
|
385
|
+
if (!startupFailure) {
|
|
386
|
+
const stability = await (0, startup_tui_1.pollDaemonStartup)({
|
|
387
|
+
sendCommand: deps.sendCommand,
|
|
388
|
+
socketPath: deps.socketPath,
|
|
389
|
+
daemonPid: lastPid,
|
|
390
|
+
/* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
|
|
391
|
+
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
392
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
393
|
+
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
394
|
+
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
395
|
+
now: deps.now ?? (() => Date.now()),
|
|
396
|
+
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
397
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
398
|
+
/* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
|
|
399
|
+
readLatestDaemonEvent: readLatestDaemonStartupEvent,
|
|
400
|
+
/* v8 ignore stop */
|
|
401
|
+
onProgress: deps.reportDaemonStartupPhase,
|
|
402
|
+
render: !deps.reportDaemonStartupPhase,
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
alreadyRunning: false,
|
|
406
|
+
message: `daemon started (pid ${lastPid ?? "unknown"})`,
|
|
407
|
+
stability,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
lastFailure = startupFailure;
|
|
411
|
+
if (!startupFailure.retryable || attempt >= retryLimit) {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
deps.reportDaemonStartupPhase?.("daemon startup lost stability; retrying once");
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
alreadyRunning: false,
|
|
418
|
+
message: formatDaemonStartupFailureMessage(lastPid, lastFailure, deps),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function hasStartupHealthMonitor(deps) {
|
|
422
|
+
return !!deps.healthFilePath && !!deps.readHealthState && !!deps.readHealthUpdatedAt;
|
|
423
|
+
}
|
|
424
|
+
function hasFreshCurrentBootHealthSignal(deps, bootStartedAtMs, pid) {
|
|
425
|
+
const healthState = deps.readHealthState(deps.healthFilePath);
|
|
426
|
+
if (!healthState)
|
|
427
|
+
return false;
|
|
428
|
+
const healthUpdatedAt = deps.readHealthUpdatedAt(deps.healthFilePath);
|
|
429
|
+
if (healthUpdatedAt === null || healthUpdatedAt < bootStartedAtMs) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
const healthStartedAtMs = Date.parse(healthState.startedAt);
|
|
433
|
+
if (!Number.isFinite(healthStartedAtMs) || healthStartedAtMs < bootStartedAtMs) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
if (pid !== null && healthState.pid !== pid) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
function formatDaemonStartupFailureMessage(pid, failure, deps) {
|
|
442
|
+
const lines = [
|
|
443
|
+
`daemon spawned (pid ${pid ?? "unknown"}) but failed to stabilize: ${failure.reason}`,
|
|
444
|
+
];
|
|
445
|
+
const recentLogLines = deps.readRecentDaemonLogLines?.(DEFAULT_DAEMON_STARTUP_LOG_LINES) ?? [];
|
|
446
|
+
if (recentLogLines.length > 0) {
|
|
447
|
+
lines.push("recent daemon logs:");
|
|
448
|
+
lines.push(...recentLogLines.map((line) => ` ${line}`));
|
|
449
|
+
}
|
|
450
|
+
lines.push("fix hint for daemon: check daemon logs or run `ouro doctor`");
|
|
451
|
+
return lines.join("\n");
|
|
452
|
+
}
|
|
453
|
+
async function waitForDaemonStartup(deps, options) {
|
|
454
|
+
const now = deps.now ?? Date.now;
|
|
455
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
456
|
+
const timeoutMs = deps.startupTimeoutMs ?? DEFAULT_DAEMON_STARTUP_TIMEOUT_MS;
|
|
457
|
+
const pollIntervalMs = deps.startupPollIntervalMs ?? DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS;
|
|
458
|
+
const stabilityWindowMs = deps.startupStabilityWindowMs ?? DEFAULT_DAEMON_STARTUP_STABILITY_WINDOW_MS;
|
|
459
|
+
const deadline = options.bootStartedAtMs + timeoutMs;
|
|
460
|
+
const useHealthMonitor = hasStartupHealthMonitor(deps);
|
|
461
|
+
let stableSinceMs = null;
|
|
462
|
+
let sawSocket = false;
|
|
463
|
+
if (!useHealthMonitor) {
|
|
464
|
+
const verified = await verifyDaemonAlive(deps.checkSocketAlive, deps.socketPath, timeoutMs, pollIntervalMs, sleep, now);
|
|
465
|
+
return verified
|
|
466
|
+
? null
|
|
467
|
+
: {
|
|
468
|
+
reason: `daemon failed to respond within ${Math.ceil(timeoutMs / 1000)}s`,
|
|
469
|
+
retryable: false,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
while (now() < deadline) {
|
|
473
|
+
await sleep(pollIntervalMs);
|
|
474
|
+
const aliveNow = await deps.checkSocketAlive(deps.socketPath);
|
|
475
|
+
if (!aliveNow) {
|
|
476
|
+
if (sawSocket) {
|
|
477
|
+
return {
|
|
478
|
+
reason: "daemon socket disappeared during startup",
|
|
479
|
+
retryable: true,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (!sawSocket) {
|
|
485
|
+
sawSocket = true;
|
|
486
|
+
stableSinceMs = now();
|
|
487
|
+
deps.reportDaemonStartupPhase?.("verifying daemon health");
|
|
488
|
+
}
|
|
489
|
+
if (!hasFreshCurrentBootHealthSignal(deps, options.bootStartedAtMs, options.pid)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (stableSinceMs !== null && now() - stableSinceMs >= stabilityWindowMs) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
reason: sawSocket
|
|
498
|
+
? "daemon did not publish fresh health for the current boot attempt"
|
|
499
|
+
: `daemon failed to respond within ${Math.ceil(timeoutMs / 1000)}s`,
|
|
500
|
+
retryable: sawSocket,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async function verifyDaemonAlive(checkSocketAlive, socketPath, maxWaitMs = 10_000, pollIntervalMs = 500, sleep = defaultSleep, now = Date.now) {
|
|
504
|
+
const deadline = now() + maxWaitMs;
|
|
505
|
+
while (now() < deadline) {
|
|
506
|
+
await sleep(pollIntervalMs);
|
|
507
|
+
if (await checkSocketAlive(socketPath))
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
function defaultSleep(ms) {
|
|
513
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
514
|
+
}
|
|
515
|
+
// ── GitHub Copilot model helpers ──
|
|
516
|
+
async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
|
|
517
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
518
|
+
const response = await fetchImpl(url, {
|
|
519
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
520
|
+
});
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
throw new Error(`model listing failed (HTTP ${response.status})`);
|
|
523
|
+
}
|
|
524
|
+
const body = await response.json();
|
|
525
|
+
/* v8 ignore start -- response shape handling: tested via config-models.test.ts @preserve */
|
|
526
|
+
const items = Array.isArray(body) ? body : (body?.data ?? []);
|
|
527
|
+
return items.map((item) => {
|
|
528
|
+
const rec = item;
|
|
529
|
+
const capabilities = Array.isArray(rec.capabilities)
|
|
530
|
+
? rec.capabilities.filter((c) => typeof c === "string")
|
|
531
|
+
: undefined;
|
|
532
|
+
return {
|
|
533
|
+
id: String(rec.id ?? rec.name ?? ""),
|
|
534
|
+
name: String(rec.name ?? rec.id ?? ""),
|
|
535
|
+
...(capabilities ? { capabilities } : {}),
|
|
536
|
+
};
|
|
537
|
+
});
|
|
538
|
+
/* v8 ignore stop */
|
|
539
|
+
}
|
|
540
|
+
// ── Provider credential verification ──
|
|
541
|
+
/* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
|
|
542
|
+
async function verifyProviderCredentials(provider, providers) {
|
|
543
|
+
const config = providers[provider];
|
|
544
|
+
if (!config)
|
|
545
|
+
return "not configured";
|
|
546
|
+
try {
|
|
547
|
+
const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../../heart/provider-ping")));
|
|
548
|
+
const result = await pingProvider(provider, config);
|
|
549
|
+
return result.ok ? "ok" : `failed (${result.message})`;
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
return `failed (${error instanceof Error ? error.message : String(error)})`;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function checkManualCloneBundles(deps) {
|
|
556
|
+
if (!deps.promptInput)
|
|
557
|
+
return;
|
|
558
|
+
let entries;
|
|
559
|
+
try {
|
|
560
|
+
entries = fs.readdirSync(deps.bundlesRoot).filter((e) => e.endsWith(".ouro"));
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
for (const agentDir of entries) {
|
|
566
|
+
const bundlePath = path.join(deps.bundlesRoot, agentDir);
|
|
567
|
+
const gitDir = path.join(bundlePath, ".git");
|
|
568
|
+
if (!fs.existsSync(gitDir))
|
|
569
|
+
continue;
|
|
570
|
+
// Check for remotes
|
|
571
|
+
let remoteOutput;
|
|
572
|
+
try {
|
|
573
|
+
remoteOutput = (0, child_process_1.execFileSync)("git", ["remote", "-v"], { cwd: bundlePath, stdio: "pipe" }).toString().trim();
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (!remoteOutput)
|
|
579
|
+
continue;
|
|
580
|
+
// Check if sync is already enabled
|
|
581
|
+
const agentJsonPath = path.join(bundlePath, "agent.json");
|
|
582
|
+
if (fs.existsSync(agentJsonPath)) {
|
|
583
|
+
try {
|
|
584
|
+
const raw = fs.readFileSync(agentJsonPath, "utf-8");
|
|
585
|
+
const config = JSON.parse(raw);
|
|
586
|
+
if (config.sync?.enabled)
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// Can't read agent.json — skip
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Parse first remote name
|
|
595
|
+
const firstLine = remoteOutput.split("\n")[0];
|
|
596
|
+
/* v8 ignore next -- defensive fallback: .trim() above strips leading tabs so empty-field path is unreachable @preserve */
|
|
597
|
+
const remoteName = firstLine.split("\t")[0] || "origin";
|
|
598
|
+
(0, runtime_1.emitNervesEvent)({
|
|
599
|
+
component: "daemon",
|
|
600
|
+
event: "daemon.manual_clone_detected",
|
|
601
|
+
message: "bundle appears to be a manually cloned git repo",
|
|
602
|
+
meta: { agent: agentDir, remote: remoteName },
|
|
603
|
+
});
|
|
604
|
+
/* v8 ignore next -- ?? fallback: promptInput always returns string in practice @preserve */
|
|
605
|
+
const answer = (await deps.promptInput(`Bundle ${agentDir} appears to be a git clone with a remote. Enable sync? (y/n): `)) ?? "";
|
|
606
|
+
if (answer.trim().toLowerCase() === "y" && fs.existsSync(agentJsonPath)) {
|
|
607
|
+
const raw = fs.readFileSync(agentJsonPath, "utf-8");
|
|
608
|
+
const config = JSON.parse(raw);
|
|
609
|
+
config.sync = { enabled: true, remote: remoteName };
|
|
610
|
+
fs.writeFileSync(agentJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
611
|
+
(0, runtime_1.emitNervesEvent)({
|
|
612
|
+
component: "daemon",
|
|
613
|
+
event: "daemon.manual_clone_sync_enabled",
|
|
614
|
+
message: "sync enabled for manually cloned bundle",
|
|
615
|
+
meta: { agent: agentDir, remote: remoteName },
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
(0, runtime_1.emitNervesEvent)({
|
|
620
|
+
component: "daemon",
|
|
621
|
+
event: "daemon.manual_clone_sync_skipped",
|
|
622
|
+
message: "user declined sync for manually cloned bundle",
|
|
623
|
+
meta: { agent: agentDir },
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// ── toDaemonCommand ──
|
|
629
|
+
function toDaemonCommand(command) {
|
|
630
|
+
return command;
|
|
631
|
+
}
|
|
632
|
+
// ── Hatch input resolution ──
|
|
633
|
+
async function resolveHatchInput(command, deps) {
|
|
634
|
+
const prompt = deps.promptInput;
|
|
635
|
+
const agentName = command.agentName ?? (prompt ? await prompt("Hatchling name: ") : "");
|
|
636
|
+
const humanName = command.humanName ?? (prompt ? await prompt("Your name: ") : os.userInfo().username);
|
|
637
|
+
const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex|github-copilot): ") : "");
|
|
638
|
+
if (!agentName || !humanName || !(0, cli_parse_2.isAgentProvider)(providerRaw)) {
|
|
639
|
+
throw new Error(`Usage\n${(0, cli_parse_2.usage)()}`);
|
|
640
|
+
}
|
|
641
|
+
const progress = createHumanCommandProgress(deps, "hatch auth");
|
|
642
|
+
let credentials;
|
|
643
|
+
try {
|
|
644
|
+
progress.startPhase(`resolving ${providerRaw} credentials`);
|
|
645
|
+
credentials = await (0, auth_flow_1.resolveHatchCredentials)({
|
|
646
|
+
agentName,
|
|
647
|
+
provider: providerRaw,
|
|
648
|
+
credentials: command.credentials,
|
|
649
|
+
promptInput: prompt,
|
|
650
|
+
runAuthFlow: deps.runAuthFlow,
|
|
651
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
652
|
+
});
|
|
653
|
+
progress.completePhase(`resolving ${providerRaw} credentials`, "ready");
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
progress.end();
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
progress.end();
|
|
660
|
+
return {
|
|
661
|
+
agentName,
|
|
662
|
+
humanName,
|
|
663
|
+
provider: providerRaw,
|
|
664
|
+
credentials,
|
|
665
|
+
migrationPath: command.migrationPath,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
// ── Provider state CLI helpers ──
|
|
669
|
+
function providerCliHomeDir(deps) {
|
|
670
|
+
return (0, provider_credentials_1.providerCredentialMachineHomeDir)(deps.homeDir);
|
|
671
|
+
}
|
|
672
|
+
function providerCliAgentRoot(command, deps) {
|
|
673
|
+
return path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
|
|
674
|
+
}
|
|
675
|
+
function providerCliNow(deps) {
|
|
676
|
+
return new Date((deps.now ?? Date.now)());
|
|
677
|
+
}
|
|
678
|
+
function readAgentSyncConfigForCliMutation(agent, deps) {
|
|
679
|
+
try {
|
|
680
|
+
const configPath = path.join(providerCliAgentRoot({ agent }, deps), "agent.json");
|
|
681
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
682
|
+
return {
|
|
683
|
+
enabled: parsed.sync?.enabled === true,
|
|
684
|
+
remote: typeof parsed.sync?.remote === "string" && parsed.sync.remote.trim() ? parsed.sync.remote : "origin",
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
/* v8 ignore next -- defensive: post-mutation sync should not break an already-successful CLI repair if agent.json becomes unreadable @preserve */
|
|
689
|
+
return { enabled: false, remote: "origin" };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function pushAgentBundleAfterCliMutation(agent, deps) {
|
|
693
|
+
const sync = readAgentSyncConfigForCliMutation(agent, deps);
|
|
694
|
+
if (!sync.enabled)
|
|
695
|
+
return null;
|
|
696
|
+
const agentRoot = providerCliAgentRoot({ agent }, deps);
|
|
697
|
+
const result = (0, sync_1.postTurnPush)(agentRoot, { enabled: true, remote: sync.remote });
|
|
698
|
+
if (result.ok) {
|
|
699
|
+
return `bundle sync: ran post-change sync (remote: ${sync.remote})`;
|
|
700
|
+
}
|
|
701
|
+
return `bundle sync: could not push bundle changes (${result.error})`;
|
|
702
|
+
}
|
|
703
|
+
function appendBundleSyncSummary(message, agent, deps) {
|
|
704
|
+
const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
|
|
705
|
+
return syncSummary ? `${message}\n${syncSummary}` : message;
|
|
706
|
+
}
|
|
707
|
+
function writeAgentVaultConfig(agentName, configPath, config, vault) {
|
|
708
|
+
const nextConfig = {
|
|
709
|
+
...config,
|
|
710
|
+
vault: {
|
|
711
|
+
email: vault.email,
|
|
712
|
+
serverUrl: vault.serverUrl,
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
716
|
+
(0, runtime_1.emitNervesEvent)({
|
|
717
|
+
component: "daemon",
|
|
718
|
+
event: "daemon.vault_config_written",
|
|
719
|
+
message: "wrote credential vault locator to agent config",
|
|
720
|
+
meta: { agentName, configPath, email: vault.email, serverUrl: vault.serverUrl },
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function isJsonRecord(value) {
|
|
724
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
725
|
+
}
|
|
726
|
+
function cloneJsonRecord(value) {
|
|
727
|
+
return JSON.parse(JSON.stringify(value));
|
|
728
|
+
}
|
|
729
|
+
function importableCredentialFields(value) {
|
|
730
|
+
if (!isJsonRecord(value))
|
|
731
|
+
return {};
|
|
732
|
+
const result = {};
|
|
733
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
734
|
+
if (typeof fieldValue === "string" && fieldValue.trim()) {
|
|
735
|
+
result[key] = fieldValue;
|
|
736
|
+
}
|
|
737
|
+
else if (typeof fieldValue === "number" && Number.isFinite(fieldValue)) {
|
|
738
|
+
result[key] = fieldValue;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
function providerImportFromRaw(provider, raw) {
|
|
744
|
+
if (!isJsonRecord(raw))
|
|
745
|
+
return null;
|
|
746
|
+
const hasStructuredFields = isJsonRecord(raw.credentials) || isJsonRecord(raw.config);
|
|
747
|
+
const fields = hasStructuredFields
|
|
748
|
+
? {
|
|
749
|
+
credentials: importableCredentialFields(raw.credentials),
|
|
750
|
+
config: importableCredentialFields(raw.config),
|
|
751
|
+
}
|
|
752
|
+
: (0, provider_credentials_1.splitProviderCredentialFields)(provider, raw);
|
|
753
|
+
if (Object.keys(fields.credentials).length === 0 && Object.keys(fields.config).length === 0)
|
|
754
|
+
return null;
|
|
755
|
+
return { provider, credentials: fields.credentials, config: fields.config };
|
|
756
|
+
}
|
|
757
|
+
function recoverProviderImports(raw) {
|
|
758
|
+
const providers = isJsonRecord(raw.providers) ? raw.providers : raw;
|
|
759
|
+
const imports = [];
|
|
760
|
+
for (const [providerName, providerRaw] of Object.entries(providers)) {
|
|
761
|
+
if (!(0, cli_parse_2.isAgentProvider)(providerName))
|
|
762
|
+
continue;
|
|
763
|
+
const imported = providerImportFromRaw(providerName, providerRaw);
|
|
764
|
+
if (imported)
|
|
765
|
+
imports.push(imported);
|
|
766
|
+
}
|
|
767
|
+
return imports;
|
|
768
|
+
}
|
|
769
|
+
const RECOVER_RUNTIME_EXCLUDED_TOP_LEVEL = new Set(["providers", "vault", "context", "schemaVersion", "updatedAt"]);
|
|
770
|
+
const MACHINE_RUNTIME_CONFIG_TOP_LEVEL = new Set(["bluebubbles", "bluebubblesChannel"]);
|
|
771
|
+
function recoverRuntimeConfig(raw) {
|
|
772
|
+
const config = {};
|
|
773
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
774
|
+
if (RECOVER_RUNTIME_EXCLUDED_TOP_LEVEL.has(key) || (0, cli_parse_2.isAgentProvider)(key))
|
|
775
|
+
continue;
|
|
776
|
+
config[key] = isJsonRecord(value) ? cloneJsonRecord(value) : value;
|
|
777
|
+
}
|
|
778
|
+
return config;
|
|
779
|
+
}
|
|
780
|
+
function mergeRuntimeConfig(a, b) {
|
|
781
|
+
const merged = { ...a };
|
|
782
|
+
for (const [key, value] of Object.entries(b)) {
|
|
783
|
+
if (isJsonRecord(merged[key]) && isJsonRecord(value)) {
|
|
784
|
+
merged[key] = mergeRuntimeConfig(merged[key], value);
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
merged[key] = isJsonRecord(value) ? cloneJsonRecord(value) : value;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return merged;
|
|
791
|
+
}
|
|
792
|
+
function splitRuntimeConfigByScope(config) {
|
|
793
|
+
const agentConfig = {};
|
|
794
|
+
const machineConfig = {};
|
|
795
|
+
for (const [key, value] of Object.entries(config)) {
|
|
796
|
+
const target = MACHINE_RUNTIME_CONFIG_TOP_LEVEL.has(key) ? machineConfig : agentConfig;
|
|
797
|
+
target[key] = isJsonRecord(value) ? cloneJsonRecord(value) : value;
|
|
798
|
+
}
|
|
799
|
+
return { agentConfig, machineConfig };
|
|
800
|
+
}
|
|
801
|
+
function readVaultRecoverSource(sourcePath) {
|
|
802
|
+
const resolved = path.resolve(sourcePath);
|
|
803
|
+
let parsed;
|
|
804
|
+
try {
|
|
805
|
+
parsed = JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
const reason = String(error);
|
|
809
|
+
throw new Error(`cannot read vault recover source ${resolved}: ${reason}`);
|
|
810
|
+
}
|
|
811
|
+
if (!isJsonRecord(parsed)) {
|
|
812
|
+
throw new Error(`vault recover source ${resolved} must be a JSON object`);
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
sourcePath: resolved,
|
|
816
|
+
providers: recoverProviderImports(parsed),
|
|
817
|
+
runtimeConfig: recoverRuntimeConfig(parsed),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function isGeneratedRepairVaultEmail(email) {
|
|
821
|
+
const [local, domain] = email.trim().split("@");
|
|
822
|
+
return domain?.toLowerCase() === "ouro.bot" && /\+(?:replaced|recovered)-\d{14}(?:$|\+)/i.test(local);
|
|
823
|
+
}
|
|
824
|
+
function defaultRepairVaultEmail(agentName, config) {
|
|
825
|
+
const configuredEmail = config.vault?.email?.trim();
|
|
826
|
+
if (configuredEmail && !isGeneratedRepairVaultEmail(configuredEmail))
|
|
827
|
+
return configuredEmail;
|
|
828
|
+
return (0, identity_1.defaultStableVaultEmail)(agentName);
|
|
829
|
+
}
|
|
830
|
+
function ensureVaultSecretPrompt(promptSecret, action) {
|
|
831
|
+
if (promptSecret)
|
|
832
|
+
return promptSecret;
|
|
833
|
+
throw new Error(`vault ${action} requires an interactive secret prompt that does not echo the vault unlock secret`);
|
|
834
|
+
}
|
|
835
|
+
function rejectGeneratedVaultUnlockSecret(action) {
|
|
836
|
+
throw new Error(`vault ${action} no longer supports --generate-unlock-secret. Re-run without that flag and enter a human-chosen unlock secret; Ouro will not print vault unlock secrets.`);
|
|
837
|
+
}
|
|
838
|
+
async function createRepairVaultForAgent(input) {
|
|
839
|
+
const result = await runCommandProgressPhase(input.progress, "creating vault account", () => (0, vault_setup_1.createVaultAccount)("Ouro credential vault", input.serverUrl, input.email, input.unlockSecret), (created) => created.success ? "created" : "failed");
|
|
840
|
+
if (!result.success) {
|
|
841
|
+
const message = [
|
|
842
|
+
`vault ${input.action} failed for ${input.agentName}: ${result.error}`,
|
|
843
|
+
"",
|
|
844
|
+
"Could not create the selected vault account.",
|
|
845
|
+
"If this is the existing vault, run:",
|
|
846
|
+
` ouro vault unlock --agent ${input.agentName}`,
|
|
847
|
+
"If the unlock secret is lost and you intentionally need a different vault account, rerun with --email <email>.",
|
|
848
|
+
"If this looks like a server or network issue, check --server and retry.",
|
|
849
|
+
].join("\n");
|
|
850
|
+
input.deps.writeStdout(message);
|
|
851
|
+
return { ok: false, message };
|
|
852
|
+
}
|
|
853
|
+
const store = await runCommandProgressPhase(input.progress, "saving local unlock", () => {
|
|
854
|
+
writeAgentVaultConfig(input.agentName, input.configPath, input.config, { email: input.email, serverUrl: input.serverUrl });
|
|
855
|
+
return (0, vault_unlock_1.storeVaultUnlockSecret)({
|
|
856
|
+
agentName: input.agentName,
|
|
857
|
+
email: input.email,
|
|
858
|
+
serverUrl: input.serverUrl,
|
|
859
|
+
}, input.unlockSecret, { homeDir: input.deps.homeDir, store: input.store });
|
|
860
|
+
}, (saved) => saved.kind);
|
|
861
|
+
await runCommandProgressPhase(input.progress, "checking vault access", async () => {
|
|
862
|
+
(0, credential_access_1.resetCredentialStore)();
|
|
863
|
+
await (0, credential_access_1.getCredentialStore)(input.agentName).get("__ouro_vault_probe__");
|
|
864
|
+
}, () => "ok");
|
|
865
|
+
return { ok: true, store };
|
|
866
|
+
}
|
|
867
|
+
async function executeVaultUnlock(command, deps) {
|
|
868
|
+
if (command.agent === "SerpentGuide") {
|
|
869
|
+
throw new Error("SerpentGuide does not have a persistent credential vault. Hatch bootstrap uses selected provider credentials in memory only.");
|
|
870
|
+
}
|
|
871
|
+
const promptSecret = ensureVaultSecretPrompt(deps.promptSecret, "unlock");
|
|
872
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
|
|
873
|
+
const vault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
|
|
874
|
+
const unlockSecret = await promptSecret(`Ouro vault unlock secret for ${vault.email}: `);
|
|
875
|
+
const progress = createHumanCommandProgress(deps, "vault unlock");
|
|
876
|
+
let store;
|
|
877
|
+
try {
|
|
878
|
+
store = await runCommandProgressPhase(progress, "saving local unlock", () => (0, vault_unlock_1.storeVaultUnlockSecret)({
|
|
879
|
+
agentName: command.agent,
|
|
880
|
+
email: vault.email,
|
|
881
|
+
serverUrl: vault.serverUrl,
|
|
882
|
+
}, unlockSecret, { homeDir: deps.homeDir, store: command.store }), (saved) => saved.kind);
|
|
883
|
+
await runCommandProgressPhase(progress, "checking vault access", async () => {
|
|
884
|
+
(0, credential_access_1.resetCredentialStore)();
|
|
885
|
+
await (0, credential_access_1.getCredentialStore)(command.agent).get("__ouro_vault_probe__");
|
|
886
|
+
}, () => "ok");
|
|
887
|
+
}
|
|
888
|
+
finally {
|
|
889
|
+
progress.end();
|
|
890
|
+
}
|
|
891
|
+
const message = [
|
|
892
|
+
`vault unlocked for ${command.agent} on this machine`,
|
|
893
|
+
`vault: ${vault.email} at ${vault.serverUrl}`,
|
|
894
|
+
`local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`,
|
|
895
|
+
].join("\n");
|
|
896
|
+
deps.writeStdout(message);
|
|
897
|
+
return message;
|
|
898
|
+
}
|
|
899
|
+
async function executeVaultCreate(command, deps) {
|
|
900
|
+
if (command.agent === "SerpentGuide") {
|
|
901
|
+
throw new Error("SerpentGuide does not have a persistent credential vault. Create a vault for the hatchling agent, not SerpentGuide.");
|
|
902
|
+
}
|
|
903
|
+
if (command.generateUnlockSecret)
|
|
904
|
+
rejectGeneratedVaultUnlockSecret("create");
|
|
905
|
+
const { configPath, config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
|
|
906
|
+
const configuredVault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
|
|
907
|
+
const email = command.email ?? config.vault?.email ?? configuredVault.email;
|
|
908
|
+
const promptSecret = ensureVaultSecretPrompt(deps.promptSecret, "create");
|
|
909
|
+
const serverUrl = command.serverUrl ?? config.vault?.serverUrl ?? configuredVault.serverUrl;
|
|
910
|
+
const unlockSecret = await (0, vault_unlock_1.promptConfirmedVaultUnlockSecret)({
|
|
911
|
+
promptSecret,
|
|
912
|
+
question: `Choose Ouro vault unlock secret for ${email}: `,
|
|
913
|
+
confirmQuestion: `Confirm Ouro vault unlock secret for ${email}: `,
|
|
914
|
+
emptyError: "vault create requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.",
|
|
915
|
+
});
|
|
916
|
+
const progress = createHumanCommandProgress(deps, "vault create");
|
|
917
|
+
let store;
|
|
918
|
+
try {
|
|
919
|
+
const result = await runCommandProgressPhase(progress, "creating vault account", () => (0, vault_setup_1.createVaultAccount)("Ouro credential vault", serverUrl, email, unlockSecret), (created) => created.success ? "created" : "failed");
|
|
920
|
+
if (!result.success) {
|
|
921
|
+
const message = [
|
|
922
|
+
`vault create failed for ${command.agent}: ${result.error}`,
|
|
923
|
+
"",
|
|
924
|
+
"If this vault account already exists, run:",
|
|
925
|
+
` ouro vault unlock --agent ${command.agent}`,
|
|
926
|
+
].join("\n");
|
|
927
|
+
progress.end();
|
|
928
|
+
deps.writeStdout(message);
|
|
929
|
+
return message;
|
|
930
|
+
}
|
|
931
|
+
store = await runCommandProgressPhase(progress, "saving local unlock", () => {
|
|
932
|
+
writeAgentVaultConfig(command.agent, configPath, config, { email, serverUrl });
|
|
933
|
+
return (0, vault_unlock_1.storeVaultUnlockSecret)({
|
|
934
|
+
agentName: command.agent,
|
|
935
|
+
email,
|
|
936
|
+
serverUrl,
|
|
937
|
+
}, unlockSecret, { homeDir: deps.homeDir, store: command.store });
|
|
938
|
+
}, (saved) => saved.kind);
|
|
939
|
+
await runCommandProgressPhase(progress, "checking vault access", async () => {
|
|
940
|
+
(0, credential_access_1.resetCredentialStore)();
|
|
941
|
+
await (0, credential_access_1.getCredentialStore)(command.agent).get("__ouro_vault_probe__");
|
|
942
|
+
}, () => "ok");
|
|
943
|
+
}
|
|
944
|
+
finally {
|
|
945
|
+
progress.end();
|
|
946
|
+
}
|
|
947
|
+
/* v8 ignore next -- defensive: success path assigns store before continuing @preserve */
|
|
948
|
+
if (!store)
|
|
949
|
+
throw new Error(`vault create failed for ${command.agent}: local unlock material was not saved`);
|
|
950
|
+
const message = appendBundleSyncSummary([
|
|
951
|
+
`vault created for ${command.agent}`,
|
|
952
|
+
`vault: ${email} at ${serverUrl}`,
|
|
953
|
+
`local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`,
|
|
954
|
+
"All raw credentials for this agent will be stored in this Ouro credential vault.",
|
|
955
|
+
"Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
|
|
956
|
+
].join("\n"), command.agent, deps);
|
|
957
|
+
deps.writeStdout(message);
|
|
958
|
+
return message;
|
|
959
|
+
}
|
|
960
|
+
async function executeVaultReplace(command, deps) {
|
|
961
|
+
if (command.agent === "SerpentGuide") {
|
|
962
|
+
throw new Error("SerpentGuide does not have a persistent credential vault. Replace the hatchling agent vault, not SerpentGuide.");
|
|
963
|
+
}
|
|
964
|
+
if (command.generateUnlockSecret)
|
|
965
|
+
rejectGeneratedVaultUnlockSecret("replace");
|
|
966
|
+
const promptSecret = ensureVaultSecretPrompt(deps.promptSecret, "replace");
|
|
967
|
+
const { configPath, config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
|
|
968
|
+
const configuredVault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
|
|
969
|
+
const email = command.email ?? defaultRepairVaultEmail(command.agent, config);
|
|
970
|
+
const serverUrl = command.serverUrl ?? config.vault?.serverUrl ?? configuredVault.serverUrl;
|
|
971
|
+
const unlockSecret = await (0, vault_unlock_1.promptConfirmedVaultUnlockSecret)({
|
|
972
|
+
promptSecret,
|
|
973
|
+
question: `Choose new Ouro vault unlock secret for ${email}: `,
|
|
974
|
+
confirmQuestion: `Confirm new Ouro vault unlock secret for ${email}: `,
|
|
975
|
+
emptyError: "vault replace requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.",
|
|
976
|
+
});
|
|
977
|
+
const progress = createHumanCommandProgress(deps, "vault replace");
|
|
978
|
+
let repair;
|
|
979
|
+
try {
|
|
980
|
+
repair = await createRepairVaultForAgent({
|
|
981
|
+
action: "replace",
|
|
982
|
+
agentName: command.agent,
|
|
983
|
+
email,
|
|
984
|
+
serverUrl,
|
|
985
|
+
unlockSecret,
|
|
986
|
+
store: command.store,
|
|
987
|
+
deps,
|
|
988
|
+
progress,
|
|
989
|
+
configPath,
|
|
990
|
+
config,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
finally {
|
|
994
|
+
progress.end();
|
|
995
|
+
}
|
|
996
|
+
/* v8 ignore next -- defensive: createRepairVaultForAgent either returns or throws @preserve */
|
|
997
|
+
if (!repair)
|
|
998
|
+
throw new Error(`vault replace failed for ${command.agent}: no vault repair result`);
|
|
999
|
+
if (!repair.ok)
|
|
1000
|
+
return repair.message;
|
|
1001
|
+
const message = appendBundleSyncSummary([
|
|
1002
|
+
`vault replaced for ${command.agent}`,
|
|
1003
|
+
`vault: ${email} at ${serverUrl}`,
|
|
1004
|
+
`local unlock store: ${repair.store.kind}${repair.store.secure ? "" : " (explicit plaintext fallback)"}`,
|
|
1005
|
+
"imported: none",
|
|
1006
|
+
`next: ouro repair --agent ${command.agent}`,
|
|
1007
|
+
"Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
|
|
1008
|
+
].join("\n"), command.agent, deps);
|
|
1009
|
+
deps.writeStdout(message);
|
|
1010
|
+
return message;
|
|
1011
|
+
}
|
|
1012
|
+
async function executeVaultRecover(command, deps) {
|
|
1013
|
+
if (command.agent === "SerpentGuide") {
|
|
1014
|
+
throw new Error("SerpentGuide does not have a persistent credential vault. Recover the hatchling agent vault, not SerpentGuide.");
|
|
1015
|
+
}
|
|
1016
|
+
if (command.generateUnlockSecret)
|
|
1017
|
+
rejectGeneratedVaultUnlockSecret("recover");
|
|
1018
|
+
const sourceImports = command.sources.map(readVaultRecoverSource);
|
|
1019
|
+
const promptSecret = ensureVaultSecretPrompt(deps.promptSecret, "recover");
|
|
1020
|
+
const now = providerCliNow(deps);
|
|
1021
|
+
const { configPath, config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
|
|
1022
|
+
const configuredVault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
|
|
1023
|
+
const email = command.email ?? defaultRepairVaultEmail(command.agent, config);
|
|
1024
|
+
const serverUrl = command.serverUrl ?? config.vault?.serverUrl ?? configuredVault.serverUrl;
|
|
1025
|
+
const unlockSecret = await (0, vault_unlock_1.promptConfirmedVaultUnlockSecret)({
|
|
1026
|
+
promptSecret,
|
|
1027
|
+
question: `Choose new Ouro vault unlock secret for ${email}: `,
|
|
1028
|
+
confirmQuestion: `Confirm new Ouro vault unlock secret for ${email}: `,
|
|
1029
|
+
emptyError: "vault recover requires an unlock secret. Re-run in an interactive terminal and enter a human-chosen unlock secret.",
|
|
1030
|
+
});
|
|
1031
|
+
const progress = createHumanCommandProgress(deps, "vault recover");
|
|
1032
|
+
let repair;
|
|
1033
|
+
const importedProviders = new Set();
|
|
1034
|
+
let runtimeFields = [];
|
|
1035
|
+
let machineRuntimeFields = [];
|
|
1036
|
+
try {
|
|
1037
|
+
repair = await createRepairVaultForAgent({
|
|
1038
|
+
action: "recover",
|
|
1039
|
+
agentName: command.agent,
|
|
1040
|
+
email,
|
|
1041
|
+
serverUrl,
|
|
1042
|
+
unlockSecret,
|
|
1043
|
+
store: command.store,
|
|
1044
|
+
deps,
|
|
1045
|
+
progress,
|
|
1046
|
+
configPath,
|
|
1047
|
+
config,
|
|
1048
|
+
});
|
|
1049
|
+
if (!repair.ok)
|
|
1050
|
+
return repair.message;
|
|
1051
|
+
await runCommandProgressPhase(progress, "importing recovered credentials", async () => {
|
|
1052
|
+
let mergedRecoveredRuntimeConfig = {};
|
|
1053
|
+
for (const source of sourceImports) {
|
|
1054
|
+
for (const provider of source.providers) {
|
|
1055
|
+
await (0, provider_credentials_1.upsertProviderCredential)({
|
|
1056
|
+
agentName: command.agent,
|
|
1057
|
+
provider: provider.provider,
|
|
1058
|
+
credentials: provider.credentials,
|
|
1059
|
+
config: provider.config,
|
|
1060
|
+
provenance: { source: "manual" },
|
|
1061
|
+
now,
|
|
1062
|
+
});
|
|
1063
|
+
importedProviders.add(provider.provider);
|
|
1064
|
+
}
|
|
1065
|
+
mergedRecoveredRuntimeConfig = mergeRuntimeConfig(mergedRecoveredRuntimeConfig, source.runtimeConfig);
|
|
1066
|
+
}
|
|
1067
|
+
const splitRuntimeConfig = splitRuntimeConfigByScope(mergedRecoveredRuntimeConfig);
|
|
1068
|
+
runtimeFields = summarizeRuntimeConfigFields(splitRuntimeConfig.agentConfig);
|
|
1069
|
+
machineRuntimeFields = summarizeRuntimeConfigFields(splitRuntimeConfig.machineConfig);
|
|
1070
|
+
if (runtimeFields.length > 0) {
|
|
1071
|
+
await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(command.agent, splitRuntimeConfig.agentConfig, now);
|
|
1072
|
+
}
|
|
1073
|
+
if (machineRuntimeFields.length > 0) {
|
|
1074
|
+
await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(command.agent, currentMachineId(deps), splitRuntimeConfig.machineConfig, now);
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
providerCount: importedProviders.size,
|
|
1078
|
+
runtimeCount: runtimeFields.length,
|
|
1079
|
+
machineRuntimeCount: machineRuntimeFields.length,
|
|
1080
|
+
};
|
|
1081
|
+
}, (imported) => `${imported.providerCount} providers, ${imported.runtimeCount + imported.machineRuntimeCount} runtime fields`);
|
|
1082
|
+
}
|
|
1083
|
+
finally {
|
|
1084
|
+
progress.end();
|
|
1085
|
+
}
|
|
1086
|
+
const providerList = [...importedProviders].sort();
|
|
1087
|
+
const message = appendBundleSyncSummary([
|
|
1088
|
+
`vault recovered for ${command.agent}`,
|
|
1089
|
+
`vault: ${email} at ${serverUrl}`,
|
|
1090
|
+
`local unlock store: ${repair.store.kind}${repair.store.secure ? "" : " (explicit plaintext fallback)"}`,
|
|
1091
|
+
`sources imported: ${sourceImports.length}`,
|
|
1092
|
+
`provider credentials imported: ${providerList.length === 0 ? "none" : providerList.join(", ")}`,
|
|
1093
|
+
`runtime credentials imported: ${runtimeFields.length === 0 ? "none" : runtimeFields.join(", ")}`,
|
|
1094
|
+
`machine runtime credentials imported: ${machineRuntimeFields.length === 0 ? "none" : machineRuntimeFields.join(", ")}`,
|
|
1095
|
+
"credential values were not printed",
|
|
1096
|
+
"Keep the vault unlock secret saved outside Ouro. Another machine will need it once.",
|
|
1097
|
+
].join("\n"), command.agent, deps);
|
|
1098
|
+
deps.writeStdout(message);
|
|
1099
|
+
return message;
|
|
1100
|
+
}
|
|
1101
|
+
async function executeVaultStatus(command, deps) {
|
|
1102
|
+
if (command.agent === "SerpentGuide") {
|
|
1103
|
+
const message = "SerpentGuide has no persistent credential vault. Hatch bootstrap uses selected provider credentials in memory only.";
|
|
1104
|
+
deps.writeStdout(message);
|
|
1105
|
+
return message;
|
|
1106
|
+
}
|
|
1107
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
|
|
1108
|
+
const vault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
|
|
1109
|
+
if (!config.vault) {
|
|
1110
|
+
const lines = [
|
|
1111
|
+
`agent: ${command.agent}`,
|
|
1112
|
+
`vault: ${vault.email} at ${vault.serverUrl}`,
|
|
1113
|
+
"vault locator: not configured in agent.json",
|
|
1114
|
+
"local unlock: not checked",
|
|
1115
|
+
"",
|
|
1116
|
+
`fix: Run 'ouro vault create --agent ${command.agent}' to create this agent's vault, then run 'ouro auth --agent ${command.agent} --provider <provider>' and 'ouro provider refresh --agent ${command.agent}'.`,
|
|
1117
|
+
];
|
|
1118
|
+
const message = lines.join("\n");
|
|
1119
|
+
deps.writeStdout(message);
|
|
1120
|
+
return message;
|
|
1121
|
+
}
|
|
1122
|
+
const status = (0, vault_unlock_1.getVaultUnlockStatus)({
|
|
1123
|
+
agentName: command.agent,
|
|
1124
|
+
email: vault.email,
|
|
1125
|
+
serverUrl: vault.serverUrl,
|
|
1126
|
+
}, { homeDir: deps.homeDir, store: command.store });
|
|
1127
|
+
const lines = [
|
|
1128
|
+
`agent: ${command.agent}`,
|
|
1129
|
+
`vault: ${vault.email} at ${vault.serverUrl}`,
|
|
1130
|
+
"vault locator: agent.json",
|
|
1131
|
+
`local unlock store: ${status.store ? `${status.store.kind}${status.store.secure ? "" : " (explicit plaintext fallback)"}` : "unavailable"}`,
|
|
1132
|
+
`local unlock: ${status.stored ? "available" : "missing"}`,
|
|
1133
|
+
];
|
|
1134
|
+
if (status.stored) {
|
|
1135
|
+
const progress = createHumanCommandProgress(deps, "vault status");
|
|
1136
|
+
try {
|
|
1137
|
+
const runtime = await runCommandProgressPhase(progress, "reading runtime credentials", () => (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true }), (runtimeResult) => runtimeResult.ok ? runtimeResult.revision : runtimeResult.reason);
|
|
1138
|
+
if (runtime.ok) {
|
|
1139
|
+
lines.push(`runtime credentials: ${summarizeRuntimeConfigFields(runtime.config).join(", ") || "none stored"} (${runtime.revision})`);
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
lines.push(`runtime credentials: ${runtime.reason} (${runtime.error})`);
|
|
1143
|
+
if (runtime.reason === "missing") {
|
|
1144
|
+
lines.push(` fix: Run 'ouro vault config set --agent ${command.agent} --key <field>' to store sense/integration credentials.`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const pool = await runCommandProgressPhase(progress, "reading provider credentials", () => (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, {
|
|
1148
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1149
|
+
}), (poolResult) => {
|
|
1150
|
+
if (!poolResult.ok)
|
|
1151
|
+
return poolResult.reason;
|
|
1152
|
+
const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(poolResult.pool);
|
|
1153
|
+
return summary.providers.map((provider) => provider.provider).join(", ") || "none stored";
|
|
1154
|
+
});
|
|
1155
|
+
if (pool.ok) {
|
|
1156
|
+
const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(pool.pool);
|
|
1157
|
+
lines.push(`provider credentials: ${summary.providers.length === 0 ? "none stored" : ""}`);
|
|
1158
|
+
for (const provider of summary.providers) {
|
|
1159
|
+
lines.push(` ${provider.provider}: credential fields ${provider.credentialFields.join(", ") || "none"}, config fields ${provider.configFields.join(", ") || "none"}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
lines.push(`provider credentials: unavailable (${pool.error})`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
finally {
|
|
1167
|
+
progress.end();
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
lines.push("");
|
|
1172
|
+
lines.push(status.fix);
|
|
1173
|
+
}
|
|
1174
|
+
const message = lines.join("\n");
|
|
1175
|
+
deps.writeStdout(message);
|
|
1176
|
+
return message;
|
|
1177
|
+
}
|
|
1178
|
+
const RUNTIME_CONFIG_KEY_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
1179
|
+
const FORBIDDEN_RUNTIME_CONFIG_KEY_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
1180
|
+
function parseRuntimeConfigKey(key) {
|
|
1181
|
+
const segments = key.split(".").map((segment) => segment.trim()).filter(Boolean);
|
|
1182
|
+
if (segments.length < 2) {
|
|
1183
|
+
throw new Error("runtime config key must be a dotted path such as bluebubbles.password");
|
|
1184
|
+
}
|
|
1185
|
+
for (const segment of segments) {
|
|
1186
|
+
if (!RUNTIME_CONFIG_KEY_SEGMENT.test(segment) || FORBIDDEN_RUNTIME_CONFIG_KEY_SEGMENTS.has(segment)) {
|
|
1187
|
+
throw new Error(`invalid runtime config key segment '${segment}'`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return segments;
|
|
1191
|
+
}
|
|
1192
|
+
function setRuntimeConfigValue(config, key, value) {
|
|
1193
|
+
const segments = parseRuntimeConfigKey(key);
|
|
1194
|
+
const next = JSON.parse(JSON.stringify(config));
|
|
1195
|
+
let cursor = next;
|
|
1196
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1197
|
+
const existing = cursor[segment];
|
|
1198
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
1199
|
+
cursor[segment] = {};
|
|
1200
|
+
}
|
|
1201
|
+
cursor = cursor[segment];
|
|
1202
|
+
}
|
|
1203
|
+
cursor[segments[segments.length - 1]] = value;
|
|
1204
|
+
return next;
|
|
1205
|
+
}
|
|
1206
|
+
function summarizeRuntimeConfigFields(config) {
|
|
1207
|
+
const fields = [];
|
|
1208
|
+
const visit = (prefix, value) => {
|
|
1209
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1210
|
+
fields.push(prefix);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1214
|
+
visit(prefix ? `${prefix}.${key}` : key, child);
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
visit("", config);
|
|
1218
|
+
return fields.filter(Boolean).sort();
|
|
1219
|
+
}
|
|
1220
|
+
function isSensitiveRuntimeConfigKey(key) {
|
|
1221
|
+
const lastSegment = key.split(".").pop();
|
|
1222
|
+
return /(password|secret|token|api[-_]?key|key)$/i.test(lastSegment);
|
|
1223
|
+
}
|
|
1224
|
+
function currentMachineId(deps) {
|
|
1225
|
+
return (0, machine_identity_1.loadOrCreateMachineIdentity)({
|
|
1226
|
+
homeDir: providerCliHomeDir(deps),
|
|
1227
|
+
now: () => providerCliNow(deps),
|
|
1228
|
+
}).machineId;
|
|
1229
|
+
}
|
|
1230
|
+
async function promptRuntimeConfigValue(command, deps) {
|
|
1231
|
+
if (command.value !== undefined)
|
|
1232
|
+
return command.value;
|
|
1233
|
+
if (isSensitiveRuntimeConfigKey(command.key)) {
|
|
1234
|
+
if (!deps.promptSecret) {
|
|
1235
|
+
throw new Error("secret entry requires an interactive terminal so the value can be hidden. Re-run without --value in a terminal, or pass --value only from a trusted non-logged script.");
|
|
1236
|
+
}
|
|
1237
|
+
return deps.promptSecret(`Value for ${command.key}: `);
|
|
1238
|
+
}
|
|
1239
|
+
const prompt = deps.promptInput;
|
|
1240
|
+
return prompt ? prompt(`Value for ${command.key}: `) : "";
|
|
1241
|
+
}
|
|
1242
|
+
function runtimeScopeLabel(scope) {
|
|
1243
|
+
return scope === "machine" ? "this machine's vault runtime config item" : "the agent vault runtime/config item";
|
|
1244
|
+
}
|
|
1245
|
+
async function storeRuntimeConfigKey(input) {
|
|
1246
|
+
const machineId = input.scope === "machine" ? currentMachineId(input.deps) : undefined;
|
|
1247
|
+
input.onProgress?.("checking existing runtime config");
|
|
1248
|
+
const current = input.scope === "machine"
|
|
1249
|
+
? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(input.agent, machineId, { preserveCachedOnFailure: true })
|
|
1250
|
+
: await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(input.agent, { preserveCachedOnFailure: true });
|
|
1251
|
+
if (!current.ok && current.reason !== "missing") {
|
|
1252
|
+
throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
|
|
1253
|
+
}
|
|
1254
|
+
input.onProgress?.(`storing ${input.key} in ${current.itemPath}`);
|
|
1255
|
+
const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, input.key, input.value);
|
|
1256
|
+
const stored = input.scope === "machine"
|
|
1257
|
+
? await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(input.agent, machineId, nextConfig, providerCliNow(input.deps))
|
|
1258
|
+
: await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(input.agent, nextConfig, providerCliNow(input.deps));
|
|
1259
|
+
input.onProgress?.(`stored ${input.key}; credential value was not printed`);
|
|
1260
|
+
return { revision: stored.revision, itemPath: stored.itemPath, ...(machineId ? { machineId } : {}) };
|
|
1261
|
+
}
|
|
1262
|
+
async function reloadRunningAgentAfterCredentialChange(agent, deps) {
|
|
1263
|
+
try {
|
|
1264
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
1265
|
+
if (!alive)
|
|
1266
|
+
return "daemon is not running; next `ouro up` will load it";
|
|
1267
|
+
const response = await deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent });
|
|
1268
|
+
if (response.ok)
|
|
1269
|
+
return `restarted ${agent} so the running agent reloads credentials`;
|
|
1270
|
+
return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
return `daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async function executeVaultConfigSet(command, deps) {
|
|
1277
|
+
if (command.agent === "SerpentGuide") {
|
|
1278
|
+
throw new Error("SerpentGuide does not have persistent runtime credentials. Store credentials in the hatchling agent vault.");
|
|
1279
|
+
}
|
|
1280
|
+
const scope = command.scope ?? "agent";
|
|
1281
|
+
const value = await promptRuntimeConfigValue(command, deps);
|
|
1282
|
+
if (!value) {
|
|
1283
|
+
throw new Error("vault config set requires --value <value> or an interactive prompt");
|
|
1284
|
+
}
|
|
1285
|
+
const progress = createHumanCommandProgress(deps, "vault config set");
|
|
1286
|
+
let stored;
|
|
1287
|
+
try {
|
|
1288
|
+
stored = await runCommandProgressPhase(progress, "storing runtime credential", () => storeRuntimeConfigKey({
|
|
1289
|
+
agent: command.agent,
|
|
1290
|
+
key: command.key,
|
|
1291
|
+
value,
|
|
1292
|
+
scope,
|
|
1293
|
+
deps,
|
|
1294
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1295
|
+
}), (result) => result.revision);
|
|
1296
|
+
}
|
|
1297
|
+
finally {
|
|
1298
|
+
progress.end();
|
|
1299
|
+
}
|
|
1300
|
+
/* v8 ignore next -- defensive: storeRuntimeConfigKey either returns or throws @preserve */
|
|
1301
|
+
if (!stored)
|
|
1302
|
+
throw new Error(`vault config set failed for ${command.agent}: no stored runtime credential result`);
|
|
1303
|
+
const message = [
|
|
1304
|
+
`stored ${command.key} for ${command.agent} in ${runtimeScopeLabel(scope)}`,
|
|
1305
|
+
`runtime credentials: ${stored.revision}`,
|
|
1306
|
+
`item: ${stored.itemPath}`,
|
|
1307
|
+
"value was not printed",
|
|
1308
|
+
].join("\n");
|
|
1309
|
+
deps.writeStdout(message);
|
|
1310
|
+
return message;
|
|
1311
|
+
}
|
|
1312
|
+
async function executeVaultConfigStatus(command, deps) {
|
|
1313
|
+
if (command.agent === "SerpentGuide") {
|
|
1314
|
+
const message = "SerpentGuide has no persistent runtime credentials. Hatch bootstrap stores selected credentials in the hatchling vault.";
|
|
1315
|
+
deps.writeStdout(message);
|
|
1316
|
+
return message;
|
|
1317
|
+
}
|
|
1318
|
+
const scopes = command.scope === "all" ? ["agent", "machine"] : [command.scope ?? "agent"];
|
|
1319
|
+
const lines = [`agent: ${command.agent}`];
|
|
1320
|
+
const progress = createHumanCommandProgress(deps, "vault config status");
|
|
1321
|
+
try {
|
|
1322
|
+
for (const scope of scopes) {
|
|
1323
|
+
const machineId = scope === "machine" ? currentMachineId(deps) : undefined;
|
|
1324
|
+
const runtime = await runCommandProgressPhase(progress, `reading ${scope} runtime config`, () => scope === "machine"
|
|
1325
|
+
? (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(command.agent, machineId, { preserveCachedOnFailure: true })
|
|
1326
|
+
: (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true }), (runtimeResult) => runtimeResult.ok ? runtimeResult.revision : runtimeResult.reason);
|
|
1327
|
+
if (scopes.length > 1)
|
|
1328
|
+
lines.push("");
|
|
1329
|
+
lines.push(`${scope} runtime config item: ${runtime.itemPath}`);
|
|
1330
|
+
if (runtime.ok) {
|
|
1331
|
+
lines.push(`status: available (${runtime.revision})`);
|
|
1332
|
+
const fields = summarizeRuntimeConfigFields(runtime.config);
|
|
1333
|
+
lines.push(`fields: ${fields.length === 0 ? "none stored" : fields.join(", ")}`);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
lines.push(`status: ${runtime.reason}`);
|
|
1337
|
+
lines.push(`error: ${runtime.error}`);
|
|
1338
|
+
lines.push(runtime.reason === "missing"
|
|
1339
|
+
? `fix: Run 'ouro vault config set --agent ${command.agent} --key <field>${scope === "machine" ? " --scope machine" : ""}' to store runtime credentials.`
|
|
1340
|
+
: `fix: ${(0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro vault config status'.")}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
finally {
|
|
1345
|
+
progress.end();
|
|
1346
|
+
}
|
|
1347
|
+
const message = lines.join("\n");
|
|
1348
|
+
deps.writeStdout(message);
|
|
1349
|
+
return message;
|
|
1350
|
+
}
|
|
1351
|
+
function requirePromptSecret(deps, purpose) {
|
|
1352
|
+
if (deps.promptSecret)
|
|
1353
|
+
return deps.promptSecret;
|
|
1354
|
+
throw new Error(`${purpose} requires an interactive terminal so the secret can be hidden.`);
|
|
1355
|
+
}
|
|
1356
|
+
function requirePromptInput(deps, purpose) {
|
|
1357
|
+
if (deps.promptInput)
|
|
1358
|
+
return deps.promptInput;
|
|
1359
|
+
throw new Error(`${purpose} requires an interactive terminal.`);
|
|
1360
|
+
}
|
|
1361
|
+
function parseOptionalPort(value, fallback, label) {
|
|
1362
|
+
const trimmed = value.trim();
|
|
1363
|
+
if (!trimmed)
|
|
1364
|
+
return fallback;
|
|
1365
|
+
const parsed = Number(trimmed);
|
|
1366
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
1367
|
+
throw new Error(`${label} must be an integer between 1 and 65535`);
|
|
1368
|
+
}
|
|
1369
|
+
return parsed;
|
|
1370
|
+
}
|
|
1371
|
+
function parseOptionalPositiveInteger(value, fallback, label) {
|
|
1372
|
+
const trimmed = value.trim();
|
|
1373
|
+
if (!trimmed)
|
|
1374
|
+
return fallback;
|
|
1375
|
+
const parsed = Number(trimmed);
|
|
1376
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
1377
|
+
throw new Error(`${label} must be a positive integer`);
|
|
1378
|
+
}
|
|
1379
|
+
return parsed;
|
|
1380
|
+
}
|
|
1381
|
+
function normalizeWebhookPath(value, fallback) {
|
|
1382
|
+
const trimmed = value.trim();
|
|
1383
|
+
if (!trimmed)
|
|
1384
|
+
return fallback;
|
|
1385
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
1386
|
+
}
|
|
1387
|
+
function enableAgentSense(agent, sense, deps) {
|
|
1388
|
+
const { configPath } = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot);
|
|
1389
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1390
|
+
const senses = raw.senses && typeof raw.senses === "object" && !Array.isArray(raw.senses)
|
|
1391
|
+
? raw.senses
|
|
1392
|
+
: {};
|
|
1393
|
+
const existing = senses[sense] && typeof senses[sense] === "object" && !Array.isArray(senses[sense])
|
|
1394
|
+
? senses[sense]
|
|
1395
|
+
: {};
|
|
1396
|
+
raw.senses = {
|
|
1397
|
+
...senses,
|
|
1398
|
+
cli: senses.cli ?? { enabled: true },
|
|
1399
|
+
teams: senses.teams ?? { enabled: false },
|
|
1400
|
+
[sense]: { ...existing, enabled: true },
|
|
1401
|
+
};
|
|
1402
|
+
fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
1403
|
+
}
|
|
1404
|
+
function connectMenu(agent) {
|
|
1405
|
+
return [
|
|
1406
|
+
`Connect ${agent}`,
|
|
1407
|
+
"Pick what this agent should be able to use.",
|
|
1408
|
+
"",
|
|
1409
|
+
" 1. Perplexity search",
|
|
1410
|
+
" Portable. Stores an API key in the agent vault.",
|
|
1411
|
+
"",
|
|
1412
|
+
" 2. BlueBubbles iMessage",
|
|
1413
|
+
" This machine only. Connects a local Mac Messages bridge.",
|
|
1414
|
+
"",
|
|
1415
|
+
" 3. Provider auth",
|
|
1416
|
+
` Model credentials: ouro auth --agent ${agent} --provider <provider>`,
|
|
1417
|
+
"",
|
|
1418
|
+
" 4. Cancel",
|
|
1419
|
+
"",
|
|
1420
|
+
"Choose [1-4]: ",
|
|
1421
|
+
].join("\n");
|
|
1422
|
+
}
|
|
1423
|
+
async function executeConnectPerplexity(agent, deps) {
|
|
1424
|
+
if (agent === "SerpentGuide") {
|
|
1425
|
+
throw new Error("SerpentGuide has no persistent runtime credentials. Connect Perplexity on the hatchling agent instead.");
|
|
1426
|
+
}
|
|
1427
|
+
const promptSecret = requirePromptSecret(deps, "Perplexity API key entry");
|
|
1428
|
+
deps.writeStdout([
|
|
1429
|
+
`Connect Perplexity for ${agent}`,
|
|
1430
|
+
"The API key stays hidden while you type.",
|
|
1431
|
+
`Ouro stores it in ${agent}'s vault runtime/config item.`,
|
|
1432
|
+
].join("\n"));
|
|
1433
|
+
const key = (await promptSecret("Perplexity API key: ")).trim();
|
|
1434
|
+
if (!key)
|
|
1435
|
+
throw new Error("Perplexity API key cannot be blank");
|
|
1436
|
+
const progress = createHumanCommandProgress(deps, "connect perplexity");
|
|
1437
|
+
let stored;
|
|
1438
|
+
let reload;
|
|
1439
|
+
try {
|
|
1440
|
+
progress.startPhase("saving Perplexity search");
|
|
1441
|
+
stored = await storeRuntimeConfigKey({
|
|
1442
|
+
agent,
|
|
1443
|
+
key: "integrations.perplexityApiKey",
|
|
1444
|
+
value: key,
|
|
1445
|
+
scope: "agent",
|
|
1446
|
+
deps,
|
|
1447
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1448
|
+
});
|
|
1449
|
+
progress.completePhase("saving Perplexity search", "secret stored");
|
|
1450
|
+
progress.startPhase(`reloading ${agent}`);
|
|
1451
|
+
reload = await reloadRunningAgentAfterCredentialChange(agent, deps);
|
|
1452
|
+
progress.completePhase(`reloading ${agent}`, reload);
|
|
1453
|
+
progress.end();
|
|
1454
|
+
}
|
|
1455
|
+
catch (error) {
|
|
1456
|
+
progress.end();
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1459
|
+
const message = [
|
|
1460
|
+
`Perplexity connected for ${agent}`,
|
|
1461
|
+
"capability: Perplexity search",
|
|
1462
|
+
`stored: ${stored.itemPath}`,
|
|
1463
|
+
`reload: ${reload}`,
|
|
1464
|
+
"secret was not printed",
|
|
1465
|
+
"",
|
|
1466
|
+
"Next: ask the agent to search.",
|
|
1467
|
+
].join("\n");
|
|
1468
|
+
deps.writeStdout(message);
|
|
1469
|
+
return message;
|
|
1470
|
+
}
|
|
1471
|
+
async function executeConnectBlueBubbles(agent, deps) {
|
|
1472
|
+
if (agent === "SerpentGuide") {
|
|
1473
|
+
throw new Error("SerpentGuide has no persistent runtime credentials. Attach BlueBubbles on the hatchling agent instead.");
|
|
1474
|
+
}
|
|
1475
|
+
const promptInput = requirePromptInput(deps, "BlueBubbles setup");
|
|
1476
|
+
const promptSecret = requirePromptSecret(deps, "BlueBubbles password entry");
|
|
1477
|
+
deps.writeStdout([
|
|
1478
|
+
`Connect BlueBubbles for ${agent}`,
|
|
1479
|
+
"This is a local attachment for this machine.",
|
|
1480
|
+
"The app password stays hidden while you type.",
|
|
1481
|
+
].join("\n"));
|
|
1482
|
+
const serverUrl = (await promptInput("BlueBubbles server URL for this machine: ")).trim();
|
|
1483
|
+
if (!serverUrl)
|
|
1484
|
+
throw new Error("BlueBubbles server URL cannot be blank");
|
|
1485
|
+
const password = (await promptSecret("BlueBubbles app password: ")).trim();
|
|
1486
|
+
if (!password)
|
|
1487
|
+
throw new Error("BlueBubbles app password cannot be blank");
|
|
1488
|
+
const port = parseOptionalPort(await promptInput("Local webhook port [18790]: "), 18790, "BlueBubbles webhook port");
|
|
1489
|
+
const webhookPath = normalizeWebhookPath(await promptInput("Local webhook path [/bluebubbles-webhook]: "), "/bluebubbles-webhook");
|
|
1490
|
+
const requestTimeoutMs = parseOptionalPositiveInteger(await promptInput("Request timeout ms [30000]: "), 30000, "BlueBubbles request timeout");
|
|
1491
|
+
const machineId = currentMachineId(deps);
|
|
1492
|
+
const progress = createHumanCommandProgress(deps, "connect bluebubbles");
|
|
1493
|
+
let stored;
|
|
1494
|
+
try {
|
|
1495
|
+
progress.startPhase("saving BlueBubbles attachment");
|
|
1496
|
+
progress.updateDetail("checking existing machine runtime config");
|
|
1497
|
+
const current = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, machineId, { preserveCachedOnFailure: true });
|
|
1498
|
+
if (!current.ok && current.reason !== "missing") {
|
|
1499
|
+
progress.end();
|
|
1500
|
+
throw new Error(`cannot read existing machine runtime credentials from ${current.itemPath}: ${current.error}`);
|
|
1501
|
+
}
|
|
1502
|
+
const nextConfig = {
|
|
1503
|
+
...(current.ok ? current.config : {}),
|
|
1504
|
+
bluebubbles: {
|
|
1505
|
+
serverUrl,
|
|
1506
|
+
password,
|
|
1507
|
+
accountId: "default",
|
|
1508
|
+
},
|
|
1509
|
+
bluebubblesChannel: {
|
|
1510
|
+
port,
|
|
1511
|
+
webhookPath,
|
|
1512
|
+
requestTimeoutMs,
|
|
1513
|
+
},
|
|
1514
|
+
};
|
|
1515
|
+
progress.updateDetail("storing local machine config");
|
|
1516
|
+
stored = await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(agent, machineId, nextConfig, providerCliNow(deps));
|
|
1517
|
+
progress.updateDetail("enabling BlueBubbles in agent.json");
|
|
1518
|
+
enableAgentSense(agent, "bluebubbles", deps);
|
|
1519
|
+
progress.completePhase("saving BlueBubbles attachment", "secret stored");
|
|
1520
|
+
progress.end();
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
progress.end();
|
|
1524
|
+
throw error;
|
|
1525
|
+
}
|
|
1526
|
+
const message = appendBundleSyncSummary([
|
|
1527
|
+
`BlueBubbles attached for ${agent} on this machine`,
|
|
1528
|
+
`machine: ${machineId}`,
|
|
1529
|
+
`stored: ${stored.itemPath}`,
|
|
1530
|
+
"agent.json: senses.bluebubbles.enabled = true",
|
|
1531
|
+
"secret was not printed",
|
|
1532
|
+
"",
|
|
1533
|
+
"Next: point BlueBubbles at this machine's webhook, then run `ouro up`.",
|
|
1534
|
+
].join("\n"), agent, deps);
|
|
1535
|
+
deps.writeStdout(message);
|
|
1536
|
+
return message;
|
|
1537
|
+
}
|
|
1538
|
+
async function executeConnect(command, deps) {
|
|
1539
|
+
if (command.target === "perplexity")
|
|
1540
|
+
return executeConnectPerplexity(command.agent, deps);
|
|
1541
|
+
if (command.target === "bluebubbles")
|
|
1542
|
+
return executeConnectBlueBubbles(command.agent, deps);
|
|
1543
|
+
const promptInput = deps.promptInput;
|
|
1544
|
+
if (!promptInput) {
|
|
1545
|
+
const message = [
|
|
1546
|
+
connectMenu(command.agent).replace(/\nChoose \[1-4\]: $/, ""),
|
|
1547
|
+
"",
|
|
1548
|
+
`Run: ouro connect perplexity --agent ${command.agent}`,
|
|
1549
|
+
`Or: ouro connect bluebubbles --agent ${command.agent}`,
|
|
1550
|
+
].join("\n");
|
|
1551
|
+
deps.writeStdout(message);
|
|
1552
|
+
return message;
|
|
1553
|
+
}
|
|
1554
|
+
const answer = (await promptInput(connectMenu(command.agent))).trim().toLowerCase();
|
|
1555
|
+
if (answer === "1" || answer === "perplexity" || answer === "perplexity-search") {
|
|
1556
|
+
return executeConnectPerplexity(command.agent, deps);
|
|
1557
|
+
}
|
|
1558
|
+
if (answer === "2" || answer === "bluebubbles" || answer === "imessage" || answer === "messages") {
|
|
1559
|
+
return executeConnectBlueBubbles(command.agent, deps);
|
|
1560
|
+
}
|
|
1561
|
+
if (answer === "3" || answer === "provider" || answer === "providers" || answer === "auth") {
|
|
1562
|
+
const message = [
|
|
1563
|
+
"Provider auth is its own flow:",
|
|
1564
|
+
` ouro auth --agent ${command.agent} --provider <provider>`,
|
|
1565
|
+
"",
|
|
1566
|
+
"Use `ouro connect` for integrations and local senses after provider auth is ready.",
|
|
1567
|
+
].join("\n");
|
|
1568
|
+
deps.writeStdout(message);
|
|
1569
|
+
return message;
|
|
1570
|
+
}
|
|
1571
|
+
const message = "connect cancelled.";
|
|
1572
|
+
deps.writeStdout(message);
|
|
1573
|
+
return message;
|
|
1574
|
+
}
|
|
1575
|
+
function readOrBootstrapProviderState(agentName, deps) {
|
|
1576
|
+
const agentRoot = providerCliAgentRoot({ agent: agentName }, deps);
|
|
1577
|
+
const readResult = (0, provider_state_1.readProviderState)(agentRoot);
|
|
1578
|
+
if (readResult.ok)
|
|
1579
|
+
return { agentRoot, state: readResult.state };
|
|
1580
|
+
if (readResult.reason === "invalid") {
|
|
1581
|
+
throw new Error(`provider state for ${agentName} is invalid at ${readResult.statePath}: ${readResult.error}`);
|
|
1582
|
+
}
|
|
1583
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(agentName, deps.bundlesRoot);
|
|
1584
|
+
const homeDir = providerCliHomeDir(deps);
|
|
1585
|
+
const machine = (0, machine_identity_1.loadOrCreateMachineIdentity)({
|
|
1586
|
+
homeDir,
|
|
1587
|
+
now: () => providerCliNow(deps),
|
|
1588
|
+
});
|
|
1589
|
+
const state = (0, provider_state_1.bootstrapProviderStateFromAgentConfig)({
|
|
1590
|
+
machineId: machine.machineId,
|
|
1591
|
+
now: providerCliNow(deps),
|
|
1592
|
+
agentConfig: {
|
|
1593
|
+
humanFacing: {
|
|
1594
|
+
provider: config.humanFacing.provider,
|
|
1595
|
+
model: config.humanFacing.model || (0, provider_models_1.getDefaultModelForProvider)(config.humanFacing.provider),
|
|
1596
|
+
},
|
|
1597
|
+
agentFacing: {
|
|
1598
|
+
provider: config.agentFacing.provider,
|
|
1599
|
+
model: config.agentFacing.model || (0, provider_models_1.getDefaultModelForProvider)(config.agentFacing.provider),
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
});
|
|
1603
|
+
(0, provider_state_1.writeProviderState)(agentRoot, state);
|
|
1604
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1605
|
+
component: "daemon",
|
|
1606
|
+
event: "daemon.provider_state_bootstrapped",
|
|
1607
|
+
message: "bootstrapped local provider state from agent config",
|
|
1608
|
+
meta: { agent: agentName, agentRoot },
|
|
1609
|
+
});
|
|
1610
|
+
return { agentRoot, state };
|
|
1611
|
+
}
|
|
1612
|
+
function credentialPingConfig(record) {
|
|
1613
|
+
return {
|
|
1614
|
+
...record.credentials,
|
|
1615
|
+
...record.config,
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
function pingAttemptCount(result) {
|
|
1619
|
+
if (typeof result.attempts === "number")
|
|
1620
|
+
return result.attempts;
|
|
1621
|
+
if (Array.isArray(result.attempts))
|
|
1622
|
+
return result.attempts.length;
|
|
1623
|
+
return undefined;
|
|
1624
|
+
}
|
|
1625
|
+
async function readProviderCredentialRecord(agent, provider, _deps, options = {}) {
|
|
1626
|
+
const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agent, options);
|
|
1627
|
+
if (poolResult.ok) {
|
|
1628
|
+
const existing = poolResult.pool.providers[provider];
|
|
1629
|
+
if (existing)
|
|
1630
|
+
return { ok: true, record: existing };
|
|
1631
|
+
}
|
|
1632
|
+
else if (poolResult.reason === "invalid" || poolResult.reason === "unavailable") {
|
|
1633
|
+
return { ok: false, reason: poolResult.reason, poolPath: poolResult.poolPath, error: poolResult.error };
|
|
1634
|
+
}
|
|
1635
|
+
return {
|
|
1636
|
+
ok: false,
|
|
1637
|
+
reason: "missing",
|
|
1638
|
+
poolPath: poolResult.poolPath,
|
|
1639
|
+
error: `no credentials stored for ${provider}`,
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function writeProviderBinding(input) {
|
|
1643
|
+
const updatedAt = providerCliNow(input.deps).toISOString();
|
|
1644
|
+
input.state.updatedAt = updatedAt;
|
|
1645
|
+
input.state.lanes[input.lane] = {
|
|
1646
|
+
provider: input.provider,
|
|
1647
|
+
model: input.model,
|
|
1648
|
+
source: "local",
|
|
1649
|
+
updatedAt,
|
|
1650
|
+
};
|
|
1651
|
+
input.state.readiness[input.lane] = {
|
|
1652
|
+
status: input.status,
|
|
1653
|
+
provider: input.provider,
|
|
1654
|
+
model: input.model,
|
|
1655
|
+
checkedAt: updatedAt,
|
|
1656
|
+
...(input.credentialRevision ? { credentialRevision: input.credentialRevision } : {}),
|
|
1657
|
+
...(input.error ? { error: input.error } : {}),
|
|
1658
|
+
...(input.attempts !== undefined ? { attempts: input.attempts } : {}),
|
|
1659
|
+
};
|
|
1660
|
+
(0, provider_state_1.writeProviderState)(input.agentRoot, input.state);
|
|
1661
|
+
}
|
|
1662
|
+
function writeProviderReadiness(input) {
|
|
1663
|
+
const checkedAt = providerCliNow(input.deps).toISOString();
|
|
1664
|
+
input.state.updatedAt = checkedAt;
|
|
1665
|
+
input.state.readiness[input.lane] = {
|
|
1666
|
+
status: input.status,
|
|
1667
|
+
provider: input.provider,
|
|
1668
|
+
model: input.model,
|
|
1669
|
+
checkedAt,
|
|
1670
|
+
credentialRevision: input.credentialRevision,
|
|
1671
|
+
...(input.error ? { error: input.error } : {}),
|
|
1672
|
+
...(input.attempts !== undefined ? { attempts: input.attempts } : {}),
|
|
1673
|
+
};
|
|
1674
|
+
(0, provider_state_1.writeProviderState)(input.agentRoot, input.state);
|
|
1675
|
+
}
|
|
1676
|
+
async function executeProviderUse(command, deps, options = {}) {
|
|
1677
|
+
const progress = options.writeStdout === false ? null : createHumanCommandProgress(deps, "provider use");
|
|
1678
|
+
const writeMessage = (message) => {
|
|
1679
|
+
progress?.end();
|
|
1680
|
+
if (options.writeStdout !== false)
|
|
1681
|
+
deps.writeStdout(message);
|
|
1682
|
+
return message;
|
|
1683
|
+
};
|
|
1684
|
+
try {
|
|
1685
|
+
const { agentRoot, state } = readOrBootstrapProviderState(command.agent, deps);
|
|
1686
|
+
progress?.startPhase(`reading ${command.provider} credentials`);
|
|
1687
|
+
const credential = await readProviderCredentialRecord(command.agent, command.provider, deps, {
|
|
1688
|
+
onProgress: (message) => progress?.updateDetail(message),
|
|
1689
|
+
});
|
|
1690
|
+
if (!credential.ok) {
|
|
1691
|
+
progress?.completePhase(`reading ${command.provider} credentials`, credential.reason);
|
|
1692
|
+
if (!command.force) {
|
|
1693
|
+
const message = [
|
|
1694
|
+
`no credentials stored for ${command.provider}.`,
|
|
1695
|
+
`Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`,
|
|
1696
|
+
].join("\n");
|
|
1697
|
+
return writeMessage(message);
|
|
1698
|
+
}
|
|
1699
|
+
writeProviderBinding({
|
|
1700
|
+
agentRoot,
|
|
1701
|
+
state,
|
|
1702
|
+
lane: command.lane,
|
|
1703
|
+
provider: command.provider,
|
|
1704
|
+
model: command.model,
|
|
1705
|
+
deps,
|
|
1706
|
+
status: "failed",
|
|
1707
|
+
error: credential.error,
|
|
1708
|
+
});
|
|
1709
|
+
const message = `forced ${command.agent} ${command.lane} to ${command.provider} / ${command.model}: failed (${credential.error})`;
|
|
1710
|
+
return writeMessage(message);
|
|
1711
|
+
}
|
|
1712
|
+
progress?.completePhase(`reading ${command.provider} credentials`, "found");
|
|
1713
|
+
progress?.startPhase(`checking ${command.provider} / ${command.model}`);
|
|
1714
|
+
const pingResult = await (0, provider_ping_1.pingProvider)(command.provider, credentialPingConfig(credential.record), {
|
|
1715
|
+
model: command.model,
|
|
1716
|
+
attemptPolicy: { baseDelayMs: 0 },
|
|
1717
|
+
sleep: deps.sleep,
|
|
1718
|
+
});
|
|
1719
|
+
const attempts = pingAttemptCount(pingResult);
|
|
1720
|
+
const status = pingResult.ok ? "ready" : `failed (${pingResult.message})`;
|
|
1721
|
+
progress?.completePhase(`checking ${command.provider} / ${command.model}`, status);
|
|
1722
|
+
if (!pingResult.ok && !command.force) {
|
|
1723
|
+
const message = [
|
|
1724
|
+
`${command.agent} ${command.lane} ${command.provider} / ${command.model}: failed (${pingResult.message})`,
|
|
1725
|
+
`Fix credentials with \`ouro auth --agent ${command.agent} --provider ${command.provider}\` or force the local binding with \`ouro use --agent ${command.agent} --lane ${command.lane} --provider ${command.provider} --model ${command.model} --force\`.`,
|
|
1726
|
+
].join("\n");
|
|
1727
|
+
return writeMessage(message);
|
|
1728
|
+
}
|
|
1729
|
+
writeProviderBinding({
|
|
1730
|
+
agentRoot,
|
|
1731
|
+
state,
|
|
1732
|
+
lane: command.lane,
|
|
1733
|
+
provider: command.provider,
|
|
1734
|
+
model: command.model,
|
|
1735
|
+
deps,
|
|
1736
|
+
status: pingResult.ok ? "ready" : "failed",
|
|
1737
|
+
credentialRevision: credential.record.revision,
|
|
1738
|
+
...(!pingResult.ok ? { error: pingResult.message } : {}),
|
|
1739
|
+
...(attempts !== undefined ? { attempts } : {}),
|
|
1740
|
+
});
|
|
1741
|
+
const message = `${command.force ? "forced " : ""}${command.agent} ${command.lane} ${command.provider} / ${command.model}: ${status}`;
|
|
1742
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1743
|
+
component: "daemon",
|
|
1744
|
+
event: "daemon.provider_use_completed",
|
|
1745
|
+
message: "provider use command completed",
|
|
1746
|
+
meta: { agent: command.agent, lane: command.lane, provider: command.provider, model: command.model, status: pingResult.ok ? "ready" : "failed" },
|
|
1747
|
+
});
|
|
1748
|
+
return writeMessage(message);
|
|
1749
|
+
}
|
|
1750
|
+
catch (error) {
|
|
1751
|
+
progress?.end();
|
|
1752
|
+
throw error;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
async function executeProviderCheck(command, deps) {
|
|
1756
|
+
const progress = createHumanCommandProgress(deps, "provider check");
|
|
1757
|
+
const writeMessage = (message) => {
|
|
1758
|
+
progress.end();
|
|
1759
|
+
deps.writeStdout(message);
|
|
1760
|
+
return message;
|
|
1761
|
+
};
|
|
1762
|
+
try {
|
|
1763
|
+
const { agentRoot, state } = readOrBootstrapProviderState(command.agent, deps);
|
|
1764
|
+
const binding = state.lanes[command.lane];
|
|
1765
|
+
progress.startPhase(`reading ${binding.provider} credentials`);
|
|
1766
|
+
const credential = await readProviderCredentialRecord(command.agent, binding.provider, deps, {
|
|
1767
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1768
|
+
});
|
|
1769
|
+
if (!credential.ok) {
|
|
1770
|
+
progress.completePhase(`reading ${binding.provider} credentials`, credential.reason);
|
|
1771
|
+
const message = [
|
|
1772
|
+
`${command.agent} ${command.lane} ${binding.provider} / ${binding.model}: unknown (${credential.error})`,
|
|
1773
|
+
`Run \`ouro auth --agent ${command.agent} --provider ${binding.provider}\` first.`,
|
|
1774
|
+
].join("\n");
|
|
1775
|
+
return writeMessage(message);
|
|
1776
|
+
}
|
|
1777
|
+
progress.completePhase(`reading ${binding.provider} credentials`, "found");
|
|
1778
|
+
progress.startPhase(`checking ${binding.provider} / ${binding.model}`);
|
|
1779
|
+
const pingResult = await (0, provider_ping_1.pingProvider)(binding.provider, credentialPingConfig(credential.record), {
|
|
1780
|
+
model: binding.model,
|
|
1781
|
+
attemptPolicy: { baseDelayMs: 0 },
|
|
1782
|
+
sleep: deps.sleep,
|
|
1783
|
+
});
|
|
1784
|
+
const attempts = pingAttemptCount(pingResult);
|
|
1785
|
+
const status = pingResult.ok ? "ready" : `failed (${pingResult.message})`;
|
|
1786
|
+
progress.completePhase(`checking ${binding.provider} / ${binding.model}`, status);
|
|
1787
|
+
writeProviderReadiness({
|
|
1788
|
+
agentRoot,
|
|
1789
|
+
state,
|
|
1790
|
+
lane: command.lane,
|
|
1791
|
+
provider: binding.provider,
|
|
1792
|
+
model: binding.model,
|
|
1793
|
+
deps,
|
|
1794
|
+
status: pingResult.ok ? "ready" : "failed",
|
|
1795
|
+
credentialRevision: credential.record.revision,
|
|
1796
|
+
...(!pingResult.ok ? { error: pingResult.message } : {}),
|
|
1797
|
+
...(attempts !== undefined ? { attempts } : {}),
|
|
1798
|
+
});
|
|
1799
|
+
const message = `${command.agent} ${command.lane} ${binding.provider} / ${binding.model}: ${status}`;
|
|
1800
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1801
|
+
component: "daemon",
|
|
1802
|
+
event: "daemon.provider_check_completed",
|
|
1803
|
+
message: "provider check command completed",
|
|
1804
|
+
meta: { agent: command.agent, lane: command.lane, provider: binding.provider, model: binding.model, status: pingResult.ok ? "ready" : "failed" },
|
|
1805
|
+
});
|
|
1806
|
+
return writeMessage(message);
|
|
1807
|
+
}
|
|
1808
|
+
catch (error) {
|
|
1809
|
+
progress.end();
|
|
1810
|
+
throw error;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function renderProviderCredentialLine(credential) {
|
|
1814
|
+
if (credential.status === "present") {
|
|
1815
|
+
const credentialFields = credential.credentialFields.length > 0 ? ` credentials: ${credential.credentialFields.join(", ")}` : " credentials: none";
|
|
1816
|
+
const configFields = credential.configFields.length > 0 ? ` config: ${credential.configFields.join(", ")}` : " config: none";
|
|
1817
|
+
return `credentials: present in vault (${credential.source}; ${credential.revision};${credentialFields};${configFields})`;
|
|
1818
|
+
}
|
|
1819
|
+
if (credential.status === "invalid-pool") {
|
|
1820
|
+
return `credentials: vault unavailable (${credential.error}); repair: ${credential.repair.command}`;
|
|
1821
|
+
}
|
|
1822
|
+
return `credentials: missing; repair: ${credential.repair.command}`;
|
|
1823
|
+
}
|
|
1824
|
+
async function executeProviderStatus(command, deps) {
|
|
1825
|
+
const agentRoot = providerCliAgentRoot(command, deps);
|
|
1826
|
+
const progress = createHumanCommandProgress(deps, "provider status");
|
|
1827
|
+
try {
|
|
1828
|
+
await runCommandProgressPhase(progress, "reading provider credentials", () => (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, {
|
|
1829
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1830
|
+
}), (poolResult) => {
|
|
1831
|
+
if (!poolResult.ok)
|
|
1832
|
+
return poolResult.reason;
|
|
1833
|
+
const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(poolResult.pool);
|
|
1834
|
+
return summary.providers.map((provider) => provider.provider).join(", ") || "none stored";
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
finally {
|
|
1838
|
+
progress.end();
|
|
1839
|
+
}
|
|
1840
|
+
const homeDir = providerCliHomeDir(deps);
|
|
1841
|
+
const lines = [`provider status: ${command.agent}`];
|
|
1842
|
+
for (const lane of ["outward", "inner"]) {
|
|
1843
|
+
const resolved = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({
|
|
1844
|
+
agentName: command.agent,
|
|
1845
|
+
agentRoot,
|
|
1846
|
+
homeDir,
|
|
1847
|
+
lane,
|
|
1848
|
+
});
|
|
1849
|
+
if (!resolved.ok) {
|
|
1850
|
+
lines.push(` ${lane}: unavailable`);
|
|
1851
|
+
lines.push(` ${resolved.reason}: ${resolved.repair.command}`);
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
const binding = resolved.binding;
|
|
1855
|
+
lines.push(` ${lane}: ${binding.provider} / ${binding.model} (${binding.source})`);
|
|
1856
|
+
lines.push(` readiness: ${binding.readiness.status}${binding.readiness.error ? ` (${binding.readiness.error})` : ""}`);
|
|
1857
|
+
lines.push(` ${renderProviderCredentialLine(binding.credential)}`);
|
|
1858
|
+
for (const warning of binding.warnings) {
|
|
1859
|
+
lines.push(` warning: ${warning.message}`);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const message = lines.join("\n");
|
|
1863
|
+
deps.writeStdout(message);
|
|
1864
|
+
return message;
|
|
1865
|
+
}
|
|
1866
|
+
async function executeProviderRefresh(command, deps) {
|
|
1867
|
+
if (command.agent === "SerpentGuide") {
|
|
1868
|
+
const message = "SerpentGuide has no persistent provider credentials to refresh. Hatch bootstrap uses selected credentials in memory only.";
|
|
1869
|
+
deps.writeStdout(message);
|
|
1870
|
+
return message;
|
|
1871
|
+
}
|
|
1872
|
+
const progress = createHumanCommandProgress(deps, "provider refresh");
|
|
1873
|
+
progress.startPhase("refreshing provider credentials");
|
|
1874
|
+
let pool;
|
|
1875
|
+
try {
|
|
1876
|
+
pool = await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, {
|
|
1877
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
catch (error) {
|
|
1881
|
+
progress.end();
|
|
1882
|
+
throw error;
|
|
1883
|
+
}
|
|
1884
|
+
const lines = [];
|
|
1885
|
+
if (pool.ok) {
|
|
1886
|
+
const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(pool.pool);
|
|
1887
|
+
lines.push(`refreshed provider credential snapshot for ${command.agent}`);
|
|
1888
|
+
lines.push(`providers: ${summary.providers.map((provider) => provider.provider).join(", ") || "none"}`);
|
|
1889
|
+
progress.completePhase("refreshing provider credentials", summary.providers.map((provider) => provider.provider).join(", ") || "none");
|
|
1890
|
+
}
|
|
1891
|
+
else {
|
|
1892
|
+
progress.end();
|
|
1893
|
+
lines.push(`provider credential refresh failed for ${command.agent}: ${pool.error}`);
|
|
1894
|
+
lines.push((0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro provider refresh'."));
|
|
1895
|
+
const message = lines.join("\n");
|
|
1896
|
+
deps.writeStdout(message);
|
|
1897
|
+
return message;
|
|
1898
|
+
}
|
|
1899
|
+
progress.startPhase(`reloading ${command.agent}`);
|
|
1900
|
+
const reload = await reloadRunningAgentAfterCredentialChange(command.agent, deps);
|
|
1901
|
+
progress.completePhase(`reloading ${command.agent}`, reload);
|
|
1902
|
+
progress.end();
|
|
1903
|
+
lines.push(reload === "daemon is not running; next `ouro up` will load it"
|
|
1904
|
+
? "daemon is not running; the next start will load the refreshed snapshot"
|
|
1905
|
+
: reload);
|
|
1906
|
+
const message = lines.join("\n");
|
|
1907
|
+
deps.writeStdout(message);
|
|
1908
|
+
return message;
|
|
1909
|
+
}
|
|
1910
|
+
async function executeAuthRun(command, deps) {
|
|
1911
|
+
const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot).config.humanFacing.provider;
|
|
1912
|
+
/* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
|
|
1913
|
+
const authRunner = deps.runAuthFlow ?? (await Promise.resolve().then(() => __importStar(require("../auth/auth-flow")))).runRuntimeAuthFlow;
|
|
1914
|
+
const progress = createHumanCommandProgress(deps, "auth");
|
|
1915
|
+
progress.startPhase(`authenticating ${provider}`);
|
|
1916
|
+
let result;
|
|
1917
|
+
try {
|
|
1918
|
+
result = await authRunner({
|
|
1919
|
+
agentName: command.agent,
|
|
1920
|
+
provider,
|
|
1921
|
+
promptInput: deps.promptInput,
|
|
1922
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
catch (error) {
|
|
1926
|
+
progress.end();
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
progress.completePhase(`authenticating ${provider}`, "credentials stored");
|
|
1930
|
+
// Behavior: ouro auth stores credentials only — does NOT switch provider.
|
|
1931
|
+
// Use `ouro auth switch` to change the active provider.
|
|
1932
|
+
// Verify the credentials actually work by pinging the provider.
|
|
1933
|
+
/* v8 ignore start -- integration: real API ping after auth @preserve */
|
|
1934
|
+
try {
|
|
1935
|
+
progress.startPhase(`verifying ${provider}`);
|
|
1936
|
+
const credential = await readProviderCredentialRecord(command.agent, provider, deps, {
|
|
1937
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1938
|
+
});
|
|
1939
|
+
const status = credential.ok
|
|
1940
|
+
? await verifyProviderCredentials(provider, {
|
|
1941
|
+
[provider]: { ...credential.record.config, ...credential.record.credentials },
|
|
1942
|
+
})
|
|
1943
|
+
: `stored but could not be re-read from vault (${credential.error})`;
|
|
1944
|
+
progress.completePhase(`verifying ${provider}`, status);
|
|
1945
|
+
}
|
|
1946
|
+
catch (error) {
|
|
1947
|
+
// Verification failure is non-blocking — credentials were saved regardless.
|
|
1948
|
+
progress.completePhase(`verifying ${provider}`, `skipped (${error instanceof Error ? error.message : String(error)})`);
|
|
1949
|
+
}
|
|
1950
|
+
/* v8 ignore stop */
|
|
1951
|
+
progress.end();
|
|
1952
|
+
deps.writeStdout(result.message);
|
|
1953
|
+
return result.message;
|
|
1954
|
+
}
|
|
1955
|
+
async function readinessReportForAgent(agent, deps) {
|
|
1956
|
+
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
1957
|
+
try {
|
|
1958
|
+
const result = await checkAgentProviderHealth(agent, bundlesRoot, deps);
|
|
1959
|
+
if (result.ok) {
|
|
1960
|
+
return { agent, ok: true, issues: [] };
|
|
1961
|
+
}
|
|
1962
|
+
else {
|
|
1963
|
+
const issue = result.issue ?? (0, readiness_repair_1.genericReadinessIssue)({
|
|
1964
|
+
summary: result.error ?? `${agent} is not ready.`,
|
|
1965
|
+
...(result.fix ? { fix: result.fix } : {}),
|
|
1966
|
+
});
|
|
1967
|
+
return { agent, ok: false, issues: [issue] };
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
catch (error) {
|
|
1971
|
+
return {
|
|
1972
|
+
agent,
|
|
1973
|
+
ok: false,
|
|
1974
|
+
issues: [(0, readiness_repair_1.genericReadinessIssue)({
|
|
1975
|
+
summary: `${agent} readiness check failed.`,
|
|
1976
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1977
|
+
fix: "Run 'ouro doctor' for diagnostics, then retry 'ouro repair'.",
|
|
1978
|
+
})],
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
async function executeReadinessRepairAction(agent, action, deps) {
|
|
1983
|
+
if (action.kind === "vault-create") {
|
|
1984
|
+
await executeVaultCreate({ kind: "vault.create", agent }, deps);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (action.kind === "vault-unlock") {
|
|
1988
|
+
await executeVaultUnlock({ kind: "vault.unlock", agent }, deps);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
if (action.kind === "vault-replace") {
|
|
1992
|
+
await executeVaultReplace({ kind: "vault.replace", agent }, deps);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
if (action.kind === "provider-auth") {
|
|
1996
|
+
await executeAuthRun({ kind: "auth.run", agent, provider: action.provider }, deps);
|
|
1997
|
+
await executeProviderRefresh({ kind: "provider.refresh", agent }, deps);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
deps.writeStdout(`manual step for ${agent}: ${action.command}`);
|
|
2001
|
+
}
|
|
2002
|
+
async function executeRepair(command, deps) {
|
|
2003
|
+
const agents = command.agent
|
|
2004
|
+
? [command.agent]
|
|
2005
|
+
: await listCliAgents(deps);
|
|
2006
|
+
const uniqueAgents = [...new Set(agents)];
|
|
2007
|
+
const lines = [];
|
|
2008
|
+
const repairDeps = {
|
|
2009
|
+
...deps,
|
|
2010
|
+
writeStdout: (text) => {
|
|
2011
|
+
lines.push(text);
|
|
2012
|
+
deps.writeStdout(text);
|
|
2013
|
+
},
|
|
2014
|
+
};
|
|
2015
|
+
if (uniqueAgents.length === 0) {
|
|
2016
|
+
const message = "no agents found to repair.";
|
|
2017
|
+
repairDeps.writeStdout(message);
|
|
2018
|
+
return message;
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
const reports = await Promise.all(uniqueAgents.map((agent) => readinessReportForAgent(agent, repairDeps)));
|
|
2022
|
+
for (const report of reports) {
|
|
2023
|
+
if (report.ok) {
|
|
2024
|
+
repairDeps.writeStdout(`${report.agent}: ready`);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
await (0, readiness_repair_1.runGuidedReadinessRepair)(reports, {
|
|
2028
|
+
promptInput: deps.promptInput,
|
|
2029
|
+
writeStdout: repairDeps.writeStdout,
|
|
2030
|
+
runRepairAction: async (agentName, action) => executeReadinessRepairAction(agentName, action, repairDeps),
|
|
2031
|
+
});
|
|
2032
|
+
return lines.join("\n");
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
function readinessReportsFromDegraded(degraded) {
|
|
2036
|
+
return degraded.map((entry) => ({
|
|
2037
|
+
agent: entry.agent,
|
|
2038
|
+
ok: false,
|
|
2039
|
+
issues: [readinessIssueFromDegraded(entry)],
|
|
2040
|
+
}));
|
|
2041
|
+
}
|
|
2042
|
+
function degradedEntrySignature(entry) {
|
|
2043
|
+
const issue = readinessIssueFromDegraded(entry);
|
|
2044
|
+
return JSON.stringify({
|
|
2045
|
+
agent: entry.agent,
|
|
2046
|
+
kind: issue.kind,
|
|
2047
|
+
summary: issue.summary,
|
|
2048
|
+
detail: issue.detail ?? "",
|
|
2049
|
+
actions: issue.actions.map((action) => `${action.kind}:${action.command}:${action.executable === false ? "manual" : "run"}`),
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
function mergeRemainingDegraded(untouched, rechecked) {
|
|
2053
|
+
const byAgent = new Map();
|
|
2054
|
+
for (const entry of untouched)
|
|
2055
|
+
byAgent.set(entry.agent, entry);
|
|
2056
|
+
for (const entry of rechecked)
|
|
2057
|
+
byAgent.set(entry.agent, entry);
|
|
2058
|
+
return [...byAgent.values()];
|
|
2059
|
+
}
|
|
2060
|
+
async function runReadinessRepairForDegraded(degraded, deps) {
|
|
2061
|
+
let current = degraded;
|
|
2062
|
+
let repairsAttempted = false;
|
|
2063
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
2064
|
+
const attemptedAgents = new Set();
|
|
2065
|
+
const result = await (0, readiness_repair_1.runGuidedReadinessRepair)(readinessReportsFromDegraded(current), {
|
|
2066
|
+
promptInput: deps.promptInput,
|
|
2067
|
+
writeStdout: deps.writeStdout,
|
|
2068
|
+
onActionAttempted: (agentName) => {
|
|
2069
|
+
attemptedAgents.add(agentName);
|
|
2070
|
+
},
|
|
2071
|
+
runRepairAction: async (agentName, action) => executeReadinessRepairAction(agentName, action, deps),
|
|
2072
|
+
});
|
|
2073
|
+
if (!result.repairsAttempted) {
|
|
2074
|
+
return { repairsAttempted, remainingDegraded: current };
|
|
2075
|
+
}
|
|
2076
|
+
repairsAttempted = true;
|
|
2077
|
+
/* v8 ignore next -- onActionAttempted runs before any runnable repair action, so repairsAttempted implies at least one attempted agent @preserve */
|
|
2078
|
+
if (attemptedAgents.size === 0) {
|
|
2079
|
+
return { repairsAttempted, remainingDegraded: current };
|
|
2080
|
+
}
|
|
2081
|
+
const previousByAgent = new Map();
|
|
2082
|
+
for (const entry of current) {
|
|
2083
|
+
if (attemptedAgents.has(entry.agent)) {
|
|
2084
|
+
previousByAgent.set(entry.agent, degradedEntrySignature(entry));
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
const untouched = current.filter((entry) => !attemptedAgents.has(entry.agent));
|
|
2088
|
+
const rechecked = await checkAgentProviders(deps, [...attemptedAgents]);
|
|
2089
|
+
const remaining = mergeRemainingDegraded(untouched, rechecked);
|
|
2090
|
+
if (remaining.length === 0) {
|
|
2091
|
+
return { repairsAttempted, remainingDegraded: remaining };
|
|
2092
|
+
}
|
|
2093
|
+
const nextPrompt = rechecked.filter((entry) => previousByAgent.get(entry.agent) !== degradedEntrySignature(entry));
|
|
2094
|
+
if (nextPrompt.length === 0) {
|
|
2095
|
+
return { repairsAttempted, remainingDegraded: remaining };
|
|
2096
|
+
}
|
|
2097
|
+
current = nextPrompt;
|
|
2098
|
+
}
|
|
2099
|
+
return { repairsAttempted, remainingDegraded: current };
|
|
2100
|
+
}
|
|
2101
|
+
async function executeLegacyAuthSwitch(command, deps) {
|
|
2102
|
+
const { state } = readOrBootstrapProviderState(command.agent, deps);
|
|
2103
|
+
const lanes = command.facing
|
|
2104
|
+
? [command.facing === "human" ? "outward" : "inner"]
|
|
2105
|
+
: ["outward", "inner"];
|
|
2106
|
+
const messages = [];
|
|
2107
|
+
for (const lane of lanes) {
|
|
2108
|
+
const model = state.lanes[lane].model;
|
|
2109
|
+
messages.push(await executeProviderUse({
|
|
2110
|
+
kind: "provider.use",
|
|
2111
|
+
agent: command.agent,
|
|
2112
|
+
lane,
|
|
2113
|
+
provider: command.provider,
|
|
2114
|
+
model,
|
|
2115
|
+
force: true,
|
|
2116
|
+
legacyFacing: command.facing,
|
|
2117
|
+
}, deps, { writeStdout: false }));
|
|
2118
|
+
}
|
|
2119
|
+
const message = [
|
|
2120
|
+
`deprecated: switched this machine's local provider binding. \`ouro auth switch\` no longer edits agent.json.`,
|
|
2121
|
+
...messages,
|
|
2122
|
+
`Use \`ouro use --agent ${command.agent} --lane <outward|inner> --provider ${command.provider} --model <model>\` for explicit provider/model selection.`,
|
|
2123
|
+
].join("\n");
|
|
2124
|
+
deps.writeStdout(message);
|
|
2125
|
+
return message;
|
|
2126
|
+
}
|
|
2127
|
+
async function executeLegacyConfigModel(command, deps) {
|
|
2128
|
+
const lane = command.facing === "agent" ? "inner" : "outward";
|
|
2129
|
+
const { agentRoot, state } = readOrBootstrapProviderState(command.agent, deps);
|
|
2130
|
+
const binding = state.lanes[lane];
|
|
2131
|
+
const progress = binding.provider === "github-copilot" ? createHumanCommandProgress(deps, "config model") : null;
|
|
2132
|
+
const writeMessage = (message) => {
|
|
2133
|
+
progress?.end();
|
|
2134
|
+
deps.writeStdout(message);
|
|
2135
|
+
return message;
|
|
2136
|
+
};
|
|
2137
|
+
if (binding.provider === "github-copilot") {
|
|
2138
|
+
try {
|
|
2139
|
+
progress?.startPhase("reading github-copilot credentials");
|
|
2140
|
+
const credential = await readProviderCredentialRecord(command.agent, "github-copilot", deps, {
|
|
2141
|
+
onProgress: (message) => progress?.updateDetail(message),
|
|
2142
|
+
});
|
|
2143
|
+
if (credential.ok) {
|
|
2144
|
+
const ghConfig = {
|
|
2145
|
+
...credential.record.config,
|
|
2146
|
+
...credential.record.credentials,
|
|
2147
|
+
};
|
|
2148
|
+
const githubToken = ghConfig.githubToken;
|
|
2149
|
+
const baseUrl = ghConfig.baseUrl;
|
|
2150
|
+
if (typeof githubToken === "string" && typeof baseUrl === "string") {
|
|
2151
|
+
progress?.completePhase("reading github-copilot credentials", "found");
|
|
2152
|
+
const fetchFn = deps.fetchImpl ?? fetch;
|
|
2153
|
+
try {
|
|
2154
|
+
progress?.startPhase("listing github-copilot models");
|
|
2155
|
+
const models = await listGithubCopilotModels(baseUrl, githubToken, fetchFn);
|
|
2156
|
+
const available = models.map((m) => m.id);
|
|
2157
|
+
progress?.completePhase("listing github-copilot models", `${available.length} model${available.length === 1 ? "" : "s"}`);
|
|
2158
|
+
if (available.length > 0 && !available.includes(command.modelName)) {
|
|
2159
|
+
const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
|
|
2160
|
+
return writeMessage(message);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
catch {
|
|
2164
|
+
progress?.completePhase("listing github-copilot models", "skipped");
|
|
2165
|
+
// Catalog validation failed; the live ping below gives the actionable result.
|
|
2166
|
+
}
|
|
2167
|
+
progress?.startPhase(`checking ${command.modelName}`);
|
|
2168
|
+
const pingResult = await (0, provider_ping_1.pingGithubCopilotModel)(baseUrl, githubToken, command.modelName, fetchFn);
|
|
2169
|
+
progress?.completePhase(`checking ${command.modelName}`, pingResult.ok ? "ok" : "failed");
|
|
2170
|
+
if (!pingResult.ok) {
|
|
2171
|
+
const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
|
|
2172
|
+
return writeMessage(message);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
else {
|
|
2176
|
+
progress?.completePhase("reading github-copilot credentials", "missing fields");
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
else {
|
|
2180
|
+
progress?.completePhase("reading github-copilot credentials", credential.reason);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
catch (error) {
|
|
2184
|
+
progress?.end();
|
|
2185
|
+
throw error;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const updatedAt = providerCliNow(deps).toISOString();
|
|
2189
|
+
state.updatedAt = updatedAt;
|
|
2190
|
+
state.lanes[lane] = {
|
|
2191
|
+
...binding,
|
|
2192
|
+
model: command.modelName,
|
|
2193
|
+
source: "local",
|
|
2194
|
+
updatedAt,
|
|
2195
|
+
};
|
|
2196
|
+
delete state.readiness[lane];
|
|
2197
|
+
(0, provider_state_1.writeProviderState)(agentRoot, state);
|
|
2198
|
+
const message = `deprecated: updated ${command.agent} model on ${lane}/${binding.provider}: ${binding.model} -> ${command.modelName}\nUse \`ouro use --agent ${command.agent} --lane ${lane} --provider ${binding.provider} --model ${command.modelName}\` next time.`;
|
|
2199
|
+
return writeMessage(message);
|
|
2200
|
+
}
|
|
2201
|
+
// ── System setup ──
|
|
2202
|
+
async function registerOuroBundleTypeNonBlocking(deps) {
|
|
2203
|
+
const registerOuroBundleType = deps.registerOuroBundleType;
|
|
2204
|
+
if (!registerOuroBundleType)
|
|
2205
|
+
return;
|
|
2206
|
+
try {
|
|
2207
|
+
await Promise.resolve(registerOuroBundleType());
|
|
2208
|
+
}
|
|
2209
|
+
catch (error) {
|
|
2210
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2211
|
+
level: "warn",
|
|
2212
|
+
component: "daemon",
|
|
2213
|
+
event: "daemon.ouro_uti_register_error",
|
|
2214
|
+
message: "failed .ouro UTI registration from CLI flow",
|
|
2215
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
async function performSystemSetup(deps) {
|
|
2220
|
+
// Install ouro command to PATH (non-blocking)
|
|
2221
|
+
if (deps.installOuroCommand) {
|
|
2222
|
+
try {
|
|
2223
|
+
const installResult = deps.installOuroCommand();
|
|
2224
|
+
/* v8 ignore next -- old-launcher repair hint: fires when stale ~/.local/bin/ouro is fixed @preserve */
|
|
2225
|
+
if (installResult.repairedOldLauncher) {
|
|
2226
|
+
deps.writeStdout("repaired stale ouro launcher at ~/.local/bin/ouro");
|
|
2227
|
+
}
|
|
2228
|
+
if (installResult.pathResolution?.status === "shadowed") {
|
|
2229
|
+
deps.writeStdout(`fix ouro PATH: ${installResult.pathResolution.detail}; ` +
|
|
2230
|
+
`fix: ${installResult.pathResolution.remediation}`);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
catch (error) {
|
|
2234
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2235
|
+
level: "warn",
|
|
2236
|
+
component: "daemon",
|
|
2237
|
+
event: "daemon.system_setup_ouro_cmd_error",
|
|
2238
|
+
message: "failed to install ouro command to PATH",
|
|
2239
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
// Self-healing: ensure current version is installed in ~/.ouro-cli/ layout.
|
|
2244
|
+
// Handles the case where the wrapper exists but CurrentVersion is missing
|
|
2245
|
+
// (e.g., first run after migration from old npx wrapper).
|
|
2246
|
+
if (deps.ensureCurrentVersionInstalled) {
|
|
2247
|
+
try {
|
|
2248
|
+
deps.ensureCurrentVersionInstalled();
|
|
2249
|
+
}
|
|
2250
|
+
catch (error) {
|
|
2251
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2252
|
+
level: "warn",
|
|
2253
|
+
component: "daemon",
|
|
2254
|
+
event: "daemon.system_setup_version_install_error",
|
|
2255
|
+
message: "failed to ensure current version installed",
|
|
2256
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error) },
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if (deps.syncGlobalOuroBotWrapper) {
|
|
2261
|
+
try {
|
|
2262
|
+
await Promise.resolve(deps.syncGlobalOuroBotWrapper());
|
|
2263
|
+
}
|
|
2264
|
+
catch (error) {
|
|
2265
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2266
|
+
level: "warn",
|
|
2267
|
+
component: "daemon",
|
|
2268
|
+
event: "daemon.system_setup_ouro_bot_wrapper_error",
|
|
2269
|
+
message: "failed to sync global ouro.bot wrapper",
|
|
2270
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
// Ensure skill-management skill is available
|
|
2275
|
+
if (deps.ensureSkillManagement) {
|
|
2276
|
+
try {
|
|
2277
|
+
await deps.ensureSkillManagement();
|
|
2278
|
+
/* v8 ignore start -- defensive: ensureSkillManagement handles its own errors internally @preserve */
|
|
2279
|
+
}
|
|
2280
|
+
catch (error) {
|
|
2281
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2282
|
+
level: "warn",
|
|
2283
|
+
component: "daemon",
|
|
2284
|
+
event: "daemon.system_setup_skill_management_error",
|
|
2285
|
+
message: "failed to ensure skill-management skill",
|
|
2286
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
/* v8 ignore stop */
|
|
2290
|
+
}
|
|
2291
|
+
// Register .ouro bundle type (UTI on macOS)
|
|
2292
|
+
await registerOuroBundleTypeNonBlocking(deps);
|
|
2293
|
+
}
|
|
2294
|
+
// ── Task command execution ──
|
|
2295
|
+
function executeTaskCommand(command, taskMod) {
|
|
2296
|
+
if (command.kind === "task.board") {
|
|
2297
|
+
if (command.status) {
|
|
2298
|
+
const lines = taskMod.boardStatus(command.status);
|
|
2299
|
+
return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
|
|
2300
|
+
}
|
|
2301
|
+
const board = taskMod.getBoard();
|
|
2302
|
+
return board.full || board.compact || "no tasks found";
|
|
2303
|
+
}
|
|
2304
|
+
if (command.kind === "task.create") {
|
|
2305
|
+
try {
|
|
2306
|
+
const created = taskMod.createTask({
|
|
2307
|
+
title: command.title,
|
|
2308
|
+
type: command.type ?? "one-shot",
|
|
2309
|
+
category: "general",
|
|
2310
|
+
body: "",
|
|
2311
|
+
});
|
|
2312
|
+
return `created: ${created}`;
|
|
2313
|
+
}
|
|
2314
|
+
catch (error) {
|
|
2315
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
if (command.kind === "task.update") {
|
|
2319
|
+
const result = taskMod.updateStatus(command.id, command.status);
|
|
2320
|
+
if (!result.ok) {
|
|
2321
|
+
return `error: ${result.reason ?? "status update failed"}`;
|
|
2322
|
+
}
|
|
2323
|
+
const archivedSuffix = result.archived && result.archived.length > 0
|
|
2324
|
+
? ` | archived: ${result.archived.join(", ")}`
|
|
2325
|
+
: "";
|
|
2326
|
+
return `updated: ${command.id} -> ${result.to}${archivedSuffix}`;
|
|
2327
|
+
}
|
|
2328
|
+
if (command.kind === "task.show") {
|
|
2329
|
+
const task = taskMod.getTask(command.id);
|
|
2330
|
+
if (!task)
|
|
2331
|
+
return `task not found: ${command.id}`;
|
|
2332
|
+
return [
|
|
2333
|
+
`title: ${task.title}`,
|
|
2334
|
+
`type: ${task.type}`,
|
|
2335
|
+
`status: ${task.status}`,
|
|
2336
|
+
`category: ${task.category}`,
|
|
2337
|
+
`created: ${task.created}`,
|
|
2338
|
+
`updated: ${task.updated}`,
|
|
2339
|
+
`path: ${task.path}`,
|
|
2340
|
+
task.body ? `\n${task.body}` : "",
|
|
2341
|
+
].filter(Boolean).join("\n");
|
|
2342
|
+
}
|
|
2343
|
+
if (command.kind === "task.actionable") {
|
|
2344
|
+
const lines = taskMod.boardAction();
|
|
2345
|
+
return lines.length > 0 ? lines.join("\n") : "no action required";
|
|
2346
|
+
}
|
|
2347
|
+
if (command.kind === "task.deps") {
|
|
2348
|
+
const lines = taskMod.boardDeps();
|
|
2349
|
+
return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
|
|
2350
|
+
}
|
|
2351
|
+
if (command.kind === "task.fix") {
|
|
2352
|
+
try {
|
|
2353
|
+
const fixOptions = {
|
|
2354
|
+
mode: command.mode,
|
|
2355
|
+
...(command.issueId ? { issueId: command.issueId } : {}),
|
|
2356
|
+
...(command.option !== undefined ? { option: command.option } : {}),
|
|
2357
|
+
};
|
|
2358
|
+
const result = taskMod.fix(fixOptions);
|
|
2359
|
+
if (command.mode === "dry-run") {
|
|
2360
|
+
if (result.remaining.length === 0) {
|
|
2361
|
+
return `task health: clean`;
|
|
2362
|
+
}
|
|
2363
|
+
const safeIssues = result.remaining.filter((i) => i.confidence === "safe");
|
|
2364
|
+
const reviewIssues = result.remaining.filter((i) => i.confidence === "needs_review");
|
|
2365
|
+
const lines = [`${result.remaining.length} issues found`];
|
|
2366
|
+
if (safeIssues.length > 0) {
|
|
2367
|
+
lines.push("", `safe fixes (${safeIssues.length}):`);
|
|
2368
|
+
for (const issue of safeIssues) {
|
|
2369
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
if (reviewIssues.length > 0) {
|
|
2373
|
+
lines.push("", `needs review (${reviewIssues.length}):`);
|
|
2374
|
+
for (const issue of reviewIssues) {
|
|
2375
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
lines.push("", `task health: ${result.health}`);
|
|
2379
|
+
return lines.join("\n");
|
|
2380
|
+
}
|
|
2381
|
+
// safe, single, or --all modes: show what was done
|
|
2382
|
+
const lines = [];
|
|
2383
|
+
if (result.applied.length > 0) {
|
|
2384
|
+
lines.push(`${result.applied.length} applied:`);
|
|
2385
|
+
for (const issue of result.applied) {
|
|
2386
|
+
lines.push(` ${issue.code}:${issue.target}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
if (result.remaining.length > 0) {
|
|
2390
|
+
lines.push(`${result.remaining.length} remaining:`);
|
|
2391
|
+
for (const issue of result.remaining) {
|
|
2392
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (result.applied.length === 0 && result.remaining.length === 0) {
|
|
2396
|
+
lines.push("no issues");
|
|
2397
|
+
}
|
|
2398
|
+
lines.push(`task health: ${result.health}`);
|
|
2399
|
+
return lines.join("\n");
|
|
2400
|
+
}
|
|
2401
|
+
catch (error) {
|
|
2402
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
// command.kind === "task.sessions"
|
|
2406
|
+
const lines = taskMod.boardSessions();
|
|
2407
|
+
return lines.length > 0 ? lines.join("\n") : "no active sessions";
|
|
2408
|
+
}
|
|
2409
|
+
// ── Friend command execution ──
|
|
2410
|
+
const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
|
|
2411
|
+
/* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
|
|
2412
|
+
function higherTrust(a, b) {
|
|
2413
|
+
const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
|
|
2414
|
+
const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
|
|
2415
|
+
return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
|
|
2416
|
+
}
|
|
2417
|
+
/* v8 ignore stop */
|
|
2418
|
+
async function executeFriendCommand(command, store) {
|
|
2419
|
+
if (command.kind === "friend.list") {
|
|
2420
|
+
const listAll = store.listAll;
|
|
2421
|
+
if (!listAll)
|
|
2422
|
+
return "friend store does not support listing";
|
|
2423
|
+
const friends = await listAll.call(store);
|
|
2424
|
+
if (friends.length === 0)
|
|
2425
|
+
return "no friends found";
|
|
2426
|
+
const lines = friends.map((f) => {
|
|
2427
|
+
const trust = f.trustLevel ?? "unknown";
|
|
2428
|
+
return `${f.id} ${f.name} ${trust}`;
|
|
2429
|
+
});
|
|
2430
|
+
return lines.join("\n");
|
|
2431
|
+
}
|
|
2432
|
+
if (command.kind === "friend.show") {
|
|
2433
|
+
const record = await store.get(command.friendId);
|
|
2434
|
+
if (!record)
|
|
2435
|
+
return `friend not found: ${command.friendId}`;
|
|
2436
|
+
return JSON.stringify(record, null, 2);
|
|
2437
|
+
}
|
|
2438
|
+
if (command.kind === "friend.create") {
|
|
2439
|
+
const now = new Date().toISOString();
|
|
2440
|
+
const id = (0, crypto_1.randomUUID)();
|
|
2441
|
+
const trustLevel = (command.trustLevel ?? "acquaintance");
|
|
2442
|
+
await store.put(id, {
|
|
2443
|
+
id,
|
|
2444
|
+
name: command.name,
|
|
2445
|
+
trustLevel,
|
|
2446
|
+
externalIds: [],
|
|
2447
|
+
tenantMemberships: [],
|
|
2448
|
+
toolPreferences: {},
|
|
2449
|
+
notes: {},
|
|
2450
|
+
totalTokens: 0,
|
|
2451
|
+
createdAt: now,
|
|
2452
|
+
updatedAt: now,
|
|
2453
|
+
schemaVersion: 1,
|
|
2454
|
+
});
|
|
2455
|
+
return `created: ${id} (${command.name}, ${trustLevel})`;
|
|
2456
|
+
}
|
|
2457
|
+
if (command.kind === "friend.update") {
|
|
2458
|
+
const current = await store.get(command.friendId);
|
|
2459
|
+
if (!current)
|
|
2460
|
+
return `friend not found: ${command.friendId}`;
|
|
2461
|
+
const now = new Date().toISOString();
|
|
2462
|
+
await store.put(command.friendId, {
|
|
2463
|
+
...current,
|
|
2464
|
+
trustLevel: command.trustLevel,
|
|
2465
|
+
role: command.trustLevel,
|
|
2466
|
+
updatedAt: now,
|
|
2467
|
+
});
|
|
2468
|
+
return `updated: ${command.friendId} → trust=${command.trustLevel}`;
|
|
2469
|
+
}
|
|
2470
|
+
if (command.kind === "friend.link") {
|
|
2471
|
+
const current = await store.get(command.friendId);
|
|
2472
|
+
if (!current)
|
|
2473
|
+
return `friend not found: ${command.friendId}`;
|
|
2474
|
+
const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
2475
|
+
if (alreadyLinked)
|
|
2476
|
+
return `identity already linked: ${command.provider}:${command.externalId}`;
|
|
2477
|
+
const now = new Date().toISOString();
|
|
2478
|
+
const newExternalIds = [
|
|
2479
|
+
...current.externalIds,
|
|
2480
|
+
{ provider: command.provider, externalId: command.externalId, linkedAt: now },
|
|
2481
|
+
];
|
|
2482
|
+
// Orphan cleanup: check if another friend has this externalId
|
|
2483
|
+
const orphan = await store.findByExternalId(command.provider, command.externalId);
|
|
2484
|
+
let mergeMessage = "";
|
|
2485
|
+
let mergedNotes = { ...current.notes };
|
|
2486
|
+
let mergedTrust = current.trustLevel;
|
|
2487
|
+
let orphanExternalIds = [];
|
|
2488
|
+
if (orphan && orphan.id !== command.friendId) {
|
|
2489
|
+
// Merge orphan's notes (target's notes take priority)
|
|
2490
|
+
mergedNotes = { ...orphan.notes, ...current.notes };
|
|
2491
|
+
// Keep higher trust level
|
|
2492
|
+
mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
|
|
2493
|
+
// Collect orphan's other externalIds (excluding the one being linked)
|
|
2494
|
+
orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
|
|
2495
|
+
await store.delete(orphan.id);
|
|
2496
|
+
mergeMessage = ` (merged orphan ${orphan.id})`;
|
|
2497
|
+
}
|
|
2498
|
+
await store.put(command.friendId, {
|
|
2499
|
+
...current,
|
|
2500
|
+
externalIds: [...newExternalIds, ...orphanExternalIds],
|
|
2501
|
+
notes: mergedNotes,
|
|
2502
|
+
trustLevel: mergedTrust,
|
|
2503
|
+
updatedAt: now,
|
|
2504
|
+
});
|
|
2505
|
+
return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
|
|
2506
|
+
}
|
|
2507
|
+
// command.kind === "friend.unlink"
|
|
2508
|
+
const current = await store.get(command.friendId);
|
|
2509
|
+
if (!current)
|
|
2510
|
+
return `friend not found: ${command.friendId}`;
|
|
2511
|
+
const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
2512
|
+
if (idx === -1)
|
|
2513
|
+
return `identity not linked: ${command.provider}:${command.externalId}`;
|
|
2514
|
+
const now = new Date().toISOString();
|
|
2515
|
+
const filtered = current.externalIds.filter((_, i) => i !== idx);
|
|
2516
|
+
await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
|
|
2517
|
+
return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
|
|
2518
|
+
}
|
|
2519
|
+
// ── Reminder command execution ──
|
|
2520
|
+
function executeReminderCommand(command, taskMod) {
|
|
2521
|
+
try {
|
|
2522
|
+
const created = taskMod.createTask({
|
|
2523
|
+
title: command.title,
|
|
2524
|
+
type: command.cadence ? "ongoing" : "one-shot",
|
|
2525
|
+
category: command.category ?? "reminder",
|
|
2526
|
+
body: command.body,
|
|
2527
|
+
scheduledAt: command.scheduledAt,
|
|
2528
|
+
cadence: command.cadence,
|
|
2529
|
+
requester: command.requester,
|
|
2530
|
+
});
|
|
2531
|
+
return `created: ${created}`;
|
|
2532
|
+
}
|
|
2533
|
+
catch (error) {
|
|
2534
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
// ── Dev mode helpers ──
|
|
2538
|
+
/* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
|
|
2539
|
+
function getDevConfigPath() {
|
|
2540
|
+
return path.join((0, ouro_version_manager_1.getOuroCliHome)(), "dev-config.json");
|
|
2541
|
+
}
|
|
2542
|
+
function readPersistedDevPath() {
|
|
2543
|
+
try {
|
|
2544
|
+
const raw = fs.readFileSync(getDevConfigPath(), "utf-8");
|
|
2545
|
+
const config = JSON.parse(raw);
|
|
2546
|
+
if (typeof config.repoPath === "string")
|
|
2547
|
+
return config.repoPath;
|
|
2548
|
+
}
|
|
2549
|
+
catch { /* no persisted path */ }
|
|
2550
|
+
return null;
|
|
2551
|
+
}
|
|
2552
|
+
function persistDevPath(repoPath) {
|
|
2553
|
+
const configPath = getDevConfigPath();
|
|
2554
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
2555
|
+
fs.writeFileSync(configPath, JSON.stringify({ repoPath }), "utf-8");
|
|
2556
|
+
}
|
|
2557
|
+
function resolveDevRepoCwd(command, checkExists, deps) {
|
|
2558
|
+
if (command.repoPath) {
|
|
2559
|
+
persistDevPath(command.repoPath);
|
|
2560
|
+
return command.repoPath;
|
|
2561
|
+
}
|
|
2562
|
+
if (deps.getRepoCwd)
|
|
2563
|
+
return deps.getRepoCwd();
|
|
2564
|
+
const persisted = readPersistedDevPath();
|
|
2565
|
+
if (persisted && checkExists(path.join(persisted, ".git"))) {
|
|
2566
|
+
deps.writeStdout(`using persisted dev repo: ${persisted}`);
|
|
2567
|
+
return persisted;
|
|
2568
|
+
}
|
|
2569
|
+
return (0, identity_1.getRepoRoot)();
|
|
2570
|
+
}
|
|
2571
|
+
function resolveClonePath(options, checkExists, deps) {
|
|
2572
|
+
const cloneTarget = options.clonePath ?? path.join(os.homedir(), "Projects", "ouroboros");
|
|
2573
|
+
if (checkExists(path.join(cloneTarget, ".git"))) {
|
|
2574
|
+
// Existing repo — pull latest and rebuild
|
|
2575
|
+
deps.writeStdout(`pulling latest in ${cloneTarget}...`);
|
|
2576
|
+
(0, child_process_1.execSync)("git pull", { cwd: cloneTarget, stdio: "inherit" });
|
|
2577
|
+
deps.writeStdout(`installing dependencies in ${cloneTarget}...`);
|
|
2578
|
+
(0, child_process_1.execSync)("npm install", { cwd: cloneTarget, stdio: "inherit" });
|
|
2579
|
+
persistDevPath(cloneTarget);
|
|
2580
|
+
return cloneTarget;
|
|
2581
|
+
}
|
|
2582
|
+
// Fresh clone
|
|
2583
|
+
deps.writeStdout(`cloning ouroboros to ${cloneTarget}...`);
|
|
2584
|
+
const HARNESS_CANONICAL_REPO_URL = "https://github.com/ouroborosbot/ouroboros.git";
|
|
2585
|
+
fs.mkdirSync(path.dirname(cloneTarget), { recursive: true });
|
|
2586
|
+
(0, child_process_1.execSync)(`git clone ${HARNESS_CANONICAL_REPO_URL} "${cloneTarget}"`, { stdio: "inherit" });
|
|
2587
|
+
deps.writeStdout(`installing dependencies in ${cloneTarget}...`);
|
|
2588
|
+
(0, child_process_1.execSync)("npm install", { cwd: cloneTarget, stdio: "inherit" });
|
|
2589
|
+
deps.writeStdout(`building in ${cloneTarget}...`);
|
|
2590
|
+
try {
|
|
2591
|
+
(0, child_process_1.execSync)("npm run build", { cwd: cloneTarget, stdio: "inherit" });
|
|
2592
|
+
}
|
|
2593
|
+
catch {
|
|
2594
|
+
throw new Error(`build failed in ${cloneTarget}. check the output above.`);
|
|
2595
|
+
}
|
|
2596
|
+
return cloneTarget;
|
|
2597
|
+
}
|
|
2598
|
+
/* v8 ignore stop */
|
|
2599
|
+
// ── Main CLI execution ──
|
|
2600
|
+
async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDeps)()) {
|
|
2601
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
2602
|
+
const text = (0, cli_help_1.getGroupedHelp)();
|
|
2603
|
+
deps.writeStdout(text);
|
|
2604
|
+
return text;
|
|
2605
|
+
}
|
|
2606
|
+
if (args.length === 1 && (args[0] === "-v" || args[0] === "--version")) {
|
|
2607
|
+
const text = (0, cli_render_1.formatVersionOutput)();
|
|
2608
|
+
deps.writeStdout(text);
|
|
2609
|
+
return text;
|
|
2610
|
+
}
|
|
2611
|
+
let command;
|
|
2612
|
+
try {
|
|
2613
|
+
command = (0, cli_parse_1.parseOuroCommand)(args);
|
|
2614
|
+
}
|
|
2615
|
+
catch (parseError) {
|
|
2616
|
+
if (deps.startChat && deps.listDiscoveredAgents && args.length === 1) {
|
|
2617
|
+
const discovered = await Promise.resolve(deps.listDiscoveredAgents());
|
|
2618
|
+
if (discovered.includes(args[0])) {
|
|
2619
|
+
await ensureDaemonRunning(deps);
|
|
2620
|
+
await deps.startChat(args[0]);
|
|
2621
|
+
return "";
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
throw parseError;
|
|
2625
|
+
}
|
|
2626
|
+
if (args.length === 0) {
|
|
2627
|
+
const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : (0, cli_defaults_1.defaultListDiscoveredAgents)());
|
|
2628
|
+
if (discovered.length === 0 && deps.runSerpentGuide) {
|
|
2629
|
+
// Hatch-or-clone choice when promptInput is available
|
|
2630
|
+
if (deps.promptInput) {
|
|
2631
|
+
const choice = await deps.promptInput("No agents found. Would you like to hatch a new agent or clone an existing one? (hatch/clone): ");
|
|
2632
|
+
if (choice.trim().toLowerCase() === "clone") {
|
|
2633
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2634
|
+
component: "daemon",
|
|
2635
|
+
event: "daemon.first_run_choice_clone",
|
|
2636
|
+
message: "user chose clone in first-run flow",
|
|
2637
|
+
meta: {},
|
|
2638
|
+
});
|
|
2639
|
+
/* v8 ignore next -- ?? fallback: promptInput always returns string in practice @preserve */
|
|
2640
|
+
const remote = (await deps.promptInput("Enter the git remote URL for the agent bundle: "))?.trim() ?? "";
|
|
2641
|
+
if (!remote) {
|
|
2642
|
+
deps.writeStdout("no remote URL provided — skipping clone");
|
|
2643
|
+
// Fall through to hatch flow
|
|
2644
|
+
}
|
|
2645
|
+
else {
|
|
2646
|
+
return await runOuroCli(["clone", remote], deps);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2650
|
+
component: "daemon",
|
|
2651
|
+
event: "daemon.first_run_choice_hatch",
|
|
2652
|
+
message: "user chose hatch in first-run flow",
|
|
2653
|
+
meta: {},
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
// System setup first — ouro command, subagents, UTI — before the interactive specialist
|
|
2657
|
+
await performSystemSetup(deps);
|
|
2658
|
+
const hatchlingName = await deps.runSerpentGuide();
|
|
2659
|
+
if (!hatchlingName) {
|
|
2660
|
+
return "";
|
|
2661
|
+
}
|
|
2662
|
+
await ensureDaemonRunning(deps);
|
|
2663
|
+
if (deps.startChat) {
|
|
2664
|
+
await deps.startChat(hatchlingName);
|
|
2665
|
+
}
|
|
2666
|
+
return "";
|
|
2667
|
+
}
|
|
2668
|
+
else if (discovered.length === 0) {
|
|
2669
|
+
command = { kind: "hatch.start" };
|
|
2670
|
+
}
|
|
2671
|
+
else if (discovered.length === 1) {
|
|
2672
|
+
if (deps.startChat) {
|
|
2673
|
+
await ensureDaemonRunning(deps);
|
|
2674
|
+
const health = await checkProviderHealthBeforeChat(discovered[0], deps);
|
|
2675
|
+
if (!health.ok)
|
|
2676
|
+
return health.output;
|
|
2677
|
+
await deps.startChat(discovered[0]);
|
|
2678
|
+
return "";
|
|
2679
|
+
}
|
|
2680
|
+
command = { kind: "chat.connect", agent: discovered[0] };
|
|
2681
|
+
}
|
|
2682
|
+
else {
|
|
2683
|
+
if (deps.startChat && deps.promptInput) {
|
|
2684
|
+
const prompt = `who do you want to talk to?\n${discovered.map((a, i) => `${i + 1}. ${a}`).join("\n")}\n`;
|
|
2685
|
+
const answer = await deps.promptInput(prompt);
|
|
2686
|
+
const selected = discovered.includes(answer) ? answer : discovered[parseInt(answer, 10) - 1];
|
|
2687
|
+
if (!selected)
|
|
2688
|
+
throw new Error("Invalid selection");
|
|
2689
|
+
await ensureDaemonRunning(deps);
|
|
2690
|
+
const health = await checkProviderHealthBeforeChat(selected, deps);
|
|
2691
|
+
if (!health.ok)
|
|
2692
|
+
return health.output;
|
|
2693
|
+
await deps.startChat(selected);
|
|
2694
|
+
return "";
|
|
2695
|
+
}
|
|
2696
|
+
const message = `who do you want to talk to? ${discovered.join(", ")} (use: ouro chat <agent>)`;
|
|
2697
|
+
deps.writeStdout(message);
|
|
2698
|
+
return message;
|
|
2699
|
+
}
|
|
2700
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2701
|
+
component: "daemon",
|
|
2702
|
+
event: "daemon.cli_auto_route",
|
|
2703
|
+
message: "routed bare ouro command from discovered agents",
|
|
2704
|
+
meta: { target: command.kind, count: discovered.length },
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2708
|
+
component: "daemon",
|
|
2709
|
+
event: "daemon.cli_command",
|
|
2710
|
+
message: "ouro CLI command invoked",
|
|
2711
|
+
meta: { kind: command.kind },
|
|
2712
|
+
});
|
|
2713
|
+
if (command.kind === "help") {
|
|
2714
|
+
const text = command.command
|
|
2715
|
+
? ((0, cli_help_1.getCommandHelp)(command.command) ?? `Unknown command: ${command.command}\n\n${(0, cli_help_1.getGroupedHelp)()}`)
|
|
2716
|
+
: (0, cli_help_1.getGroupedHelp)();
|
|
2717
|
+
deps.writeStdout(text);
|
|
2718
|
+
return text;
|
|
2719
|
+
}
|
|
2720
|
+
if (command.kind === "bluebubbles.replay") {
|
|
2721
|
+
const { replayBlueBubblesMessage, formatBlueBubblesReplayText } = await Promise.resolve().then(() => __importStar(require("../../senses/bluebubbles/replay")));
|
|
2722
|
+
const replay = await replayBlueBubblesMessage({
|
|
2723
|
+
agentName: command.agent,
|
|
2724
|
+
messageGuid: command.messageGuid,
|
|
2725
|
+
eventType: command.eventType,
|
|
2726
|
+
});
|
|
2727
|
+
const text = command.json
|
|
2728
|
+
? JSON.stringify(replay, null, 2)
|
|
2729
|
+
: formatBlueBubblesReplayText(replay);
|
|
2730
|
+
deps.writeStdout(text);
|
|
2731
|
+
return text;
|
|
2732
|
+
}
|
|
2733
|
+
if (command.kind === "connect") {
|
|
2734
|
+
return executeConnect(command, deps);
|
|
2735
|
+
}
|
|
2736
|
+
if (command.kind === "daemon.up") {
|
|
2737
|
+
// ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
|
|
2738
|
+
/* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
|
|
2739
|
+
try {
|
|
2740
|
+
const devConfigPath = getDevConfigPath();
|
|
2741
|
+
if (fs.existsSync(devConfigPath)) {
|
|
2742
|
+
fs.unlinkSync(devConfigPath);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
catch { /* best effort */ }
|
|
2746
|
+
/* v8 ignore stop */
|
|
2747
|
+
// ── dev mode delegation: ouro up from a dev repo delegates to installed binary ──
|
|
2748
|
+
// Only runs when detectMode is explicitly injected (via createDefaultOuroCliDeps or tests)
|
|
2749
|
+
if (deps.detectMode) {
|
|
2750
|
+
const runtimeMode = deps.detectMode();
|
|
2751
|
+
if (runtimeMode === "dev") {
|
|
2752
|
+
/* v8 ignore next -- defensive: getInstalledBinaryPath always injected in tests @preserve */
|
|
2753
|
+
const installedBinary = deps.getInstalledBinaryPath ? deps.getInstalledBinaryPath() : null;
|
|
2754
|
+
if (installedBinary) {
|
|
2755
|
+
deps.writeStdout("delegating to installed ouro...");
|
|
2756
|
+
/* v8 ignore next 3 -- defensive: execInstalledBinary always injected; missing branch unreachable @preserve */
|
|
2757
|
+
if (deps.execInstalledBinary) {
|
|
2758
|
+
deps.execInstalledBinary(installedBinary, args);
|
|
2759
|
+
}
|
|
2760
|
+
/* v8 ignore next 2 -- unreachable after exec replaces process @preserve */
|
|
2761
|
+
return "";
|
|
2762
|
+
}
|
|
2763
|
+
const message = "no installed version found. run: npx ouro.bot";
|
|
2764
|
+
deps.writeStdout(message);
|
|
2765
|
+
return message;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
|
|
2769
|
+
const outputIsTTY = deps.isTTY ?? process.stdout.isTTY === true;
|
|
2770
|
+
const progress = new up_progress_1.UpProgress({
|
|
2771
|
+
write: deps.writeRaw ?? deps.writeStdout,
|
|
2772
|
+
isTTY: outputIsTTY,
|
|
2773
|
+
now: deps.now ?? (() => Date.now()),
|
|
2774
|
+
autoRender: true,
|
|
2775
|
+
});
|
|
2776
|
+
// ── versioned CLI update check ──
|
|
2777
|
+
if (deps.checkForCliUpdate) {
|
|
2778
|
+
progress.startPhase("update check");
|
|
2779
|
+
let pendingReExec = false;
|
|
2780
|
+
try {
|
|
2781
|
+
const updateResult = await deps.checkForCliUpdate();
|
|
2782
|
+
if (updateResult.available && updateResult.latestVersion) {
|
|
2783
|
+
/* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
|
|
2784
|
+
const currentVersion = linkedVersionBeforeUp ?? "unknown";
|
|
2785
|
+
await deps.installCliVersion(updateResult.latestVersion);
|
|
2786
|
+
deps.activateCliVersion(updateResult.latestVersion);
|
|
2787
|
+
progress.completePhase("update check", `installed ${updateResult.latestVersion}`);
|
|
2788
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
|
|
2789
|
+
/* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
|
|
2790
|
+
if (changelogCommand) {
|
|
2791
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2792
|
+
}
|
|
2793
|
+
pendingReExec = true;
|
|
2794
|
+
}
|
|
2795
|
+
/* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
|
|
2796
|
+
}
|
|
2797
|
+
catch (error) {
|
|
2798
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2799
|
+
level: "warn",
|
|
2800
|
+
component: "daemon",
|
|
2801
|
+
event: "daemon.cli_update_check_error",
|
|
2802
|
+
message: "CLI update check failed",
|
|
2803
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
/* v8 ignore stop */
|
|
2807
|
+
if (pendingReExec) {
|
|
2808
|
+
progress.end();
|
|
2809
|
+
deps.reExecFromNewVersion(args);
|
|
2810
|
+
}
|
|
2811
|
+
else {
|
|
2812
|
+
progress.completePhase("update check", "up to date");
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
progress.startPhase("system setup");
|
|
2816
|
+
await performSystemSetup(deps);
|
|
2817
|
+
progress.completePhase("system setup");
|
|
2818
|
+
// Track whether we've already printed the "ouro updated to" message
|
|
2819
|
+
// this turn so the bundle-meta-fallback path below doesn't double-print.
|
|
2820
|
+
// There are three independent paths that can detect "the binary just
|
|
2821
|
+
// got newer":
|
|
2822
|
+
// 1. checkForCliUpdate found a newer version on npm (above) — this
|
|
2823
|
+
// path always re-execs, so the duplicate-detect runs in a
|
|
2824
|
+
// different process and the in-process tracker doesn't apply.
|
|
2825
|
+
// 2. ensureCurrentVersionInstalled (called from performSystemSetup)
|
|
2826
|
+
// flipped the CurrentVersion symlink because the running package
|
|
2827
|
+
// version is newer than what the symlink pointed at. This is the
|
|
2828
|
+
// common npx path.
|
|
2829
|
+
// 3. bundle-meta.json's stored runtime version differs from the
|
|
2830
|
+
// running version (fallback for when path 2 couldn't activate
|
|
2831
|
+
// but the binary is still newer).
|
|
2832
|
+
//
|
|
2833
|
+
// Verified live on 2026-04-08: npx download triggered both path 2 and
|
|
2834
|
+
// path 3 in the same process and the user saw the message printed twice.
|
|
2835
|
+
// Path 3's existing `linkedVersionBeforeUp !== currentVersion` guard
|
|
2836
|
+
// catches the cross-process re-exec case (path 1) but not the
|
|
2837
|
+
// in-process double-fire case (path 2 + path 3 in the same process).
|
|
2838
|
+
let printedUpdateMessage = false;
|
|
2839
|
+
const linkedVersionAfterSetup = deps.getCurrentCliVersion?.() ?? null;
|
|
2840
|
+
const runtimeVersion = (0, bundle_manifest_1.getPackageVersion)();
|
|
2841
|
+
if (linkedVersionBeforeUp && linkedVersionBeforeUp !== runtimeVersion && linkedVersionAfterSetup === runtimeVersion) {
|
|
2842
|
+
deps.writeStdout(`ouro updated to ${runtimeVersion} (was ${linkedVersionBeforeUp})`);
|
|
2843
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(linkedVersionBeforeUp, runtimeVersion);
|
|
2844
|
+
if (changelogCommand) {
|
|
2845
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2846
|
+
}
|
|
2847
|
+
printedUpdateMessage = true;
|
|
2848
|
+
}
|
|
2849
|
+
// Run update hooks before starting daemon so user sees the output
|
|
2850
|
+
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
2851
|
+
(0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
|
|
2852
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
2853
|
+
const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
|
|
2854
|
+
// Snapshot the previous CLI version from the first bundle-meta before
|
|
2855
|
+
// hooks overwrite it. This detects when npx downloaded a newer CLI.
|
|
2856
|
+
const previousCliVersion = (0, cli_defaults_1.readFirstBundleMetaVersion)(bundlesRoot);
|
|
2857
|
+
const updateSummary = await (0, update_hooks_1.applyPendingUpdates)(bundlesRoot, currentVersion);
|
|
2858
|
+
// Notify about CLI binary update (npx downloaded a new version).
|
|
2859
|
+
// Skip when the symlink already points to the running version — that
|
|
2860
|
+
// means path 1 (checkForCliUpdate + reExecFromNewVersion) already
|
|
2861
|
+
// printed the update message before re-exec. Also skip when path 2
|
|
2862
|
+
// (the symlink-flip detector above) already printed in this same
|
|
2863
|
+
// process — otherwise npx invocations print the message twice.
|
|
2864
|
+
/* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
|
|
2865
|
+
if (!printedUpdateMessage
|
|
2866
|
+
&& previousCliVersion
|
|
2867
|
+
&& previousCliVersion !== currentVersion
|
|
2868
|
+
&& linkedVersionBeforeUp !== currentVersion) {
|
|
2869
|
+
deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
|
|
2870
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(previousCliVersion, currentVersion);
|
|
2871
|
+
/* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
|
|
2872
|
+
if (changelogCommand) {
|
|
2873
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
/* v8 ignore stop */
|
|
2877
|
+
if (updateSummary.updated.length > 0) {
|
|
2878
|
+
const agents = updateSummary.updated.map((e) => e.agent);
|
|
2879
|
+
const from = updateSummary.updated[0].from;
|
|
2880
|
+
const to = updateSummary.updated[0].to;
|
|
2881
|
+
const fromStr = from ? ` (was ${from})` : "";
|
|
2882
|
+
const count = agents.length;
|
|
2883
|
+
progress.startPhase("agent updates");
|
|
2884
|
+
progress.completePhase("agent updates", `${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
|
|
2885
|
+
}
|
|
2886
|
+
// ── stale bundle pruning ──
|
|
2887
|
+
const prunedBundles = (0, stale_bundle_prune_1.pruneStaleEphemeralBundles)({ bundlesRoot: deps.bundlesRoot });
|
|
2888
|
+
if (prunedBundles.length > 0) {
|
|
2889
|
+
progress.startPhase("bundle cleanup");
|
|
2890
|
+
progress.completePhase("bundle cleanup", `pruned ${prunedBundles.length} stale bundle${prunedBundles.length === 1 ? "" : "s"}`);
|
|
2891
|
+
}
|
|
2892
|
+
// ── manual-clone detection: offer to enable sync for manually cloned bundles ──
|
|
2893
|
+
await checkManualCloneBundles({
|
|
2894
|
+
bundlesRoot: deps.bundlesRoot ?? bundlesRoot,
|
|
2895
|
+
promptInput: deps.promptInput,
|
|
2896
|
+
});
|
|
2897
|
+
const daemonAliveBeforeStart = await deps.checkSocketAlive(deps.socketPath);
|
|
2898
|
+
let providerChecksAlreadyRun = false;
|
|
2899
|
+
if (!daemonAliveBeforeStart) {
|
|
2900
|
+
progress.startPhase("provider checks");
|
|
2901
|
+
const preflightProviderDegraded = await checkAgentProviders(deps, undefined, (msg) => progress.updateDetail(msg));
|
|
2902
|
+
providerChecksAlreadyRun = true;
|
|
2903
|
+
progress.completePhase("provider checks", providerRepairCountSummary(preflightProviderDegraded.length));
|
|
2904
|
+
if (preflightProviderDegraded.length > 0) {
|
|
2905
|
+
progress.end();
|
|
2906
|
+
if (command.noRepair) {
|
|
2907
|
+
writeProviderRepairSummary(deps, "Provider checks need attention", preflightProviderDegraded);
|
|
2908
|
+
const message = "daemon not started: provider checks need repair. Run `ouro repair` or rerun `ouro up` to choose a repair path.";
|
|
2909
|
+
deps.writeStdout(message);
|
|
2910
|
+
return message;
|
|
2911
|
+
}
|
|
2912
|
+
const repairResult = await runReadinessRepairForDegraded(preflightProviderDegraded, deps);
|
|
2913
|
+
if (!repairResult.repairsAttempted) {
|
|
2914
|
+
writeProviderRepairSummary(deps, "Provider checks still need attention", repairResult.remainingDegraded);
|
|
2915
|
+
const message = "daemon not started: provider checks need repair. Run `ouro repair` or rerun `ouro up` to choose a repair path.";
|
|
2916
|
+
deps.writeStdout(message);
|
|
2917
|
+
return message;
|
|
2918
|
+
}
|
|
2919
|
+
const remainingDegraded = repairResult.remainingDegraded;
|
|
2920
|
+
if (remainingDegraded.length > 0) {
|
|
2921
|
+
writeProviderRepairSummary(deps, "Still needs attention", remainingDegraded);
|
|
2922
|
+
const message = "daemon not started: provider checks still need repair.";
|
|
2923
|
+
deps.writeStdout(message);
|
|
2924
|
+
return message;
|
|
2925
|
+
}
|
|
2926
|
+
deps.writeStdout("All set. Provider checks recovered after repair.");
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
progress.startPhase("starting daemon");
|
|
2930
|
+
const daemonResult = await ensureDaemonRunning({
|
|
2931
|
+
...deps,
|
|
2932
|
+
reportDaemonStartupPhase: (label) => {
|
|
2933
|
+
;
|
|
2934
|
+
progress.announceStep?.(label);
|
|
2935
|
+
},
|
|
2936
|
+
}, { initialAlive: daemonAliveBeforeStart });
|
|
2937
|
+
progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
|
|
2938
|
+
if (daemonResult.verifyStartupStatus === false) {
|
|
2939
|
+
progress.end();
|
|
2940
|
+
deps.writeStdout(daemonResult.message);
|
|
2941
|
+
return daemonResult.message;
|
|
2942
|
+
}
|
|
2943
|
+
if (!providerChecksAlreadyRun || daemonResult.alreadyRunning) {
|
|
2944
|
+
progress.startPhase("provider checks");
|
|
2945
|
+
const providerDegraded = await checkAlreadyRunningAgentProviders(deps, (msg) => progress.updateDetail(msg));
|
|
2946
|
+
daemonResult.stability = mergeStartupStability(daemonResult.stability, providerDegraded);
|
|
2947
|
+
progress.completePhase("provider checks", providerRepairCountSummary(providerDegraded.length));
|
|
2948
|
+
}
|
|
2949
|
+
progress.end();
|
|
2950
|
+
// Interactive repair for degraded agents (Unit 5) — skipped by --no-repair (Unit 6)
|
|
2951
|
+
if (daemonResult.stability?.degraded && daemonResult.stability.degraded.length > 0) {
|
|
2952
|
+
if (command.noRepair) {
|
|
2953
|
+
// --no-repair: write degraded summary and skip interactive repair
|
|
2954
|
+
writeProviderRepairSummary(deps, "Provider checks need attention", daemonResult.stability.degraded);
|
|
2955
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2956
|
+
level: "warn",
|
|
2957
|
+
component: "daemon",
|
|
2958
|
+
event: "daemon.no_repair_degraded_summary",
|
|
2959
|
+
message: "degraded agents detected with --no-repair, skipping interactive repair",
|
|
2960
|
+
meta: { degradedCount: daemonResult.stability.degraded.length },
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
else {
|
|
2964
|
+
const repairResult = await (0, agentic_repair_1.runAgenticRepair)(daemonResult.stability.degraded, {
|
|
2965
|
+
/* v8 ignore start -- production provider discovery wiring @preserve */
|
|
2966
|
+
discoverWorkingProvider: async (agentName) => {
|
|
2967
|
+
const { discoverWorkingProvider: discover } = await Promise.resolve().then(() => __importStar(require("./provider-discovery")));
|
|
2968
|
+
const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../provider-ping")));
|
|
2969
|
+
return discover({
|
|
2970
|
+
agentName,
|
|
2971
|
+
pingProvider: pingProvider,
|
|
2972
|
+
});
|
|
2973
|
+
},
|
|
2974
|
+
createProviderRuntime: agentic_repair_1.createAgenticDiagnosisProviderRuntime,
|
|
2975
|
+
readDaemonLogsTail: () => {
|
|
2976
|
+
try {
|
|
2977
|
+
const fs = require("node:fs");
|
|
2978
|
+
const path = require("node:path");
|
|
2979
|
+
const logsDir = path.join(process.env["HOME"] ?? "", ".agentstate", "daemon", "logs");
|
|
2980
|
+
const files = fs.readdirSync(logsDir).filter((f) => f.endsWith(".log")).sort();
|
|
2981
|
+
if (files.length === 0)
|
|
2982
|
+
return "(no daemon logs found)";
|
|
2983
|
+
const lastLog = fs.readFileSync(path.join(logsDir, files[files.length - 1]), "utf8");
|
|
2984
|
+
const lines = lastLog.split("\n");
|
|
2985
|
+
return lines.slice(-50).join("\n");
|
|
2986
|
+
}
|
|
2987
|
+
catch {
|
|
2988
|
+
return "(unable to read daemon logs)";
|
|
2989
|
+
}
|
|
2990
|
+
},
|
|
2991
|
+
/* v8 ignore stop */
|
|
2992
|
+
runInteractiveRepair: interactive_repair_1.runInteractiveRepair,
|
|
2993
|
+
promptInput: deps.promptInput ?? (async () => "n"),
|
|
2994
|
+
writeStdout: deps.writeStdout,
|
|
2995
|
+
runAuthFlow: async (agent, providerOverride) => {
|
|
2996
|
+
await executeAuthRun({
|
|
2997
|
+
kind: "auth.run",
|
|
2998
|
+
agent,
|
|
2999
|
+
...(providerOverride ? { provider: providerOverride } : {}),
|
|
3000
|
+
}, deps);
|
|
3001
|
+
},
|
|
3002
|
+
runVaultUnlock: async (agent) => {
|
|
3003
|
+
await executeVaultUnlock({ kind: "vault.unlock", agent }, deps);
|
|
3004
|
+
},
|
|
3005
|
+
skipQueueSummary: true,
|
|
3006
|
+
});
|
|
3007
|
+
if (repairResult.repairsAttempted) {
|
|
3008
|
+
const repairedAgents = daemonResult.stability.degraded
|
|
3009
|
+
.filter(interactive_repair_1.hasRunnableInteractiveRepair)
|
|
3010
|
+
.map((entry) => entry.agent);
|
|
3011
|
+
progress.startPhase("post-repair check");
|
|
3012
|
+
await reportPostRepairProviderHealth(deps, repairedAgents, (msg) => progress.updateDetail(msg));
|
|
3013
|
+
progress.completePhase("post-repair check", providerRepairCountSummary(repairedAgents.length));
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
// Persist boot startup AFTER daemon is running — bootstrap is safe now
|
|
3018
|
+
// because the daemon socket exists, so launchd's KeepAlive registers
|
|
3019
|
+
// for crash recovery without starting a competing process.
|
|
3020
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
3021
|
+
try {
|
|
3022
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
3023
|
+
}
|
|
3024
|
+
catch (error) {
|
|
3025
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3026
|
+
level: "warn",
|
|
3027
|
+
component: "daemon",
|
|
3028
|
+
event: "daemon.system_setup_launchd_error",
|
|
3029
|
+
message: "failed to persist daemon boot startup",
|
|
3030
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
return daemonResult.message;
|
|
3035
|
+
}
|
|
3036
|
+
if (command.kind === "daemon.dev") {
|
|
3037
|
+
/* v8 ignore next -- defensive: existsSync always injected in tests @preserve */
|
|
3038
|
+
const checkExists = deps.existsSync ?? fs.existsSync;
|
|
3039
|
+
/* v8 ignore next -- repo resolution dispatched to v8-ignored helper @preserve */
|
|
3040
|
+
let repoCwd = resolveDevRepoCwd(command, checkExists, deps);
|
|
3041
|
+
let entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
3042
|
+
if (!checkExists(entryPath) || !checkExists(path.join(repoCwd, ".git"))) {
|
|
3043
|
+
if (command.repoPath) {
|
|
3044
|
+
// Explicit --repo-path didn't have a valid repo — error
|
|
3045
|
+
const message = `no harness repo found at ${repoCwd}. run npm run build first.`;
|
|
3046
|
+
deps.writeStdout(message);
|
|
3047
|
+
return message;
|
|
3048
|
+
}
|
|
3049
|
+
/* v8 ignore start -- auto-clone: interactive prompt + existing repo discovery + real git/npm @preserve */
|
|
3050
|
+
const defaultClonePath = path.join(os.homedir(), "Projects", "ouroboros");
|
|
3051
|
+
if (checkExists(path.join(defaultClonePath, ".git"))) {
|
|
3052
|
+
deps.writeStdout(`found existing repo at ${defaultClonePath}`);
|
|
3053
|
+
try {
|
|
3054
|
+
repoCwd = resolveClonePath({ clonePath: defaultClonePath }, checkExists, deps);
|
|
3055
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
3056
|
+
}
|
|
3057
|
+
catch (err) {
|
|
3058
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3059
|
+
deps.writeStdout(message);
|
|
3060
|
+
return message;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
else if (deps.promptInput) {
|
|
3064
|
+
deps.writeStdout("no harness repo found.");
|
|
3065
|
+
const answer = await deps.promptInput(`already have a checkout? enter its path, or press enter to clone to ${defaultClonePath}: `);
|
|
3066
|
+
const cloneTarget = answer.trim() || defaultClonePath;
|
|
3067
|
+
try {
|
|
3068
|
+
repoCwd = resolveClonePath({ clonePath: cloneTarget }, checkExists, deps);
|
|
3069
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
3070
|
+
}
|
|
3071
|
+
catch (err) {
|
|
3072
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3073
|
+
deps.writeStdout(message);
|
|
3074
|
+
return message;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
else {
|
|
3078
|
+
const message = `no harness repo found. run: ouro dev --repo-path /path/to/ouroboros`;
|
|
3079
|
+
deps.writeStdout(message);
|
|
3080
|
+
return message;
|
|
3081
|
+
}
|
|
3082
|
+
/* v8 ignore stop */
|
|
3083
|
+
}
|
|
3084
|
+
// Auto-build: always rebuild in dev mode so dist/ matches source
|
|
3085
|
+
/* v8 ignore start -- dev auto-build: execSync in repo cwd, tested manually @preserve */
|
|
3086
|
+
deps.writeStdout(`building from ${repoCwd}...`);
|
|
3087
|
+
try {
|
|
3088
|
+
(0, child_process_1.execSync)("npm run build", { cwd: repoCwd, stdio: "inherit" });
|
|
3089
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
3090
|
+
}
|
|
3091
|
+
catch {
|
|
3092
|
+
const message = `build failed in ${repoCwd}. fix compilation errors and retry.`;
|
|
3093
|
+
deps.writeStdout(message);
|
|
3094
|
+
return message;
|
|
3095
|
+
}
|
|
3096
|
+
/* v8 ignore stop */
|
|
3097
|
+
/* v8 ignore start -- defensive: ensureDaemonBootPersistence always injected in tests @preserve */
|
|
3098
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
3099
|
+
try {
|
|
3100
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
3101
|
+
/* v8 ignore next -- defensive: boot persistence error should not block dev mode @preserve */
|
|
3102
|
+
}
|
|
3103
|
+
catch (error) {
|
|
3104
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3105
|
+
level: "warn",
|
|
3106
|
+
component: "daemon",
|
|
3107
|
+
event: "daemon.dev_boot_persistence_error",
|
|
3108
|
+
message: "failed to persist daemon boot startup in dev mode",
|
|
3109
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
/* v8 ignore stop */
|
|
3113
|
+
}
|
|
3114
|
+
// Disable launchd KeepAlive before killing — prevents the installed daemon from respawning
|
|
3115
|
+
/* v8 ignore start -- dev launchd disable: requires real launchctl + plist on disk @preserve */
|
|
3116
|
+
const launchdDevDeps = {
|
|
3117
|
+
exec: (cmd) => { (0, child_process_1.execSync)(cmd); },
|
|
3118
|
+
existsFile: (p) => fs.existsSync(p),
|
|
3119
|
+
removeFile: (p) => { try {
|
|
3120
|
+
fs.unlinkSync(p);
|
|
3121
|
+
}
|
|
3122
|
+
catch { /* best effort */ } },
|
|
3123
|
+
homeDir: os.homedir(),
|
|
3124
|
+
userUid: process.getuid?.() ?? 0,
|
|
3125
|
+
};
|
|
3126
|
+
if ((0, launchd_1.isDaemonInstalled)(launchdDevDeps)) {
|
|
3127
|
+
(0, launchd_1.uninstallLaunchAgent)(launchdDevDeps);
|
|
3128
|
+
deps.writeStdout("disabled launchd auto-restart for dev mode");
|
|
3129
|
+
}
|
|
3130
|
+
/* v8 ignore stop */
|
|
3131
|
+
// Always force-restart in dev mode — you rebuilt, you want this code running
|
|
3132
|
+
/* v8 ignore start -- dev force-restart: socket alive/stop/spawn tested via integration; tests inject mocks @preserve */
|
|
3133
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
3134
|
+
if (alive) {
|
|
3135
|
+
try {
|
|
3136
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
3137
|
+
}
|
|
3138
|
+
catch { /* already stopping */ }
|
|
3139
|
+
}
|
|
3140
|
+
deps.cleanupStaleSocket(deps.socketPath);
|
|
3141
|
+
const devEntry = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
3142
|
+
const startDevDaemon = deps.startDaemonProcess === (await Promise.resolve().then(() => __importStar(require("./cli-defaults")))).defaultStartDaemonProcess
|
|
3143
|
+
? async (sp) => {
|
|
3144
|
+
const child = (0, child_process_1.spawn)("node", [devEntry, "--socket", sp], { detached: true, stdio: "ignore" });
|
|
3145
|
+
child.unref();
|
|
3146
|
+
return { pid: child.pid ?? null };
|
|
3147
|
+
}
|
|
3148
|
+
: deps.startDaemonProcess;
|
|
3149
|
+
/* v8 ignore stop */
|
|
3150
|
+
const started = await startDevDaemon(deps.socketPath);
|
|
3151
|
+
/* v8 ignore next -- defensive: pid is null only when spawn fails silently @preserve */
|
|
3152
|
+
const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})\nrun 'ouro up' to return to production mode`;
|
|
3153
|
+
deps.writeStdout(message);
|
|
3154
|
+
return message;
|
|
3155
|
+
}
|
|
3156
|
+
// ── rollback command (local, no daemon socket needed for symlinks) ──
|
|
3157
|
+
/* v8 ignore start -- rollback/versions: tested via daemon-cli-rollback/versions tests @preserve */
|
|
3158
|
+
if (command.kind === "rollback") {
|
|
3159
|
+
const currentVersion = deps.getCurrentCliVersion?.() ?? "unknown";
|
|
3160
|
+
if (command.version) {
|
|
3161
|
+
// Rollback to a specific version
|
|
3162
|
+
const installed = deps.listCliVersions?.() ?? [];
|
|
3163
|
+
if (!installed.includes(command.version)) {
|
|
3164
|
+
try {
|
|
3165
|
+
await deps.installCliVersion(command.version);
|
|
3166
|
+
}
|
|
3167
|
+
catch (error) {
|
|
3168
|
+
const message = `failed to install version ${command.version}: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
3169
|
+
deps.writeStdout(message);
|
|
3170
|
+
return message;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
deps.activateCliVersion(command.version);
|
|
3174
|
+
}
|
|
3175
|
+
else {
|
|
3176
|
+
// Rollback to previous version
|
|
3177
|
+
const previousVersion = deps.getPreviousCliVersion?.();
|
|
3178
|
+
if (!previousVersion) {
|
|
3179
|
+
const message = "no previous version to roll back to";
|
|
3180
|
+
deps.writeStdout(message);
|
|
3181
|
+
return message;
|
|
3182
|
+
}
|
|
3183
|
+
deps.activateCliVersion(previousVersion);
|
|
3184
|
+
command = { ...command, version: previousVersion };
|
|
3185
|
+
}
|
|
3186
|
+
// Stop daemon (non-fatal if not running)
|
|
3187
|
+
try {
|
|
3188
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
3189
|
+
}
|
|
3190
|
+
catch {
|
|
3191
|
+
// Daemon may not be running — that's fine
|
|
3192
|
+
}
|
|
3193
|
+
const message = `rolled back to ${command.version} (was ${currentVersion})`;
|
|
3194
|
+
deps.writeStdout(message);
|
|
3195
|
+
return message;
|
|
3196
|
+
}
|
|
3197
|
+
// ── versions command (local install list + published update truth, no daemon socket needed) ──
|
|
3198
|
+
if (command.kind === "versions") {
|
|
3199
|
+
const versions = deps.listCliVersions?.() ?? [];
|
|
3200
|
+
const current = deps.getCurrentCliVersion?.();
|
|
3201
|
+
const previous = deps.getPreviousCliVersion?.();
|
|
3202
|
+
const localSection = versions.length === 0
|
|
3203
|
+
? "no versions installed"
|
|
3204
|
+
: versions.map((v) => {
|
|
3205
|
+
let line = v;
|
|
3206
|
+
if (v === current)
|
|
3207
|
+
line += " * current";
|
|
3208
|
+
if (v === previous)
|
|
3209
|
+
line += " (previous)";
|
|
3210
|
+
return line;
|
|
3211
|
+
}).join("\n");
|
|
3212
|
+
const sections = [localSection];
|
|
3213
|
+
if (deps.checkForCliUpdate) {
|
|
3214
|
+
try {
|
|
3215
|
+
const updateResult = await deps.checkForCliUpdate();
|
|
3216
|
+
if (updateResult.latestVersion) {
|
|
3217
|
+
sections.push(`published latest: ${updateResult.latestVersion} (${updateResult.available ? "update available" : "up to date"})`);
|
|
3218
|
+
}
|
|
3219
|
+
else if (updateResult.error) {
|
|
3220
|
+
sections.push(`published latest: unavailable (${updateResult.error})`);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
catch (err) {
|
|
3224
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3225
|
+
sections.push(`published latest: unavailable (${reason})`);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
const message = sections.join("\n\n");
|
|
3229
|
+
deps.writeStdout(message);
|
|
3230
|
+
return message;
|
|
3231
|
+
}
|
|
3232
|
+
/* v8 ignore stop */
|
|
3233
|
+
if (command.kind === "daemon.logs" && deps.tailLogs) {
|
|
3234
|
+
deps.tailLogs();
|
|
3235
|
+
return "";
|
|
3236
|
+
}
|
|
3237
|
+
if (command.kind === "daemon.logs.prune") {
|
|
3238
|
+
if (!deps.pruneDaemonLogs) {
|
|
3239
|
+
const message = "logs prune unavailable (dep not wired)";
|
|
3240
|
+
deps.writeStdout(message);
|
|
3241
|
+
return message;
|
|
3242
|
+
}
|
|
3243
|
+
const result = deps.pruneDaemonLogs();
|
|
3244
|
+
const message = `compacted ${result.filesCompacted} file${result.filesCompacted === 1 ? "" : "s"}, freed ${result.bytesFreed} bytes`;
|
|
3245
|
+
deps.writeStdout(message);
|
|
3246
|
+
return message;
|
|
3247
|
+
}
|
|
3248
|
+
if (command.kind === "outlook") {
|
|
3249
|
+
let status;
|
|
3250
|
+
try {
|
|
3251
|
+
status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
|
|
3252
|
+
/* v8 ignore start — error path: daemon not running */
|
|
3253
|
+
}
|
|
3254
|
+
catch {
|
|
3255
|
+
const message = "daemon unavailable — start with `ouro up` first";
|
|
3256
|
+
deps.writeStdout(message);
|
|
3257
|
+
return message;
|
|
3258
|
+
}
|
|
3259
|
+
/* v8 ignore stop */
|
|
3260
|
+
const payload = (0, cli_render_1.parseStatusPayload)(status.data);
|
|
3261
|
+
/* v8 ignore start -- ?? branch: outlookUrl always present in test fixtures */
|
|
3262
|
+
const outlookUrl = payload?.overview.outlookUrl ?? "unavailable";
|
|
3263
|
+
/* v8 ignore stop */
|
|
3264
|
+
if (!command.json) {
|
|
3265
|
+
deps.writeStdout(outlookUrl);
|
|
3266
|
+
return outlookUrl;
|
|
3267
|
+
}
|
|
3268
|
+
/* v8 ignore start — error path: outlook URL not available */
|
|
3269
|
+
if (outlookUrl === "unavailable") {
|
|
3270
|
+
deps.writeStdout(outlookUrl);
|
|
3271
|
+
return outlookUrl;
|
|
3272
|
+
}
|
|
3273
|
+
/* v8 ignore stop */
|
|
3274
|
+
/* v8 ignore start -- ?? branch: tests always inject fetchImpl */
|
|
3275
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
3276
|
+
/* v8 ignore stop */
|
|
3277
|
+
const response = await fetchImpl(`${outlookUrl}/api/machine`);
|
|
3278
|
+
const data = await response.json();
|
|
3279
|
+
const text = JSON.stringify(data, null, 2);
|
|
3280
|
+
deps.writeStdout(text);
|
|
3281
|
+
return text;
|
|
3282
|
+
}
|
|
3283
|
+
// ── hook: handle Claude Code lifecycle hooks ──
|
|
3284
|
+
/* v8 ignore start -- hook handler: reads real stdin, sends to real daemon @preserve */
|
|
3285
|
+
if (command.kind === "hook") {
|
|
3286
|
+
let stdinData = "";
|
|
3287
|
+
try {
|
|
3288
|
+
stdinData = require("fs").readFileSync(0, "utf-8");
|
|
3289
|
+
}
|
|
3290
|
+
catch { /* no stdin */ }
|
|
3291
|
+
let event = {};
|
|
3292
|
+
try {
|
|
3293
|
+
event = JSON.parse(stdinData);
|
|
3294
|
+
}
|
|
3295
|
+
catch { /* malformed */ }
|
|
3296
|
+
const eventType = command.event;
|
|
3297
|
+
const sessionId = event.session_id ?? "unknown";
|
|
3298
|
+
const cwd = event.cwd ?? "";
|
|
3299
|
+
// Build notification content based on event type
|
|
3300
|
+
let content;
|
|
3301
|
+
if (eventType === "session-start") {
|
|
3302
|
+
const model = event.model ?? "";
|
|
3303
|
+
const source = event.source ?? "";
|
|
3304
|
+
content = `[Claude Code session started: ${sessionId}, cwd: ${cwd}${model ? `, model: ${model}` : ""}${source ? `, source: ${source}` : ""}]`;
|
|
3305
|
+
}
|
|
3306
|
+
else if (eventType === "stop") {
|
|
3307
|
+
const lastMsg = event.last_assistant_message ?? "";
|
|
3308
|
+
content = `[Claude Code session ended: ${sessionId}${lastMsg ? `, last: ${lastMsg.slice(0, 200)}` : ""}]`;
|
|
3309
|
+
}
|
|
3310
|
+
else if (eventType === "post-tool-use") {
|
|
3311
|
+
const toolName = event.tool_name ?? "";
|
|
3312
|
+
content = `[Claude Code used ${toolName} in session ${sessionId}]`;
|
|
3313
|
+
}
|
|
3314
|
+
else {
|
|
3315
|
+
content = `[Claude Code hook: ${eventType} in session ${sessionId}]`;
|
|
3316
|
+
}
|
|
3317
|
+
// Send to the specific agent configured for this hook. Short-circuit
|
|
3318
|
+
// when the daemon socket file doesn't exist — otherwise every
|
|
3319
|
+
// Claude Code lifecycle event during a daemon-down window logs two
|
|
3320
|
+
// ENOENT errors in ouro.ndjson (one for message.send, one for
|
|
3321
|
+
// inner.wake) which makes it hard to read the log around outages.
|
|
3322
|
+
// The hook is best-effort: dropping notifications when the daemon
|
|
3323
|
+
// is down is the correct behavior; we just don't want to log spam
|
|
3324
|
+
// about it.
|
|
3325
|
+
if (require("fs").existsSync(deps.socketPath)) {
|
|
3326
|
+
try {
|
|
3327
|
+
await deps.sendCommand(deps.socketPath, { kind: "message.send", from: `claude-code:${sessionId}`, to: command.agent, content }).catch(() => { });
|
|
3328
|
+
await deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: command.agent }).catch(() => { });
|
|
3329
|
+
}
|
|
3330
|
+
catch { /* daemon not running — silent */ }
|
|
3331
|
+
}
|
|
3332
|
+
else {
|
|
3333
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3334
|
+
component: "daemon",
|
|
3335
|
+
event: "daemon.hook_skipped_no_socket",
|
|
3336
|
+
message: "claude code hook skipped — daemon socket missing",
|
|
3337
|
+
meta: { socketPath: deps.socketPath, eventType, agent: command.agent },
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
// Output for Claude Code hook system
|
|
3341
|
+
deps.writeStdout(JSON.stringify({ continue: true }));
|
|
3342
|
+
return JSON.stringify({ continue: true });
|
|
3343
|
+
}
|
|
3344
|
+
/* v8 ignore stop */
|
|
3345
|
+
// ── setup: configure dev tool integration ──
|
|
3346
|
+
if (command.kind === "setup") {
|
|
3347
|
+
const { tool, agent: setupAgent } = command;
|
|
3348
|
+
const platform = (0, platform_1.detectPlatform)();
|
|
3349
|
+
// Windows native is not yet supported
|
|
3350
|
+
if (platform === "windows-native") {
|
|
3351
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3352
|
+
component: "daemon",
|
|
3353
|
+
event: "daemon.setup_windows_native_unsupported",
|
|
3354
|
+
message: "Windows native setup not yet supported",
|
|
3355
|
+
meta: { tool, agent: setupAgent },
|
|
3356
|
+
});
|
|
3357
|
+
const message = "Windows native is not yet supported. Please run from WSL2: https://learn.microsoft.com/en-us/windows/wsl/install";
|
|
3358
|
+
deps.writeStdout(message);
|
|
3359
|
+
return message;
|
|
3360
|
+
}
|
|
3361
|
+
// Resolve platform-specific paths and commands
|
|
3362
|
+
let claudeCmd;
|
|
3363
|
+
let mcpServePrefix;
|
|
3364
|
+
let hookPrefix;
|
|
3365
|
+
let claudeConfigDir;
|
|
3366
|
+
if (platform === "wsl") {
|
|
3367
|
+
const winProfile = (0, child_process_1.execFileSync)("cmd.exe", ["/C", "echo", "%USERPROFILE%"], { stdio: "pipe" }).toString().trim();
|
|
3368
|
+
const windowsHome = (0, child_process_1.execFileSync)("wslpath", ["-u", winProfile], { stdio: "pipe" }).toString().trim();
|
|
3369
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3370
|
+
component: "daemon",
|
|
3371
|
+
event: "daemon.setup_wsl_home_resolved",
|
|
3372
|
+
message: "resolved Windows home from WSL",
|
|
3373
|
+
meta: { windowsHome },
|
|
3374
|
+
});
|
|
3375
|
+
claudeCmd = "claude.exe";
|
|
3376
|
+
mcpServePrefix = "wsl ";
|
|
3377
|
+
hookPrefix = "wsl ";
|
|
3378
|
+
claudeConfigDir = path.join(windowsHome, ".claude");
|
|
3379
|
+
}
|
|
3380
|
+
else {
|
|
3381
|
+
// macos or linux
|
|
3382
|
+
claudeCmd = "claude";
|
|
3383
|
+
mcpServePrefix = "";
|
|
3384
|
+
hookPrefix = "";
|
|
3385
|
+
claudeConfigDir = path.join(os.homedir(), ".claude");
|
|
3386
|
+
}
|
|
3387
|
+
const sourceRoot = (0, identity_1.getRepoRoot)();
|
|
3388
|
+
const runtimeMode = (0, runtime_mode_1.detectRuntimeMode)(sourceRoot);
|
|
3389
|
+
const baseMcpServeCommand = runtimeMode === "dev"
|
|
3390
|
+
? `node ${path.join(sourceRoot, "dist", "heart", "daemon", "ouro-bot-entry.js")} mcp-serve --agent ${setupAgent}`
|
|
3391
|
+
: `ouro mcp-serve --agent ${setupAgent}`;
|
|
3392
|
+
const mcpServeCommand = `${mcpServePrefix}${baseMcpServeCommand}`;
|
|
3393
|
+
if (tool === "claude-code") {
|
|
3394
|
+
// 1. Register MCP server with Claude Code
|
|
3395
|
+
const mcpAddCmd = `${claudeCmd} mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
|
|
3396
|
+
(0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
|
|
3397
|
+
// 2. Write hooks config
|
|
3398
|
+
const settingsPath = path.join(claudeConfigDir, "settings.json");
|
|
3399
|
+
let settings = {};
|
|
3400
|
+
if (fs.existsSync(settingsPath)) {
|
|
3401
|
+
try {
|
|
3402
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
3403
|
+
}
|
|
3404
|
+
catch { /* start fresh */ }
|
|
3405
|
+
}
|
|
3406
|
+
settings.hooks = {
|
|
3407
|
+
...(settings.hooks ?? {}),
|
|
3408
|
+
SessionStart: [{ hooks: [{ type: "command", command: `${hookPrefix}ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
|
|
3409
|
+
Stop: [{ hooks: [{ type: "command", command: `${hookPrefix}ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
|
|
3410
|
+
PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `${hookPrefix}ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
|
|
3411
|
+
};
|
|
3412
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
3413
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
3414
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3415
|
+
component: "daemon",
|
|
3416
|
+
event: "daemon.setup_complete",
|
|
3417
|
+
message: "dev tool setup complete",
|
|
3418
|
+
meta: { tool, agent: setupAgent, runtimeMode, platform },
|
|
3419
|
+
});
|
|
3420
|
+
// 3. Write conversation formatting instructions
|
|
3421
|
+
const claudeMdPath = path.join(claudeConfigDir, "CLAUDE.md");
|
|
3422
|
+
const agentInstructions = `\n## Agent conversations (ouro)\nWhen using MCP \`send_message\` to talk to an ouro agent, format the exchange clearly:\n- Before the tool call, briefly say what you're asking/telling the agent\n- After the response, quote the agent's reply in a blockquote, then add your reaction\n- Example: **Me → Agent:** "question" / > **Agent:** "response" / Your synthesis here\n`;
|
|
3423
|
+
let existingClaudeMd = "";
|
|
3424
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
3425
|
+
existingClaudeMd = fs.readFileSync(claudeMdPath, "utf-8");
|
|
3426
|
+
}
|
|
3427
|
+
if (!existingClaudeMd.includes("Agent conversations (ouro)")) {
|
|
3428
|
+
fs.writeFileSync(claudeMdPath, existingClaudeMd + agentInstructions);
|
|
3429
|
+
}
|
|
3430
|
+
const message = `setup complete: claude-code + ${setupAgent}\n MCP server registered\n hooks configured\n conversation formatting instructions added`;
|
|
3431
|
+
deps.writeStdout(message);
|
|
3432
|
+
return message;
|
|
3433
|
+
}
|
|
3434
|
+
else {
|
|
3435
|
+
// tool === "codex" (parseSetupCommand validates tool, so this is the only remaining option)
|
|
3436
|
+
const mcpAddCmd = `codex mcp add ouro-${setupAgent} -- ${mcpServeCommand}`;
|
|
3437
|
+
(0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
|
|
3438
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3439
|
+
component: "daemon",
|
|
3440
|
+
event: "daemon.setup_complete",
|
|
3441
|
+
message: "dev tool setup complete",
|
|
3442
|
+
meta: { tool, agent: setupAgent, runtimeMode, platform },
|
|
3443
|
+
});
|
|
3444
|
+
const message = `setup complete: codex + ${setupAgent}\n MCP server registered`;
|
|
3445
|
+
deps.writeStdout(message);
|
|
3446
|
+
return message;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
/* v8 ignore start — mcp-serve block binds to process.stdin/stdout; tested via mcp-server unit tests */
|
|
3450
|
+
// ── mcp-serve: start MCP server in-process on stdin/stdout ──
|
|
3451
|
+
if (command.kind === "mcp-serve") {
|
|
3452
|
+
const { createMcpServer } = await Promise.resolve().then(() => __importStar(require("../mcp/mcp-server")));
|
|
3453
|
+
const friendId = command.friendId ?? `local-${os.userInfo().username}`;
|
|
3454
|
+
const mcpSocketPath = command.socketOverride ?? deps.socketPath;
|
|
3455
|
+
const server = createMcpServer({
|
|
3456
|
+
agent: command.agent,
|
|
3457
|
+
friendId,
|
|
3458
|
+
socketPath: mcpSocketPath,
|
|
3459
|
+
stdin: process.stdin,
|
|
3460
|
+
stdout: process.stdout,
|
|
3461
|
+
});
|
|
3462
|
+
server.start();
|
|
3463
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3464
|
+
component: "daemon",
|
|
3465
|
+
event: "daemon.mcp_serve_started",
|
|
3466
|
+
message: "MCP server started via CLI",
|
|
3467
|
+
meta: { agent: command.agent, friendId },
|
|
3468
|
+
});
|
|
3469
|
+
// Keep process alive until stdin closes
|
|
3470
|
+
await new Promise((resolve) => {
|
|
3471
|
+
process.stdin.on("end", () => {
|
|
3472
|
+
server.stop();
|
|
3473
|
+
resolve();
|
|
3474
|
+
});
|
|
3475
|
+
});
|
|
3476
|
+
return "";
|
|
3477
|
+
}
|
|
3478
|
+
/* v8 ignore stop */
|
|
3479
|
+
// ── mcp subcommands (routed through daemon socket) ──
|
|
3480
|
+
if (command.kind === "mcp.list" || command.kind === "mcp.call") {
|
|
3481
|
+
const daemonCommand = toDaemonCommand(command);
|
|
3482
|
+
let response;
|
|
3483
|
+
try {
|
|
3484
|
+
response = await deps.sendCommand(deps.socketPath, daemonCommand);
|
|
3485
|
+
}
|
|
3486
|
+
catch {
|
|
3487
|
+
const message = "daemon unavailable — start with `ouro up` first";
|
|
3488
|
+
deps.writeStdout(message);
|
|
3489
|
+
return message;
|
|
3490
|
+
}
|
|
3491
|
+
if (!response.ok) {
|
|
3492
|
+
const message = response.error ?? "unknown error";
|
|
3493
|
+
deps.writeStdout(message);
|
|
3494
|
+
return message;
|
|
3495
|
+
}
|
|
3496
|
+
const message = (0, cli_render_1.formatMcpResponse)(command, response);
|
|
3497
|
+
deps.writeStdout(message);
|
|
3498
|
+
return message;
|
|
3499
|
+
}
|
|
3500
|
+
// ── task subcommands (local, no daemon socket needed) ──
|
|
3501
|
+
if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
|
|
3502
|
+
command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
|
|
3503
|
+
command.kind === "task.sessions" || command.kind === "task.fix") {
|
|
3504
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
3505
|
+
const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
|
|
3506
|
+
/* v8 ignore stop */
|
|
3507
|
+
const message = executeTaskCommand(command, taskMod);
|
|
3508
|
+
deps.writeStdout(message);
|
|
3509
|
+
return message;
|
|
3510
|
+
}
|
|
3511
|
+
// ── reminder subcommands (local, no daemon socket needed) ──
|
|
3512
|
+
if (command.kind === "reminder.create") {
|
|
3513
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
3514
|
+
const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
|
|
3515
|
+
/* v8 ignore stop */
|
|
3516
|
+
const message = executeReminderCommand(command, taskMod);
|
|
3517
|
+
deps.writeStdout(message);
|
|
3518
|
+
return message;
|
|
3519
|
+
}
|
|
3520
|
+
// ── habit subcommands (local, no daemon socket needed) ──
|
|
3521
|
+
if (command.kind === "habit.list" || command.kind === "habit.create") {
|
|
3522
|
+
const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
|
|
3523
|
+
/* v8 ignore start -- production default: uses real bundle root @preserve */
|
|
3524
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
3525
|
+
const bundleRoot = deps.agentBundleRoot ?? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
|
|
3526
|
+
/* v8 ignore stop */
|
|
3527
|
+
const habitsDir = path.join(bundleRoot, "habits");
|
|
3528
|
+
if (command.kind === "habit.list") {
|
|
3529
|
+
let files;
|
|
3530
|
+
try {
|
|
3531
|
+
files = fs.readdirSync(habitsDir).filter((f) => f.endsWith(".md") && f !== "README.md");
|
|
3532
|
+
}
|
|
3533
|
+
catch {
|
|
3534
|
+
const message = "no habits found";
|
|
3535
|
+
deps.writeStdout(message);
|
|
3536
|
+
return message;
|
|
3537
|
+
}
|
|
3538
|
+
if (files.length === 0) {
|
|
3539
|
+
const message = "no habits found";
|
|
3540
|
+
deps.writeStdout(message);
|
|
3541
|
+
return message;
|
|
3542
|
+
}
|
|
3543
|
+
const lines = [];
|
|
3544
|
+
for (const file of files) {
|
|
3545
|
+
const fileContent = fs.readFileSync(path.join(habitsDir, file), "utf-8");
|
|
3546
|
+
const habit = parseHabitFile(fileContent, path.join(habitsDir, file));
|
|
3547
|
+
const lastRunStr = habit.lastRun ?? "never";
|
|
3548
|
+
lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
|
|
3549
|
+
}
|
|
3550
|
+
const message = lines.join("\n");
|
|
3551
|
+
deps.writeStdout(message);
|
|
3552
|
+
return message;
|
|
3553
|
+
}
|
|
3554
|
+
// habit.create
|
|
3555
|
+
const filePath = path.join(habitsDir, `${command.name}.md`);
|
|
3556
|
+
if (fs.existsSync(filePath)) {
|
|
3557
|
+
const message = `error: habit '${command.name}' already exists`;
|
|
3558
|
+
deps.writeStdout(message);
|
|
3559
|
+
return message;
|
|
3560
|
+
}
|
|
3561
|
+
fs.mkdirSync(habitsDir, { recursive: true });
|
|
3562
|
+
const now = new Date().toISOString();
|
|
3563
|
+
const habitContent = renderHabitFile({
|
|
3564
|
+
title: command.name,
|
|
3565
|
+
cadence: command.cadence ?? "null",
|
|
3566
|
+
status: "active",
|
|
3567
|
+
lastRun: now,
|
|
3568
|
+
created: now,
|
|
3569
|
+
}, `Habit: ${command.name}`);
|
|
3570
|
+
fs.writeFileSync(filePath, habitContent, "utf-8");
|
|
3571
|
+
const message = `created: ${filePath}`;
|
|
3572
|
+
deps.writeStdout(message);
|
|
3573
|
+
return message;
|
|
3574
|
+
}
|
|
3575
|
+
// ── friend subcommands (local, no daemon socket needed) ──
|
|
3576
|
+
if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
|
|
3577
|
+
command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
|
|
3578
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
3579
|
+
let store = deps.friendStore;
|
|
3580
|
+
if (!store) {
|
|
3581
|
+
// Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
|
|
3582
|
+
const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
|
|
3583
|
+
const friendsDir = agentName
|
|
3584
|
+
? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
|
|
3585
|
+
: path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
|
|
3586
|
+
store = new store_file_1.FileFriendStore(friendsDir);
|
|
3587
|
+
}
|
|
3588
|
+
/* v8 ignore stop */
|
|
3589
|
+
const message = await executeFriendCommand(command, store);
|
|
3590
|
+
deps.writeStdout(message);
|
|
3591
|
+
return message;
|
|
3592
|
+
}
|
|
3593
|
+
// ── provider state commands (local, no daemon socket needed) ──
|
|
3594
|
+
if (command.kind === "provider.use") {
|
|
3595
|
+
return executeProviderUse(command, deps);
|
|
3596
|
+
}
|
|
3597
|
+
if (command.kind === "provider.check") {
|
|
3598
|
+
return executeProviderCheck(command, deps);
|
|
3599
|
+
}
|
|
3600
|
+
if (command.kind === "provider.status") {
|
|
3601
|
+
return executeProviderStatus(command, deps);
|
|
3602
|
+
}
|
|
3603
|
+
if (command.kind === "provider.refresh") {
|
|
3604
|
+
return executeProviderRefresh(command, deps);
|
|
3605
|
+
}
|
|
3606
|
+
if (command.kind === "repair") {
|
|
3607
|
+
return executeRepair(command, deps);
|
|
3608
|
+
}
|
|
3609
|
+
if (command.kind === "vault.unlock") {
|
|
3610
|
+
return executeVaultUnlock(command, deps);
|
|
3611
|
+
}
|
|
3612
|
+
if (command.kind === "vault.create") {
|
|
3613
|
+
return executeVaultCreate(command, deps);
|
|
3614
|
+
}
|
|
3615
|
+
if (command.kind === "vault.replace") {
|
|
3616
|
+
return executeVaultReplace(command, deps);
|
|
3617
|
+
}
|
|
3618
|
+
if (command.kind === "vault.recover") {
|
|
3619
|
+
return executeVaultRecover(command, deps);
|
|
3620
|
+
}
|
|
3621
|
+
if (command.kind === "vault.status") {
|
|
3622
|
+
return executeVaultStatus(command, deps);
|
|
3623
|
+
}
|
|
3624
|
+
if (command.kind === "vault.config.set") {
|
|
3625
|
+
return executeVaultConfigSet(command, deps);
|
|
3626
|
+
}
|
|
3627
|
+
if (command.kind === "vault.config.status") {
|
|
3628
|
+
return executeVaultConfigStatus(command, deps);
|
|
3629
|
+
}
|
|
3630
|
+
// ── auth (local, no daemon socket needed) ──
|
|
3631
|
+
if (command.kind === "auth.run") {
|
|
3632
|
+
return executeAuthRun(command, deps);
|
|
3633
|
+
}
|
|
3634
|
+
// ── auth verify (local, no daemon socket needed) ──
|
|
3635
|
+
/* v8 ignore start -- auth verify/switch: tested in daemon-cli.test.ts but v8 traces differ in CI @preserve */
|
|
3636
|
+
if (command.kind === "auth.verify") {
|
|
3637
|
+
const progress = createHumanCommandProgress(deps, "auth verify");
|
|
3638
|
+
const writeMessage = (message) => {
|
|
3639
|
+
progress.end();
|
|
3640
|
+
deps.writeStdout(message);
|
|
3641
|
+
return message;
|
|
3642
|
+
};
|
|
3643
|
+
try {
|
|
3644
|
+
progress.startPhase("reading provider credentials");
|
|
3645
|
+
const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, {
|
|
3646
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
3647
|
+
});
|
|
3648
|
+
if (!poolResult.ok) {
|
|
3649
|
+
progress.completePhase("reading provider credentials", poolResult.reason);
|
|
3650
|
+
const message = `vault unavailable: ${poolResult.error}\n${(0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro auth verify'.")}`;
|
|
3651
|
+
return writeMessage(message);
|
|
3652
|
+
}
|
|
3653
|
+
const providerCount = Object.keys(poolResult.pool.providers).length;
|
|
3654
|
+
progress.completePhase("reading provider credentials", `${providerCount} provider${providerCount === 1 ? "" : "s"}`);
|
|
3655
|
+
if (command.provider) {
|
|
3656
|
+
const record = poolResult.pool.providers[command.provider];
|
|
3657
|
+
if (!record) {
|
|
3658
|
+
const message = `${command.provider}: missing. Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\`.`;
|
|
3659
|
+
return writeMessage(message);
|
|
3660
|
+
}
|
|
3661
|
+
progress.startPhase(`verifying ${command.provider}`);
|
|
3662
|
+
const status = await verifyProviderCredentials(command.provider, {
|
|
3663
|
+
[command.provider]: { ...record.config, ...record.credentials },
|
|
3664
|
+
});
|
|
3665
|
+
progress.completePhase(`verifying ${command.provider}`, status);
|
|
3666
|
+
const message = `${command.provider}: ${status}`;
|
|
3667
|
+
return writeMessage(message);
|
|
3668
|
+
}
|
|
3669
|
+
const lines = [];
|
|
3670
|
+
const entries = Object.entries(poolResult.pool.providers);
|
|
3671
|
+
if (entries.length > 0) {
|
|
3672
|
+
progress.startPhase("verifying providers");
|
|
3673
|
+
}
|
|
3674
|
+
for (const [p, record] of entries) {
|
|
3675
|
+
const status = await verifyProviderCredentials(p, {
|
|
3676
|
+
[p]: { ...record.config, ...record.credentials },
|
|
3677
|
+
});
|
|
3678
|
+
const line = `${p}: ${status}`;
|
|
3679
|
+
lines.push(line);
|
|
3680
|
+
progress.updateDetail(line);
|
|
3681
|
+
}
|
|
3682
|
+
if (entries.length > 0) {
|
|
3683
|
+
progress.completePhase("verifying providers", `${entries.length} checked`);
|
|
3684
|
+
}
|
|
3685
|
+
if (lines.length === 0)
|
|
3686
|
+
lines.push(`no provider credentials in ${command.agent}'s vault`);
|
|
3687
|
+
const message = lines.join("\n");
|
|
3688
|
+
return writeMessage(message);
|
|
3689
|
+
}
|
|
3690
|
+
catch (error) {
|
|
3691
|
+
progress.end();
|
|
3692
|
+
throw error;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
// ── auth switch (local, no daemon socket needed) ──
|
|
3696
|
+
if (command.kind === "auth.switch") {
|
|
3697
|
+
return executeLegacyAuthSwitch(command, deps);
|
|
3698
|
+
}
|
|
3699
|
+
/* v8 ignore stop */
|
|
3700
|
+
// ── config models (local, no daemon socket needed) ──
|
|
3701
|
+
/* v8 ignore start -- config models: tested via daemon-cli.test.ts @preserve */
|
|
3702
|
+
if (command.kind === "config.models") {
|
|
3703
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
|
|
3704
|
+
const provider = config.humanFacing.provider;
|
|
3705
|
+
if (provider !== "github-copilot") {
|
|
3706
|
+
const message = `model listing not available for ${provider} — check provider documentation.`;
|
|
3707
|
+
deps.writeStdout(message);
|
|
3708
|
+
return message;
|
|
3709
|
+
}
|
|
3710
|
+
const progress = createHumanCommandProgress(deps, "config models");
|
|
3711
|
+
const writeMessage = (message) => {
|
|
3712
|
+
progress.end();
|
|
3713
|
+
deps.writeStdout(message);
|
|
3714
|
+
return message;
|
|
3715
|
+
};
|
|
3716
|
+
try {
|
|
3717
|
+
progress.startPhase("reading github-copilot credentials");
|
|
3718
|
+
const credential = await readProviderCredentialRecord(command.agent, "github-copilot", deps, {
|
|
3719
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
3720
|
+
});
|
|
3721
|
+
const ghConfig = credential.ok
|
|
3722
|
+
? { ...credential.record.config, ...credential.record.credentials }
|
|
3723
|
+
: {};
|
|
3724
|
+
if (!credential.ok || typeof ghConfig.githubToken !== "string" || typeof ghConfig.baseUrl !== "string") {
|
|
3725
|
+
progress.completePhase("reading github-copilot credentials", credential.ok ? "missing fields" : credential.reason);
|
|
3726
|
+
throw new Error(`github-copilot credentials not configured. Run \`ouro auth --agent ${command.agent} --provider github-copilot\` first.`);
|
|
3727
|
+
}
|
|
3728
|
+
progress.completePhase("reading github-copilot credentials", "found");
|
|
3729
|
+
const fetchFn = deps.fetchImpl ?? fetch;
|
|
3730
|
+
progress.startPhase("listing github-copilot models");
|
|
3731
|
+
const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
|
|
3732
|
+
progress.completePhase("listing github-copilot models", `${models.length} model${models.length === 1 ? "" : "s"}`);
|
|
3733
|
+
if (models.length === 0) {
|
|
3734
|
+
return writeMessage("no models found");
|
|
3735
|
+
}
|
|
3736
|
+
const lines = ["available models:"];
|
|
3737
|
+
for (const m of models) {
|
|
3738
|
+
const caps = m.capabilities?.length ? ` (${m.capabilities.join(", ")})` : "";
|
|
3739
|
+
lines.push(` ${m.id}${caps}`);
|
|
3740
|
+
}
|
|
3741
|
+
const message = lines.join("\n");
|
|
3742
|
+
return writeMessage(message);
|
|
3743
|
+
}
|
|
3744
|
+
catch (error) {
|
|
3745
|
+
progress.end();
|
|
3746
|
+
throw error;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
/* v8 ignore stop */
|
|
3750
|
+
// ── config model (local, no daemon socket needed) ──
|
|
3751
|
+
/* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
|
|
3752
|
+
if (command.kind === "config.model") {
|
|
3753
|
+
return executeLegacyConfigModel(command, deps);
|
|
3754
|
+
}
|
|
3755
|
+
/* v8 ignore stop */
|
|
3756
|
+
// ── whoami (local, no daemon socket needed) ──
|
|
3757
|
+
if (command.kind === "whoami") {
|
|
3758
|
+
if (command.agent) {
|
|
3759
|
+
const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
|
|
3760
|
+
const message = [
|
|
3761
|
+
`agent: ${command.agent}`,
|
|
3762
|
+
`home: ${agentRoot}`,
|
|
3763
|
+
`bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
|
|
3764
|
+
].join("\n");
|
|
3765
|
+
deps.writeStdout(message);
|
|
3766
|
+
return message;
|
|
3767
|
+
}
|
|
3768
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
3769
|
+
try {
|
|
3770
|
+
const info = deps.whoamiInfo
|
|
3771
|
+
? deps.whoamiInfo()
|
|
3772
|
+
: {
|
|
3773
|
+
agentName: (0, identity_1.getAgentName)(),
|
|
3774
|
+
homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
|
|
3775
|
+
bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
|
|
3776
|
+
};
|
|
3777
|
+
const message = [
|
|
3778
|
+
`agent: ${info.agentName}`,
|
|
3779
|
+
`home: ${info.homePath}`,
|
|
3780
|
+
`bones: ${info.bonesVersion}`,
|
|
3781
|
+
].join("\n");
|
|
3782
|
+
deps.writeStdout(message);
|
|
3783
|
+
return message;
|
|
3784
|
+
}
|
|
3785
|
+
catch {
|
|
3786
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
3787
|
+
deps.writeStdout(message);
|
|
3788
|
+
return message;
|
|
3789
|
+
}
|
|
3790
|
+
/* v8 ignore stop */
|
|
3791
|
+
}
|
|
3792
|
+
// ── changelog (local, no daemon socket needed) ──
|
|
3793
|
+
if (command.kind === "changelog") {
|
|
3794
|
+
try {
|
|
3795
|
+
const changelogPath = deps.getChangelogPath
|
|
3796
|
+
? deps.getChangelogPath()
|
|
3797
|
+
: (0, bundle_manifest_1.getChangelogPath)();
|
|
3798
|
+
const raw = fs.readFileSync(changelogPath, "utf-8");
|
|
3799
|
+
const parsed = JSON.parse(raw);
|
|
3800
|
+
const entries = Array.isArray(parsed) ? parsed : (parsed.versions ?? []);
|
|
3801
|
+
let filtered = entries;
|
|
3802
|
+
if (command.from) {
|
|
3803
|
+
const fromVersion = command.from;
|
|
3804
|
+
filtered = entries.filter((e) => semver.valid(e.version) && semver.gt(e.version, fromVersion));
|
|
3805
|
+
}
|
|
3806
|
+
if (filtered.length === 0) {
|
|
3807
|
+
const message = "no changelog entries found.";
|
|
3808
|
+
deps.writeStdout(message);
|
|
3809
|
+
return message;
|
|
3810
|
+
}
|
|
3811
|
+
const lines = [];
|
|
3812
|
+
for (const entry of filtered) {
|
|
3813
|
+
lines.push(`## ${entry.version}${entry.date ? ` (${entry.date})` : ""}`);
|
|
3814
|
+
if (entry.changes) {
|
|
3815
|
+
for (const change of entry.changes) {
|
|
3816
|
+
lines.push(`- ${change}`);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
lines.push("");
|
|
3820
|
+
}
|
|
3821
|
+
const message = lines.join("\n").trim();
|
|
3822
|
+
deps.writeStdout(message);
|
|
3823
|
+
return message;
|
|
3824
|
+
}
|
|
3825
|
+
catch {
|
|
3826
|
+
const message = "no changelog entries found.";
|
|
3827
|
+
deps.writeStdout(message);
|
|
3828
|
+
return message;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
// ── thoughts (local, no daemon socket needed) ──
|
|
3832
|
+
if (command.kind === "thoughts") {
|
|
3833
|
+
try {
|
|
3834
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
3835
|
+
/* v8 ignore next -- production fallback: tests always inject bundlesRoot via createTmpBundle @preserve */
|
|
3836
|
+
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
3837
|
+
const agentRoot = path.join(bundlesRoot, `${agentName}.ouro`);
|
|
3838
|
+
const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
|
|
3839
|
+
if (command.json) {
|
|
3840
|
+
try {
|
|
3841
|
+
const raw = fs.readFileSync(sessionFilePath, "utf-8");
|
|
3842
|
+
deps.writeStdout(raw);
|
|
3843
|
+
return raw;
|
|
3844
|
+
}
|
|
3845
|
+
catch {
|
|
3846
|
+
const message = "no inner dialog session found";
|
|
3847
|
+
deps.writeStdout(message);
|
|
3848
|
+
return message;
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
|
|
3852
|
+
const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
|
|
3853
|
+
deps.writeStdout(message);
|
|
3854
|
+
if (command.follow) {
|
|
3855
|
+
deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
|
|
3856
|
+
/* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
|
|
3857
|
+
const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
|
|
3858
|
+
deps.writeStdout("\n" + formatted);
|
|
3859
|
+
});
|
|
3860
|
+
/* v8 ignore stop */
|
|
3861
|
+
// Block until process exit; cleanup watcher on SIGINT/SIGTERM
|
|
3862
|
+
return new Promise((resolve) => {
|
|
3863
|
+
const cleanup = () => { stop(); resolve(message); };
|
|
3864
|
+
process.once("SIGINT", cleanup);
|
|
3865
|
+
process.once("SIGTERM", cleanup);
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
return message;
|
|
3869
|
+
}
|
|
3870
|
+
catch {
|
|
3871
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
3872
|
+
deps.writeStdout(message);
|
|
3873
|
+
return message;
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
// ── attention queue (local, no daemon socket needed) ──
|
|
3877
|
+
/* v8 ignore start -- CLI attention handler: requires real obligation store on disk @preserve */
|
|
3878
|
+
if (command.kind === "attention.list" || command.kind === "attention.show" || command.kind === "attention.history") {
|
|
3879
|
+
try {
|
|
3880
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
3881
|
+
const { listActiveReturnObligations, readReturnObligation } = await Promise.resolve().then(() => __importStar(require("../../arc/obligations")));
|
|
3882
|
+
if (command.kind === "attention.list") {
|
|
3883
|
+
const obligations = listActiveReturnObligations(agentName);
|
|
3884
|
+
if (obligations.length === 0) {
|
|
3885
|
+
const message = "nothing held — attention queue is empty";
|
|
3886
|
+
deps.writeStdout(message);
|
|
3887
|
+
return message;
|
|
3888
|
+
}
|
|
3889
|
+
const lines = obligations.map((o) => `[${o.id}] ${o.origin.friendId} via ${o.origin.channel}/${o.origin.key} — ${o.delegatedContent.slice(0, 60)}${o.delegatedContent.length > 60 ? "..." : ""} (${o.status})`);
|
|
3890
|
+
const message = lines.join("\n");
|
|
3891
|
+
deps.writeStdout(message);
|
|
3892
|
+
return message;
|
|
3893
|
+
}
|
|
3894
|
+
if (command.kind === "attention.show") {
|
|
3895
|
+
const obligation = readReturnObligation(agentName, command.id);
|
|
3896
|
+
if (!obligation) {
|
|
3897
|
+
const message = `no obligation found with id ${command.id}`;
|
|
3898
|
+
deps.writeStdout(message);
|
|
3899
|
+
return message;
|
|
3900
|
+
}
|
|
3901
|
+
const message = JSON.stringify(obligation, null, 2);
|
|
3902
|
+
deps.writeStdout(message);
|
|
3903
|
+
return message;
|
|
3904
|
+
}
|
|
3905
|
+
// attention.history: show returned obligations
|
|
3906
|
+
const { getReturnObligationsDir } = await Promise.resolve().then(() => __importStar(require("../../arc/obligations")));
|
|
3907
|
+
const obligationsDir = getReturnObligationsDir(agentName);
|
|
3908
|
+
let attEntries = [];
|
|
3909
|
+
try {
|
|
3910
|
+
attEntries = fs.readdirSync(obligationsDir);
|
|
3911
|
+
}
|
|
3912
|
+
catch { /* empty */ }
|
|
3913
|
+
const returned = attEntries
|
|
3914
|
+
.filter((e) => e.endsWith(".json"))
|
|
3915
|
+
.map((e) => { try {
|
|
3916
|
+
return JSON.parse(fs.readFileSync(path.join(obligationsDir, e), "utf-8"));
|
|
3917
|
+
}
|
|
3918
|
+
catch {
|
|
3919
|
+
return null;
|
|
3920
|
+
} })
|
|
3921
|
+
.filter((o) => o?.status === "returned")
|
|
3922
|
+
.sort((a, b) => (b.returnedAt ?? 0) - (a.returnedAt ?? 0))
|
|
3923
|
+
.slice(0, 20);
|
|
3924
|
+
if (returned.length === 0) {
|
|
3925
|
+
const message = "no surfacing history yet";
|
|
3926
|
+
deps.writeStdout(message);
|
|
3927
|
+
return message;
|
|
3928
|
+
}
|
|
3929
|
+
const lines = returned.map((o) => {
|
|
3930
|
+
const when = o.returnedAt ? new Date(o.returnedAt).toISOString() : "unknown";
|
|
3931
|
+
return `[${o.id}] → ${o.origin.friendId} via ${o.returnTarget ?? "unknown"} at ${when}`;
|
|
3932
|
+
});
|
|
3933
|
+
const message = lines.join("\n");
|
|
3934
|
+
deps.writeStdout(message);
|
|
3935
|
+
return message;
|
|
3936
|
+
}
|
|
3937
|
+
catch {
|
|
3938
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
3939
|
+
deps.writeStdout(message);
|
|
3940
|
+
return message;
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
/* v8 ignore stop */
|
|
3944
|
+
// ── inner dialog status (local, no daemon socket needed) ──
|
|
3945
|
+
/* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
|
|
3946
|
+
if (command.kind === "inner.status") {
|
|
3947
|
+
try {
|
|
3948
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
3949
|
+
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
3950
|
+
const { buildInnerStatusOutput } = await Promise.resolve().then(() => __importStar(require("./inner-status")));
|
|
3951
|
+
const { sessionPath: getSessionPath } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
3952
|
+
const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
|
|
3953
|
+
const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../repertoire/tasks/parser")));
|
|
3954
|
+
const { listActiveReturnObligations } = await Promise.resolve().then(() => __importStar(require("../../arc/obligations")));
|
|
3955
|
+
// Read runtime state
|
|
3956
|
+
const innerSessionPath = getSessionPath("inner-dialog", "inner", "session");
|
|
3957
|
+
const runtimeJsonPath = path.join(path.dirname(innerSessionPath), "runtime.json");
|
|
3958
|
+
let runtimeState = null;
|
|
3959
|
+
try {
|
|
3960
|
+
const raw = fs.readFileSync(runtimeJsonPath, "utf-8");
|
|
3961
|
+
runtimeState = JSON.parse(raw);
|
|
3962
|
+
}
|
|
3963
|
+
catch { /* missing or corrupt — will show "unknown" */ }
|
|
3964
|
+
// Read journal files
|
|
3965
|
+
const journalDir = path.join(agentRoot, "journal");
|
|
3966
|
+
let journalFiles = [];
|
|
3967
|
+
try {
|
|
3968
|
+
const journalEntries = fs.readdirSync(journalDir, { withFileTypes: true });
|
|
3969
|
+
journalFiles = journalEntries
|
|
3970
|
+
.filter((e) => e.isFile() && !e.name.startsWith("."))
|
|
3971
|
+
.map((e) => {
|
|
3972
|
+
const stat = fs.statSync(path.join(journalDir, e.name));
|
|
3973
|
+
return { name: e.name, mtimeMs: stat.mtimeMs };
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3976
|
+
catch { /* missing dir — will show (empty) */ }
|
|
3977
|
+
// Read heartbeat cadence
|
|
3978
|
+
let heartbeat = null;
|
|
3979
|
+
try {
|
|
3980
|
+
const habitsDir = path.join(agentRoot, "habits");
|
|
3981
|
+
const heartbeatPath = path.join(habitsDir, "heartbeat.md");
|
|
3982
|
+
let cadenceMs = DEFAULT_CADENCE_MS;
|
|
3983
|
+
if (fs.existsSync(heartbeatPath)) {
|
|
3984
|
+
const heartbeatContent = fs.readFileSync(heartbeatPath, "utf-8");
|
|
3985
|
+
const lines = heartbeatContent.split(/\r?\n/);
|
|
3986
|
+
if (lines[0]?.trim() === "---") {
|
|
3987
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
3988
|
+
if (closing !== -1) {
|
|
3989
|
+
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
3990
|
+
const frontmatter = parseFrontmatter(rawFrontmatter);
|
|
3991
|
+
const parsedCadence = parseCadenceMs(frontmatter.cadence);
|
|
3992
|
+
if (parsedCadence !== null)
|
|
3993
|
+
cadenceMs = parsedCadence;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
let lastCompletedAt = null;
|
|
3998
|
+
if (runtimeState?.lastCompletedAt) {
|
|
3999
|
+
const ms = new Date(runtimeState.lastCompletedAt).getTime();
|
|
4000
|
+
if (!Number.isNaN(ms))
|
|
4001
|
+
lastCompletedAt = ms;
|
|
4002
|
+
}
|
|
4003
|
+
heartbeat = { cadenceMs, lastCompletedAt };
|
|
4004
|
+
}
|
|
4005
|
+
catch { /* no habits — heartbeat unknown */ }
|
|
4006
|
+
// Attention count
|
|
4007
|
+
const activeObligations = listActiveReturnObligations(agentName);
|
|
4008
|
+
const message = buildInnerStatusOutput({
|
|
4009
|
+
agentName,
|
|
4010
|
+
runtimeState,
|
|
4011
|
+
journalFiles,
|
|
4012
|
+
heartbeat,
|
|
4013
|
+
attentionCount: activeObligations.length,
|
|
4014
|
+
now: Date.now(),
|
|
4015
|
+
});
|
|
4016
|
+
deps.writeStdout(message);
|
|
4017
|
+
return message;
|
|
4018
|
+
}
|
|
4019
|
+
catch {
|
|
4020
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
4021
|
+
deps.writeStdout(message);
|
|
4022
|
+
return message;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
/* v8 ignore stop */
|
|
4026
|
+
// ── session list (local, no daemon socket needed) ──
|
|
4027
|
+
if (command.kind === "session.list") {
|
|
4028
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
4029
|
+
const scanner = deps.scanSessions ?? (async () => []);
|
|
4030
|
+
/* v8 ignore stop */
|
|
4031
|
+
const sessions = await scanner();
|
|
4032
|
+
if (sessions.length === 0) {
|
|
4033
|
+
const message = "no active sessions";
|
|
4034
|
+
deps.writeStdout(message);
|
|
4035
|
+
return message;
|
|
4036
|
+
}
|
|
4037
|
+
const lines = sessions.map((s) => `${s.friendId} ${s.friendName} ${s.channel} ${s.lastActivity}`);
|
|
4038
|
+
const message = lines.join("\n");
|
|
4039
|
+
deps.writeStdout(message);
|
|
4040
|
+
return message;
|
|
4041
|
+
}
|
|
4042
|
+
if (command.kind === "chat.connect" && deps.startChat) {
|
|
4043
|
+
let agent = command.agent;
|
|
4044
|
+
// No agent specified — show selection
|
|
4045
|
+
/* v8 ignore start -- interactive agent selection: requires real promptInput + discovered agents @preserve */
|
|
4046
|
+
if (!agent) {
|
|
4047
|
+
const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : (0, cli_defaults_1.defaultListDiscoveredAgents)());
|
|
4048
|
+
if (discovered.length === 0) {
|
|
4049
|
+
deps.writeStdout("no agents found — run `ouro` to hatch one");
|
|
4050
|
+
return "no agents found";
|
|
4051
|
+
}
|
|
4052
|
+
if (discovered.length === 1) {
|
|
4053
|
+
agent = discovered[0];
|
|
4054
|
+
}
|
|
4055
|
+
else if (deps.promptInput) {
|
|
4056
|
+
const prompt = `who do you want to talk to?\n${discovered.map((a, i) => `${i + 1}. ${a}`).join("\n")}\n`;
|
|
4057
|
+
const answer = await deps.promptInput(prompt);
|
|
4058
|
+
agent = discovered.includes(answer) ? answer : discovered[parseInt(answer, 10) - 1];
|
|
4059
|
+
if (!agent) {
|
|
4060
|
+
deps.writeStdout("invalid selection");
|
|
4061
|
+
return "invalid selection";
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
else {
|
|
4065
|
+
const message = `who do you want to talk to? ${discovered.join(", ")} (use: ouro chat <agent>)`;
|
|
4066
|
+
deps.writeStdout(message);
|
|
4067
|
+
return message;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
/* v8 ignore stop */
|
|
4071
|
+
await ensureDaemonRunning(deps);
|
|
4072
|
+
// Check provider health before launching chat — fail fast with
|
|
4073
|
+
// actionable guidance instead of erroring mid-conversation.
|
|
4074
|
+
const health = await checkProviderHealthBeforeChat(agent, deps);
|
|
4075
|
+
if (!health.ok)
|
|
4076
|
+
return health.output;
|
|
4077
|
+
await deps.startChat(agent);
|
|
4078
|
+
return "";
|
|
4079
|
+
}
|
|
4080
|
+
if (command.kind === "hatch.start") {
|
|
4081
|
+
// Route through serpent guide when no explicit hatch args were provided
|
|
4082
|
+
const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
|
|
4083
|
+
if (deps.runSerpentGuide && !hasExplicitHatchArgs) {
|
|
4084
|
+
// System setup first — ouro command, subagents, UTI — before the interactive specialist
|
|
4085
|
+
await performSystemSetup(deps);
|
|
4086
|
+
const hatchlingName = await deps.runSerpentGuide();
|
|
4087
|
+
if (!hatchlingName) {
|
|
4088
|
+
return "";
|
|
4089
|
+
}
|
|
4090
|
+
await ensureDaemonRunning(deps);
|
|
4091
|
+
if (deps.startChat) {
|
|
4092
|
+
await deps.startChat(hatchlingName);
|
|
4093
|
+
}
|
|
4094
|
+
return "";
|
|
4095
|
+
}
|
|
4096
|
+
const hatchRunner = deps.runHatchFlow;
|
|
4097
|
+
if (!hatchRunner) {
|
|
4098
|
+
const response = await deps.sendCommand(deps.socketPath, { kind: "hatch.start" });
|
|
4099
|
+
const message = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
|
|
4100
|
+
deps.writeStdout(message);
|
|
4101
|
+
return message;
|
|
4102
|
+
}
|
|
4103
|
+
const hatchInput = await resolveHatchInput(command, deps);
|
|
4104
|
+
const result = await hatchRunner(hatchInput);
|
|
4105
|
+
await performSystemSetup(deps);
|
|
4106
|
+
const daemonResult = await ensureDaemonRunning(deps);
|
|
4107
|
+
if (deps.startChat) {
|
|
4108
|
+
await deps.startChat(hatchInput.agentName);
|
|
4109
|
+
return "";
|
|
4110
|
+
}
|
|
4111
|
+
const message = `hatched ${hatchInput.agentName} at ${result.bundleRoot} using specialist identity ${result.selectedIdentity}; ${daemonResult.message}`;
|
|
4112
|
+
deps.writeStdout(message);
|
|
4113
|
+
return message;
|
|
4114
|
+
}
|
|
4115
|
+
// ── doctor (local, no daemon socket needed) ──
|
|
4116
|
+
if (command.kind === "doctor") {
|
|
4117
|
+
const doctorDeps = {
|
|
4118
|
+
/* v8 ignore start -- thin fs wrappers tested via doctor.test.ts with injected deps @preserve */
|
|
4119
|
+
existsSync: (p) => fs.existsSync(p),
|
|
4120
|
+
readFileSync: (p) => fs.readFileSync(p, "utf-8"),
|
|
4121
|
+
readdirSync: (p) => fs.readdirSync(p),
|
|
4122
|
+
statSync: (p) => fs.statSync(p),
|
|
4123
|
+
/* v8 ignore stop */
|
|
4124
|
+
checkSocketAlive: deps.checkSocketAlive,
|
|
4125
|
+
fetchImpl: deps.fetchImpl ?? fetch,
|
|
4126
|
+
socketPath: deps.socketPath,
|
|
4127
|
+
bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
|
|
4128
|
+
homedir: os.homedir(),
|
|
4129
|
+
envPath: process.env.PATH ?? "",
|
|
4130
|
+
};
|
|
4131
|
+
const doctorResult = await (0, doctor_1.runDoctorChecks)(doctorDeps);
|
|
4132
|
+
const output = (0, cli_render_doctor_1.formatDoctorOutput)(doctorResult);
|
|
4133
|
+
deps.writeStdout(output);
|
|
4134
|
+
(0, runtime_1.emitNervesEvent)({
|
|
4135
|
+
component: "daemon",
|
|
4136
|
+
event: "daemon.doctor_run",
|
|
4137
|
+
message: "ouro doctor completed",
|
|
4138
|
+
meta: { passed: doctorResult.summary.passed, warnings: doctorResult.summary.warnings, failed: doctorResult.summary.failed },
|
|
4139
|
+
});
|
|
4140
|
+
return output;
|
|
4141
|
+
}
|
|
4142
|
+
// ── clone: clone an agent bundle from a git remote ──
|
|
4143
|
+
if (command.kind === "clone") {
|
|
4144
|
+
(0, runtime_1.emitNervesEvent)({
|
|
4145
|
+
component: "daemon",
|
|
4146
|
+
event: "daemon.clone_start",
|
|
4147
|
+
message: "starting agent bundle clone",
|
|
4148
|
+
meta: { remote: command.remote, agent: command.agent },
|
|
4149
|
+
});
|
|
4150
|
+
// 1. Check git is installed
|
|
4151
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_git_check", message: "checking git installation", meta: {} });
|
|
4152
|
+
try {
|
|
4153
|
+
(0, child_process_1.execFileSync)("git", ["--version"], { stdio: "pipe" });
|
|
4154
|
+
}
|
|
4155
|
+
catch (err) {
|
|
4156
|
+
const message = "git is not installed -- install it from https://git-scm.com\nOn macOS: brew install git\nOn Ubuntu/Debian: sudo apt install git\nOn Windows: download from https://git-scm.com/download/win";
|
|
4157
|
+
deps.writeStdout(message);
|
|
4158
|
+
return message;
|
|
4159
|
+
}
|
|
4160
|
+
// 2. Infer agent name
|
|
4161
|
+
const agentName = command.agent ?? (0, cli_parse_1.inferAgentNameFromRemote)(command.remote);
|
|
4162
|
+
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
4163
|
+
const targetPath = path.join(bundlesRoot, agentName + ".ouro");
|
|
4164
|
+
// 3. Check target path does not exist
|
|
4165
|
+
if (fs.existsSync(targetPath)) {
|
|
4166
|
+
const message = `${targetPath} already exists. Remove it first or use --agent to pick a different name.`;
|
|
4167
|
+
deps.writeStdout(message);
|
|
4168
|
+
return message;
|
|
4169
|
+
}
|
|
4170
|
+
// 4. Check remote accessible
|
|
4171
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_remote_check", message: "checking remote accessibility", meta: { remote: command.remote } });
|
|
4172
|
+
try {
|
|
4173
|
+
(0, child_process_1.execFileSync)("git", ["ls-remote", "--exit-code", command.remote], { stdio: "pipe", timeout: 15000 });
|
|
4174
|
+
}
|
|
4175
|
+
catch (lsErr) {
|
|
4176
|
+
const stderr = lsErr?.stderr?.toString() ?? "";
|
|
4177
|
+
const isAuth = stderr.includes("Authentication failed")
|
|
4178
|
+
|| stderr.includes("could not read Username")
|
|
4179
|
+
|| stderr.includes("terminal prompts disabled")
|
|
4180
|
+
|| stderr.includes("403")
|
|
4181
|
+
|| stderr.includes("401");
|
|
4182
|
+
const hint = isAuth
|
|
4183
|
+
? `authentication failed for: ${command.remote}\nSet up credentials first:\n gh auth login (GitHub repos)\n git config credential.helper store (other hosts)`
|
|
4184
|
+
: `could not reach remote: ${command.remote}\nCheck the URL and your network connection.`;
|
|
4185
|
+
deps.writeStdout(hint);
|
|
4186
|
+
return hint;
|
|
4187
|
+
}
|
|
4188
|
+
// 5. Clone
|
|
4189
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_git_clone", message: "cloning agent bundle", meta: { remote: command.remote, targetPath } });
|
|
4190
|
+
(0, child_process_1.execFileSync)("git", ["clone", command.remote, targetPath], { stdio: "pipe" });
|
|
4191
|
+
// 6. Create machine identity
|
|
4192
|
+
(0, machine_identity_1.loadOrCreateMachineIdentity)();
|
|
4193
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_identity_created", message: "machine identity created", meta: {} });
|
|
4194
|
+
// 7. Enable sync in agent.json
|
|
4195
|
+
const agentJsonPath = path.join(targetPath, "agent.json");
|
|
4196
|
+
let syncEnabled = false;
|
|
4197
|
+
if (fs.existsSync(agentJsonPath)) {
|
|
4198
|
+
const raw = fs.readFileSync(agentJsonPath, "utf-8");
|
|
4199
|
+
const config = JSON.parse(raw);
|
|
4200
|
+
config.sync = { enabled: true, remote: "origin" };
|
|
4201
|
+
fs.writeFileSync(agentJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
4202
|
+
syncEnabled = true;
|
|
4203
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_sync_enabled", message: "sync enabled in agent.json", meta: { agentName } });
|
|
4204
|
+
}
|
|
4205
|
+
else {
|
|
4206
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "daemon", event: "daemon.clone_no_agent_json", message: "cloned repo has no agent.json — may not be a valid bundle", meta: { agentName, targetPath } });
|
|
4207
|
+
}
|
|
4208
|
+
// 8. Output success message
|
|
4209
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_complete", message: "clone complete", meta: { agentName, targetPath } });
|
|
4210
|
+
const syncSummary = syncEnabled ? pushAgentBundleAfterCliMutation(agentName, deps) : null;
|
|
4211
|
+
const syncMsg = syncEnabled ? "\nsync enabled (remote: origin)" : "\nwarning: no agent.json found — this may not be a valid agent bundle";
|
|
4212
|
+
const syncPushMsg = syncSummary ? `\n${syncSummary}` : "";
|
|
4213
|
+
deps.writeStdout(`cloned ${agentName} to ${targetPath}${syncMsg}${syncPushMsg}`);
|
|
4214
|
+
// 9. Guided post-clone flow (when interactive)
|
|
4215
|
+
if (deps.promptInput) {
|
|
4216
|
+
// Auth
|
|
4217
|
+
const authAnswer = await deps.promptInput(`\nSet up provider auth now? (y/n): `) ?? "";
|
|
4218
|
+
if (authAnswer.trim().toLowerCase() === "y") {
|
|
4219
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_chain_auth", message: "chaining auth from clone flow", meta: { agentName } });
|
|
4220
|
+
try {
|
|
4221
|
+
await runOuroCli(["auth", "--agent", agentName], deps);
|
|
4222
|
+
/* v8 ignore start -- chained command failures: tested via interactive clone test, catch branches are defensive @preserve */
|
|
4223
|
+
}
|
|
4224
|
+
catch (e) {
|
|
4225
|
+
deps.writeStdout(`auth setup failed: ${e instanceof Error ? e.message : String(e)}\nYou can retry later with: ouro auth --agent ${agentName}`);
|
|
4226
|
+
}
|
|
4227
|
+
/* v8 ignore stop */
|
|
4228
|
+
}
|
|
4229
|
+
// Daemon
|
|
4230
|
+
const upAnswer = await deps.promptInput(`\nStart the daemon now? (y/n): `) ?? "";
|
|
4231
|
+
if (upAnswer.trim().toLowerCase() === "y") {
|
|
4232
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_chain_up", message: "chaining daemon start from clone flow", meta: { agentName } });
|
|
4233
|
+
try {
|
|
4234
|
+
await runOuroCli(["up"], deps);
|
|
4235
|
+
/* v8 ignore start -- chained command failures: defensive catch @preserve */
|
|
4236
|
+
}
|
|
4237
|
+
catch (e) {
|
|
4238
|
+
deps.writeStdout(`daemon start failed: ${e instanceof Error ? e.message : String(e)}\nYou can retry later with: ouro up`);
|
|
4239
|
+
}
|
|
4240
|
+
/* v8 ignore stop */
|
|
4241
|
+
}
|
|
4242
|
+
// Dev tool setup
|
|
4243
|
+
const setupAnswer = await deps.promptInput(`\nSet up Claude Code integration? (y/n): `) ?? "";
|
|
4244
|
+
if (setupAnswer.trim().toLowerCase() === "y") {
|
|
4245
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.clone_chain_setup", message: "chaining dev tool setup from clone flow", meta: { agentName } });
|
|
4246
|
+
try {
|
|
4247
|
+
await runOuroCli(["setup", "--tool", "claude-code", "--agent", agentName], deps);
|
|
4248
|
+
/* v8 ignore start -- chained command failures: defensive catch @preserve */
|
|
4249
|
+
}
|
|
4250
|
+
catch (e) {
|
|
4251
|
+
deps.writeStdout(`dev tool setup failed: ${e instanceof Error ? e.message : String(e)}\nYou can retry later with: ouro setup --tool claude-code --agent ${agentName}`);
|
|
4252
|
+
}
|
|
4253
|
+
/* v8 ignore stop */
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
else {
|
|
4257
|
+
deps.writeStdout(`\nnext steps:\n ouro vault unlock --agent ${agentName}\n ouro provider refresh --agent ${agentName}\n ouro auth verify --agent ${agentName}\n ouro up\n ouro setup --tool claude-code --agent ${agentName}`);
|
|
4258
|
+
}
|
|
4259
|
+
/* v8 ignore start -- PATH hint: only fires inside npx, not testable in vitest @preserve */
|
|
4260
|
+
if (process.env.npm_execpath) {
|
|
4261
|
+
const shell = process.env.SHELL ? path.basename(process.env.SHELL) : "";
|
|
4262
|
+
const bashProfile = process.platform === "darwin" ? "~/.bash_profile" : "~/.bashrc";
|
|
4263
|
+
const sourceCmd = shell === "zsh" ? "source ~/.zshrc"
|
|
4264
|
+
: shell === "bash" ? `source ${bashProfile}`
|
|
4265
|
+
: shell === "fish" ? "source ~/.config/fish/config.fish"
|
|
4266
|
+
: "restart your shell";
|
|
4267
|
+
deps.writeStdout(`\ntip: if 'ouro' is not found, run: ${sourceCmd}`);
|
|
4268
|
+
}
|
|
4269
|
+
/* v8 ignore stop */
|
|
4270
|
+
return `clone complete: ${agentName}`;
|
|
4271
|
+
}
|
|
4272
|
+
const daemonCommand = toDaemonCommand(command);
|
|
4273
|
+
let response;
|
|
4274
|
+
try {
|
|
4275
|
+
response = await deps.sendCommand(deps.socketPath, daemonCommand);
|
|
4276
|
+
}
|
|
4277
|
+
catch (error) {
|
|
4278
|
+
if (command.kind === "message.send") {
|
|
4279
|
+
const pendingPath = deps.fallbackPendingMessage(command);
|
|
4280
|
+
const message = `daemon unavailable; queued message fallback at ${pendingPath}`;
|
|
4281
|
+
deps.writeStdout(message);
|
|
4282
|
+
return message;
|
|
4283
|
+
}
|
|
4284
|
+
if (command.kind === "daemon.status" && (0, cli_render_1.isDaemonUnavailableError)(error)) {
|
|
4285
|
+
const message = (0, cli_render_1.daemonUnavailableStatusOutput)(deps.socketPath, deps.healthFilePath);
|
|
4286
|
+
deps.writeStdout(message);
|
|
4287
|
+
return message;
|
|
4288
|
+
}
|
|
4289
|
+
if (command.kind === "daemon.stop" && (0, cli_render_1.isDaemonUnavailableError)(error)) {
|
|
4290
|
+
const message = "daemon not running";
|
|
4291
|
+
deps.writeStdout(message);
|
|
4292
|
+
return message;
|
|
4293
|
+
}
|
|
4294
|
+
throw error;
|
|
4295
|
+
}
|
|
4296
|
+
const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
|
|
4297
|
+
const message = command.kind === "daemon.status"
|
|
4298
|
+
? (0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage)
|
|
4299
|
+
: fallbackMessage;
|
|
4300
|
+
deps.writeStdout(message);
|
|
4301
|
+
return message;
|
|
4302
|
+
}
|