@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.200
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 +187 -191
- package/SerpentGuide.ouro/agent.json +82 -0
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +6 -3
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +2 -2
- package/assets/ouroboros.png +0 -0
- package/changelog.json +1248 -0
- package/dist/heart/active-work.js +832 -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/cares.js +179 -0
- package/dist/heart/commitments.js +111 -0
- package/dist/heart/config.js +114 -28
- package/dist/heart/core.js +689 -120
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-config-check.js +149 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/agent-service.js +360 -0
- package/dist/heart/daemon/auth-flow.js +477 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +2897 -193
- package/dist/heart/daemon/daemon-entry.js +194 -8
- package/dist/heart/daemon/daemon-health.js +136 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +231 -0
- package/dist/heart/daemon/daemon-tombstone.js +123 -0
- package/dist/heart/daemon/daemon.js +488 -13
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +369 -0
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +32 -122
- package/dist/heart/daemon/hatch-specialist.js +7 -2
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +206 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/launchd.js +165 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/mcp-server.js +591 -0
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
- package/dist/heart/daemon/ouro-path-installer.js +221 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/outlook-http.js +439 -0
- package/dist/heart/daemon/outlook-read.js +1618 -0
- package/dist/heart/daemon/outlook-render.js +1032 -0
- package/dist/heart/daemon/outlook-types.js +7 -0
- package/dist/heart/daemon/outlook-view.js +194 -0
- package/dist/heart/daemon/process-manager.js +94 -1
- package/dist/heart/daemon/run-hooks.js +39 -0
- package/dist/heart/daemon/runtime-logging.js +61 -14
- package/dist/heart/daemon/runtime-metadata.js +219 -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 +314 -0
- 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 +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +101 -0
- package/dist/heart/daemon/specialist-tools.js +283 -0
- package/dist/heart/daemon/staged-restart.js +138 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +220 -52
- package/dist/heart/intentions.js +173 -0
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/migrate-config.js +127 -0
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +225 -0
- package/dist/heart/presence.js +185 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +178 -0
- package/dist/heart/providers/anthropic-token.js +163 -0
- package/dist/heart/providers/anthropic.js +199 -36
- package/dist/heart/providers/azure.js +116 -10
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +51 -16
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/session-activity.js +172 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/start-of-turn-packet.js +232 -0
- package/dist/heart/streaming.js +120 -33
- package/dist/heart/sync.js +217 -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-loop.js +200 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +122 -32
- package/dist/mind/bundle-manifest.js +77 -1
- package/dist/mind/context.js +125 -14
- package/dist/mind/{memory.js → diary.js} +69 -56
- package/dist/mind/episodes.js +119 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +64 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +36 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +9 -1
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/obligations.js +138 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +861 -105
- package/dist/mind/scrutiny.js +173 -0
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/audit.js +1 -1
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +63 -3
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/outlook-ui/assets/index-Ck8agNeO.js +61 -0
- package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
- package/dist/outlook-ui/index.html +15 -0
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/ado-semantic.js +11 -5
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +330 -0
- package/dist/repertoire/coding/feedback.js +301 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +210 -4
- package/dist/repertoire/coding/spawner.js +58 -12
- package/dist/repertoire/coding/tools.js +209 -7
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +291 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +251 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +14 -23
- package/dist/repertoire/tasks/board.js +43 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +28 -10
- 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 +11 -28
- package/dist/repertoire/tools-base.js +1360 -271
- package/dist/repertoire/tools-bluebubbles.js +94 -0
- package/dist/repertoire/tools-github.js +1 -6
- package/dist/repertoire/tools-teams.js +65 -59
- package/dist/repertoire/tools.js +318 -91
- 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 +97 -0
- package/dist/senses/bluebubbles-client.js +532 -0
- package/dist/senses/bluebubbles-entry.js +13 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +261 -0
- package/dist/senses/bluebubbles-mutation-log.js +116 -0
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +1327 -0
- package/dist/senses/cli/adapter.js +274 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +349 -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 +198 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +1 -1
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +687 -262
- package/dist/senses/commands.js +65 -1
- 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 +592 -84
- package/dist/senses/pipeline.js +693 -0
- package/dist/senses/shared-turn.js +196 -0
- package/dist/senses/surface-tool.js +95 -0
- package/dist/senses/teams.js +817 -151
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +24 -7
- package/skills/configure-dev-tools.md +81 -0
- package/subagents/README.md +4 -70
- package/AdoptionSpecialist.ouro/agent.json +0 -20
- package/dist/heart/daemon/subagent-installer.js +0 -125
- package/dist/inner-worker-entry.js +0 -4
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
- /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/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-serpent.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
|
@@ -34,47 +34,401 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
37
|
+
exports.listGithubCopilotModels = listGithubCopilotModels;
|
|
38
|
+
exports.pingGithubCopilotModel = pingGithubCopilotModel;
|
|
37
39
|
exports.parseOuroCommand = parseOuroCommand;
|
|
40
|
+
exports.readFirstBundleMetaVersion = readFirstBundleMetaVersion;
|
|
41
|
+
exports.discoverExistingCredentials = discoverExistingCredentials;
|
|
38
42
|
exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
|
|
39
43
|
exports.runOuroCli = runOuroCli;
|
|
40
44
|
const child_process_1 = require("child_process");
|
|
45
|
+
const crypto_1 = require("crypto");
|
|
41
46
|
const fs = __importStar(require("fs"));
|
|
42
|
-
const net = __importStar(require("net"));
|
|
43
47
|
const os = __importStar(require("os"));
|
|
44
48
|
const path = __importStar(require("path"));
|
|
49
|
+
const semver = __importStar(require("semver"));
|
|
45
50
|
const identity_1 = require("../identity");
|
|
46
51
|
const runtime_1 = require("../../nerves/runtime");
|
|
47
52
|
const store_file_1 = require("../../mind/friends/store-file");
|
|
48
53
|
const types_1 = require("../../mind/friends/types");
|
|
49
54
|
const ouro_uti_1 = require("./ouro-uti");
|
|
50
|
-
const
|
|
55
|
+
const ouro_path_installer_1 = require("./ouro-path-installer");
|
|
56
|
+
const ouro_version_manager_1 = require("./ouro-version-manager");
|
|
57
|
+
const skill_management_installer_1 = require("./skill-management-installer");
|
|
51
58
|
const hatch_flow_1 = require("./hatch-flow");
|
|
59
|
+
const specialist_orchestrator_1 = require("./specialist-orchestrator");
|
|
60
|
+
const specialist_prompt_1 = require("./specialist-prompt");
|
|
61
|
+
const specialist_tools_1 = require("./specialist-tools");
|
|
62
|
+
const runtime_metadata_1 = require("./runtime-metadata");
|
|
63
|
+
const runtime_mode_1 = require("./runtime-mode");
|
|
64
|
+
const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
|
|
65
|
+
const agent_discovery_1 = require("./agent-discovery");
|
|
66
|
+
const update_hooks_1 = require("./update-hooks");
|
|
67
|
+
const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
68
|
+
const agent_config_v2_1 = require("./hooks/agent-config-v2");
|
|
69
|
+
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
70
|
+
const tasks_1 = require("../../repertoire/tasks");
|
|
71
|
+
const thoughts_1 = require("./thoughts");
|
|
72
|
+
const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
|
|
73
|
+
const launchd_1 = require("./launchd");
|
|
74
|
+
const socket_client_1 = require("./socket-client");
|
|
75
|
+
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
76
|
+
const daemon_health_1 = require("./daemon-health");
|
|
77
|
+
const session_activity_1 = require("../session-activity");
|
|
78
|
+
const auth_flow_1 = require("./auth-flow");
|
|
79
|
+
function stringField(value) {
|
|
80
|
+
return typeof value === "string" ? value : null;
|
|
81
|
+
}
|
|
82
|
+
function numberField(value) {
|
|
83
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
84
|
+
}
|
|
85
|
+
function booleanField(value) {
|
|
86
|
+
return typeof value === "boolean" ? value : null;
|
|
87
|
+
}
|
|
88
|
+
function parseStatusPayload(data) {
|
|
89
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
90
|
+
return null;
|
|
91
|
+
const raw = data;
|
|
92
|
+
const overview = raw.overview;
|
|
93
|
+
const senses = raw.senses;
|
|
94
|
+
const workers = raw.workers;
|
|
95
|
+
if (!overview || typeof overview !== "object" || Array.isArray(overview))
|
|
96
|
+
return null;
|
|
97
|
+
if (!Array.isArray(senses) || !Array.isArray(workers))
|
|
98
|
+
return null;
|
|
99
|
+
const parsedOverview = {
|
|
100
|
+
daemon: stringField(overview.daemon) ?? "unknown",
|
|
101
|
+
health: stringField(overview.health) ?? "unknown",
|
|
102
|
+
socketPath: stringField(overview.socketPath) ?? "unknown",
|
|
103
|
+
outlookUrl: stringField(overview.outlookUrl) ?? "unavailable",
|
|
104
|
+
version: stringField(overview.version) ?? "unknown",
|
|
105
|
+
lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
|
|
106
|
+
repoRoot: stringField(overview.repoRoot) ?? "unknown",
|
|
107
|
+
configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
|
|
108
|
+
workerCount: numberField(overview.workerCount) ?? 0,
|
|
109
|
+
senseCount: numberField(overview.senseCount) ?? 0,
|
|
110
|
+
entryPath: stringField(overview.entryPath) ?? "unknown",
|
|
111
|
+
mode: stringField(overview.mode) ?? "unknown",
|
|
112
|
+
};
|
|
113
|
+
const parsedSenses = senses.map((entry) => {
|
|
114
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
115
|
+
return null;
|
|
116
|
+
const row = entry;
|
|
117
|
+
const agent = stringField(row.agent);
|
|
118
|
+
const sense = stringField(row.sense);
|
|
119
|
+
const status = stringField(row.status);
|
|
120
|
+
const detail = stringField(row.detail);
|
|
121
|
+
const enabled = booleanField(row.enabled);
|
|
122
|
+
if (!agent || !sense || !status || detail === null || enabled === null)
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
agent,
|
|
126
|
+
sense,
|
|
127
|
+
label: stringField(row.label) ?? undefined,
|
|
128
|
+
enabled,
|
|
129
|
+
status,
|
|
130
|
+
detail,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
const parsedWorkers = workers.map((entry) => {
|
|
134
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
135
|
+
return null;
|
|
136
|
+
const row = entry;
|
|
137
|
+
const agent = stringField(row.agent);
|
|
138
|
+
const worker = stringField(row.worker);
|
|
139
|
+
const status = stringField(row.status);
|
|
140
|
+
const restartCount = numberField(row.restartCount);
|
|
141
|
+
const hasPid = Object.prototype.hasOwnProperty.call(row, "pid");
|
|
142
|
+
const pid = row.pid === null ? null : numberField(row.pid);
|
|
143
|
+
const pidInvalid = !hasPid || (row.pid !== null && pid === null);
|
|
144
|
+
if (!agent || !worker || !status || restartCount === null || pidInvalid)
|
|
145
|
+
return null;
|
|
146
|
+
return {
|
|
147
|
+
agent,
|
|
148
|
+
worker,
|
|
149
|
+
status,
|
|
150
|
+
pid,
|
|
151
|
+
restartCount,
|
|
152
|
+
lastExitCode: numberField(row.lastExitCode) ?? null,
|
|
153
|
+
lastSignal: stringField(row.lastSignal) ?? null,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
if (parsedSenses.some((row) => row === null) || parsedWorkers.some((row) => row === null))
|
|
157
|
+
return null;
|
|
158
|
+
return {
|
|
159
|
+
overview: parsedOverview,
|
|
160
|
+
senses: parsedSenses,
|
|
161
|
+
workers: parsedWorkers,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function humanizeSenseName(sense, label) {
|
|
165
|
+
if (label)
|
|
166
|
+
return label;
|
|
167
|
+
if (sense === "cli")
|
|
168
|
+
return "CLI";
|
|
169
|
+
if (sense === "bluebubbles")
|
|
170
|
+
return "BlueBubbles";
|
|
171
|
+
if (sense === "teams")
|
|
172
|
+
return "Teams";
|
|
173
|
+
return sense;
|
|
174
|
+
}
|
|
175
|
+
function formatTable(headers, rows) {
|
|
176
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
|
|
177
|
+
const renderRow = (row) => `| ${row.map((cell, index) => (index === row.length - 1
|
|
178
|
+
? cell
|
|
179
|
+
: cell.padEnd(widths[index]))).join(" | ")} |`;
|
|
180
|
+
const divider = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
|
|
181
|
+
return [
|
|
182
|
+
renderRow(headers),
|
|
183
|
+
divider,
|
|
184
|
+
...rows.map(renderRow),
|
|
185
|
+
].join("\n");
|
|
186
|
+
}
|
|
187
|
+
function formatDaemonStatusOutput(response, fallback) {
|
|
188
|
+
const payload = parseStatusPayload(response.data);
|
|
189
|
+
if (!payload)
|
|
190
|
+
return fallback;
|
|
191
|
+
const overviewRows = [
|
|
192
|
+
["Daemon", payload.overview.daemon],
|
|
193
|
+
["Socket", payload.overview.socketPath],
|
|
194
|
+
["Version", payload.overview.version],
|
|
195
|
+
["Last Updated", payload.overview.lastUpdated],
|
|
196
|
+
["Outlook", payload.overview.outlookUrl],
|
|
197
|
+
["Entry Path", payload.overview.entryPath],
|
|
198
|
+
["Mode", payload.overview.mode],
|
|
199
|
+
["Workers", String(payload.overview.workerCount)],
|
|
200
|
+
["Senses", String(payload.overview.senseCount)],
|
|
201
|
+
["Health", payload.overview.health],
|
|
202
|
+
];
|
|
203
|
+
const senseRows = payload.senses.map((row) => [
|
|
204
|
+
row.agent,
|
|
205
|
+
humanizeSenseName(row.sense, row.label),
|
|
206
|
+
row.enabled ? "ON" : "OFF",
|
|
207
|
+
row.status,
|
|
208
|
+
row.detail,
|
|
209
|
+
]);
|
|
210
|
+
const workerRows = payload.workers.map((row) => {
|
|
211
|
+
/* v8 ignore start — exit info branches tested via daemon-crash-context; v8 misreports conditional chains @preserve */
|
|
212
|
+
let exitInfo = "n/a";
|
|
213
|
+
if (row.lastExitCode !== null)
|
|
214
|
+
exitInfo = `code=${row.lastExitCode}`;
|
|
215
|
+
if (row.lastSignal !== null)
|
|
216
|
+
exitInfo = row.lastExitCode !== null ? `code=${row.lastExitCode} sig=${row.lastSignal}` : `sig=${row.lastSignal}`;
|
|
217
|
+
/* v8 ignore stop */
|
|
218
|
+
return [
|
|
219
|
+
row.agent,
|
|
220
|
+
row.worker,
|
|
221
|
+
row.status,
|
|
222
|
+
row.pid === null ? "n/a" : String(row.pid),
|
|
223
|
+
String(row.restartCount),
|
|
224
|
+
exitInfo,
|
|
225
|
+
];
|
|
226
|
+
});
|
|
227
|
+
return [
|
|
228
|
+
"Overview",
|
|
229
|
+
formatTable(["Item", "Value"], overviewRows),
|
|
230
|
+
"",
|
|
231
|
+
"Senses",
|
|
232
|
+
formatTable(["Agent", "Sense", "Enabled", "State", "Detail"], senseRows),
|
|
233
|
+
"",
|
|
234
|
+
"Workers",
|
|
235
|
+
formatTable(["Agent", "Worker", "State", "PID", "Restarts", "Last Exit"], workerRows),
|
|
236
|
+
].join("\n");
|
|
237
|
+
}
|
|
52
238
|
async function ensureDaemonRunning(deps) {
|
|
53
239
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
54
240
|
if (alive) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
241
|
+
const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
242
|
+
let runningRuntimePromise = null;
|
|
243
|
+
const fetchRunningRuntimeMetadata = async () => {
|
|
244
|
+
runningRuntimePromise ??= (async () => {
|
|
245
|
+
const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
|
|
246
|
+
const payload = parseStatusPayload(status.data);
|
|
247
|
+
return {
|
|
248
|
+
version: payload?.overview.version ?? "unknown",
|
|
249
|
+
lastUpdated: payload?.overview.lastUpdated ?? "unknown",
|
|
250
|
+
repoRoot: payload?.overview.repoRoot ?? "unknown",
|
|
251
|
+
configFingerprint: payload?.overview.configFingerprint ?? "unknown",
|
|
252
|
+
};
|
|
253
|
+
})();
|
|
254
|
+
return runningRuntimePromise;
|
|
58
255
|
};
|
|
256
|
+
return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
|
|
257
|
+
socketPath: deps.socketPath,
|
|
258
|
+
localVersion: localRuntime.version,
|
|
259
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
260
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
261
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
262
|
+
fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
|
|
263
|
+
fetchRunningRuntimeMetadata,
|
|
264
|
+
stopDaemon: async () => {
|
|
265
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
266
|
+
},
|
|
267
|
+
cleanupStaleSocket: deps.cleanupStaleSocket,
|
|
268
|
+
startDaemonProcess: deps.startDaemonProcess,
|
|
269
|
+
checkSocketAlive: deps.checkSocketAlive,
|
|
270
|
+
});
|
|
59
271
|
}
|
|
60
272
|
deps.cleanupStaleSocket(deps.socketPath);
|
|
61
273
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
274
|
+
const pid = started.pid ?? "unknown";
|
|
275
|
+
// Verify the daemon actually comes up before reporting success
|
|
276
|
+
const verified = await verifyDaemonAlive(deps.checkSocketAlive, deps.socketPath);
|
|
277
|
+
/* v8 ignore start -- daemon liveness failure: requires real daemon crash timing @preserve */
|
|
278
|
+
if (!verified) {
|
|
279
|
+
return {
|
|
280
|
+
alreadyRunning: false,
|
|
281
|
+
message: `daemon spawned (pid ${pid}) but failed to respond within 10s — check \`ouro status\` or daemon logs`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/* v8 ignore stop */
|
|
62
285
|
return {
|
|
63
286
|
alreadyRunning: false,
|
|
64
|
-
message: `daemon started (pid ${
|
|
287
|
+
message: `daemon started (pid ${pid})`,
|
|
65
288
|
};
|
|
66
289
|
}
|
|
290
|
+
/* v8 ignore start -- daemon liveness poll: real socket timing untestable in vitest @preserve */
|
|
291
|
+
async function verifyDaemonAlive(checkSocketAlive, socketPath, maxWaitMs = 10_000, pollIntervalMs = 500) {
|
|
292
|
+
const deadline = Date.now() + maxWaitMs;
|
|
293
|
+
while (Date.now() < deadline) {
|
|
294
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
295
|
+
if (await checkSocketAlive(socketPath))
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
/* v8 ignore stop */
|
|
301
|
+
/**
|
|
302
|
+
* Extract `--agent <name>` from an args array, returning the agent name and
|
|
303
|
+
* the remaining args with the flag pair removed.
|
|
304
|
+
*/
|
|
305
|
+
function extractAgentFlag(args) {
|
|
306
|
+
const idx = args.indexOf("--agent");
|
|
307
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
308
|
+
return { rest: args };
|
|
309
|
+
const agent = args[idx + 1];
|
|
310
|
+
const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
|
|
311
|
+
return { agent, rest };
|
|
312
|
+
}
|
|
67
313
|
function usage() {
|
|
68
314
|
return [
|
|
69
315
|
"Usage:",
|
|
70
316
|
" ouro [up]",
|
|
71
|
-
" ouro
|
|
317
|
+
" ouro dev [--repo-path <path>] [--clone [--clone-path <path>]]",
|
|
318
|
+
" ouro stop|down|status|logs|hatch",
|
|
319
|
+
" ouro outlook [--json]",
|
|
320
|
+
" ouro -v|--version",
|
|
321
|
+
" ouro config model --agent <name> <model-name>",
|
|
322
|
+
" ouro config models --agent <name>",
|
|
323
|
+
" ouro auth --agent <name> [--provider <provider>]",
|
|
324
|
+
" ouro auth verify --agent <name> [--provider <provider>]",
|
|
325
|
+
" ouro auth switch --agent <name> --provider <provider>",
|
|
72
326
|
" ouro chat <agent>",
|
|
73
327
|
" ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
|
|
74
328
|
" ouro poke <agent> --task <task-id>",
|
|
329
|
+
" ouro poke <agent> --habit <name>",
|
|
330
|
+
" ouro habit list [--agent <name>]",
|
|
331
|
+
" ouro habit create [--agent <name>] <name> [--cadence <interval>]",
|
|
75
332
|
" ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
|
|
333
|
+
" ouro task board [<status>] [--agent <name>]",
|
|
334
|
+
" ouro task create <title> [--type <type>] [--agent <name>]",
|
|
335
|
+
" ouro task update <id> <status> [--agent <name>]",
|
|
336
|
+
" ouro task show <id> [--agent <name>]",
|
|
337
|
+
" ouro task fix [--safe|--all] [<id> [--option <N>]] [--agent <name>]",
|
|
338
|
+
" ouro task actionable|deps|sessions [--agent <name>]",
|
|
339
|
+
" ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>] [--category <category>] [--agent <name>]",
|
|
340
|
+
" ouro friend list [--agent <name>]",
|
|
341
|
+
" ouro friend show <id> [--agent <name>]",
|
|
342
|
+
" ouro friend create --name <name> [--trust <level>] [--agent <name>]",
|
|
343
|
+
" ouro friend update <id> --trust <level> [--agent <name>]",
|
|
344
|
+
" ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
|
|
345
|
+
" ouro inner [--agent <name>]",
|
|
346
|
+
" ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
347
|
+
" ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
348
|
+
" ouro whoami [--agent <name>]",
|
|
349
|
+
" ouro session list [--agent <name>]",
|
|
350
|
+
" ouro mcp list",
|
|
351
|
+
" ouro mcp call <server> <tool> [--args '{...}']",
|
|
352
|
+
" ouro rollback [<version>]",
|
|
353
|
+
" ouro versions",
|
|
76
354
|
].join("\n");
|
|
77
355
|
}
|
|
356
|
+
function formatVersionOutput() {
|
|
357
|
+
const version = (0, runtime_metadata_1.getRuntimeMetadata)().version;
|
|
358
|
+
const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)());
|
|
359
|
+
/* v8 ignore start — cosmetic display toggle; dev mode always true in test env */
|
|
360
|
+
return mode === "dev" ? `${version} (dev)` : version;
|
|
361
|
+
/* v8 ignore stop */
|
|
362
|
+
}
|
|
363
|
+
function buildStoppedStatusPayload(socketPath) {
|
|
364
|
+
const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
365
|
+
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
366
|
+
return {
|
|
367
|
+
overview: {
|
|
368
|
+
daemon: "stopped",
|
|
369
|
+
health: "warn",
|
|
370
|
+
socketPath,
|
|
371
|
+
outlookUrl: "unavailable",
|
|
372
|
+
version: metadata.version,
|
|
373
|
+
lastUpdated: metadata.lastUpdated,
|
|
374
|
+
repoRoot: metadata.repoRoot,
|
|
375
|
+
configFingerprint: metadata.configFingerprint,
|
|
376
|
+
workerCount: 0,
|
|
377
|
+
senseCount: 0,
|
|
378
|
+
entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
|
|
379
|
+
mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
|
|
380
|
+
},
|
|
381
|
+
senses: [],
|
|
382
|
+
workers: [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
|
|
386
|
+
/* v8 ignore start — tombstone read tested in daemon-status-tombstone.test; branch misreported @preserve */
|
|
387
|
+
const tombstone = (0, daemon_tombstone_1.readDaemonTombstone)();
|
|
388
|
+
const deathLine = tombstone
|
|
389
|
+
? `Last death: ${tombstone.timestamp} -- ${tombstone.reason}: ${tombstone.message}`
|
|
390
|
+
: null;
|
|
391
|
+
/* v8 ignore stop */
|
|
392
|
+
const lines = [
|
|
393
|
+
formatDaemonStatusOutput({
|
|
394
|
+
ok: true,
|
|
395
|
+
summary: "daemon not running",
|
|
396
|
+
data: buildStoppedStatusPayload(socketPath),
|
|
397
|
+
}, "daemon not running"),
|
|
398
|
+
"",
|
|
399
|
+
];
|
|
400
|
+
/* v8 ignore start — tombstone presence requires real daemon crash @preserve */
|
|
401
|
+
if (deathLine) {
|
|
402
|
+
lines.push(deathLine);
|
|
403
|
+
lines.push("");
|
|
404
|
+
/* v8 ignore stop */
|
|
405
|
+
}
|
|
406
|
+
// Read health file for last-known state (best-effort)
|
|
407
|
+
const resolvedHealthPath = healthFilePath ?? (0, daemon_health_1.getDefaultHealthPath)();
|
|
408
|
+
const health = (0, daemon_health_1.readHealth)(resolvedHealthPath);
|
|
409
|
+
if (health) {
|
|
410
|
+
lines.push(`Last known status: ${health.status} (pid ${health.pid}, uptime ${health.uptimeSeconds}s)`);
|
|
411
|
+
if (health.safeMode?.active) {
|
|
412
|
+
lines.push(`SAFE MODE: ${health.safeMode.reason}`);
|
|
413
|
+
}
|
|
414
|
+
if (health.degraded.length > 0) {
|
|
415
|
+
lines.push("");
|
|
416
|
+
lines.push("Degraded:");
|
|
417
|
+
for (const d of health.degraded) {
|
|
418
|
+
lines.push(` ${d.component}: ${d.reason} (since ${d.since})`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
lines.push("");
|
|
422
|
+
}
|
|
423
|
+
lines.push("daemon not running; run `ouro up`");
|
|
424
|
+
return lines.join("\n");
|
|
425
|
+
}
|
|
426
|
+
function isDaemonUnavailableError(error) {
|
|
427
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
428
|
+
? String(error.code ?? "")
|
|
429
|
+
: "";
|
|
430
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
431
|
+
}
|
|
78
432
|
function parseMessageCommand(args) {
|
|
79
433
|
let to;
|
|
80
434
|
let sessionId;
|
|
@@ -116,17 +470,56 @@ function parsePokeCommand(args) {
|
|
|
116
470
|
if (!agent)
|
|
117
471
|
throw new Error(`Usage\n${usage()}`);
|
|
118
472
|
let taskId;
|
|
473
|
+
let habitName;
|
|
119
474
|
for (let i = 1; i < args.length; i += 1) {
|
|
120
475
|
if (args[i] === "--task") {
|
|
121
476
|
taskId = args[i + 1];
|
|
122
477
|
i += 1;
|
|
123
478
|
}
|
|
479
|
+
if (args[i] === "--habit") {
|
|
480
|
+
habitName = args[i + 1];
|
|
481
|
+
i += 1;
|
|
482
|
+
}
|
|
124
483
|
}
|
|
484
|
+
// --habit takes priority over --task
|
|
485
|
+
if (habitName)
|
|
486
|
+
return { kind: "habit.poke", agent, habitName };
|
|
125
487
|
if (!taskId)
|
|
126
488
|
throw new Error(`Usage\n${usage()}`);
|
|
127
489
|
return { kind: "task.poke", agent, taskId };
|
|
128
490
|
}
|
|
129
|
-
function
|
|
491
|
+
function parseHabitCommand(args) {
|
|
492
|
+
const { agent, rest } = extractAgentFlag(args);
|
|
493
|
+
const sub = rest[0];
|
|
494
|
+
if (sub === "list") {
|
|
495
|
+
return { kind: "habit.list", ...(agent ? { agent } : {}) };
|
|
496
|
+
}
|
|
497
|
+
if (sub === "create") {
|
|
498
|
+
const nameArgs = rest.slice(1);
|
|
499
|
+
let name;
|
|
500
|
+
let cadence;
|
|
501
|
+
const positional = [];
|
|
502
|
+
for (let i = 0; i < nameArgs.length; i++) {
|
|
503
|
+
if (nameArgs[i] === "--cadence" && nameArgs[i + 1]) {
|
|
504
|
+
cadence = nameArgs[++i];
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
/* v8 ignore start -- defensive: --agent already extracted by extractAgentFlag; guard prevents regression if parsing flow changes @preserve */
|
|
508
|
+
if (nameArgs[i] === "--agent" && nameArgs[i + 1]) {
|
|
509
|
+
i++; // skip --agent value (already extracted)
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
/* v8 ignore stop */
|
|
513
|
+
positional.push(nameArgs[i]);
|
|
514
|
+
}
|
|
515
|
+
name = positional[0];
|
|
516
|
+
if (!name)
|
|
517
|
+
throw new Error(`Usage\n${usage()}`);
|
|
518
|
+
return { kind: "habit.create", name, ...(agent ? { agent } : {}), ...(cadence ? { cadence } : {}) };
|
|
519
|
+
}
|
|
520
|
+
throw new Error(`Usage\n${usage()}`);
|
|
521
|
+
}
|
|
522
|
+
function parseLinkCommand(args, kind = "friend.link") {
|
|
130
523
|
const agent = args[0];
|
|
131
524
|
if (!agent)
|
|
132
525
|
throw new Error(`Usage\n${usage()}`);
|
|
@@ -158,7 +551,7 @@ function parseLinkCommand(args) {
|
|
|
158
551
|
throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation.`);
|
|
159
552
|
}
|
|
160
553
|
return {
|
|
161
|
-
kind
|
|
554
|
+
kind,
|
|
162
555
|
agent,
|
|
163
556
|
friendId,
|
|
164
557
|
provider: providerRaw,
|
|
@@ -166,7 +559,102 @@ function parseLinkCommand(args) {
|
|
|
166
559
|
};
|
|
167
560
|
}
|
|
168
561
|
function isAgentProvider(value) {
|
|
169
|
-
return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex";
|
|
562
|
+
return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
|
|
563
|
+
}
|
|
564
|
+
/* v8 ignore start -- hasStoredCredentials: per-provider branches tested via auth switch tests @preserve */
|
|
565
|
+
function hasStoredCredentials(provider, providerSecrets) {
|
|
566
|
+
if (provider === "anthropic")
|
|
567
|
+
return !!providerSecrets.setupToken;
|
|
568
|
+
if (provider === "openai-codex")
|
|
569
|
+
return !!providerSecrets.oauthAccessToken;
|
|
570
|
+
if (provider === "github-copilot")
|
|
571
|
+
return !!providerSecrets.githubToken;
|
|
572
|
+
if (provider === "minimax")
|
|
573
|
+
return !!providerSecrets.apiKey;
|
|
574
|
+
// azure
|
|
575
|
+
return !!providerSecrets.endpoint && !!providerSecrets.apiKey;
|
|
576
|
+
}
|
|
577
|
+
/* v8 ignore stop */
|
|
578
|
+
/* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
|
|
579
|
+
async function verifyProviderCredentials(provider, providers) {
|
|
580
|
+
const config = providers[provider];
|
|
581
|
+
if (!config)
|
|
582
|
+
return "not configured";
|
|
583
|
+
try {
|
|
584
|
+
const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../../heart/provider-ping")));
|
|
585
|
+
const result = await pingProvider(provider, config);
|
|
586
|
+
return result.ok ? "ok" : `failed (${result.message})`;
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
return `failed (${error instanceof Error ? error.message : String(error)})`;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
|
|
593
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
594
|
+
const response = await fetchImpl(url, {
|
|
595
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
596
|
+
});
|
|
597
|
+
if (!response.ok) {
|
|
598
|
+
throw new Error(`model listing failed (HTTP ${response.status})`);
|
|
599
|
+
}
|
|
600
|
+
const body = await response.json();
|
|
601
|
+
/* v8 ignore start -- response shape handling: tested via config-models.test.ts @preserve */
|
|
602
|
+
const items = Array.isArray(body) ? body : (body?.data ?? []);
|
|
603
|
+
return items.map((item) => {
|
|
604
|
+
const rec = item;
|
|
605
|
+
const capabilities = Array.isArray(rec.capabilities)
|
|
606
|
+
? rec.capabilities.filter((c) => typeof c === "string")
|
|
607
|
+
: undefined;
|
|
608
|
+
return {
|
|
609
|
+
id: String(rec.id ?? rec.name ?? ""),
|
|
610
|
+
name: String(rec.name ?? rec.id ?? ""),
|
|
611
|
+
...(capabilities ? { capabilities } : {}),
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
/* v8 ignore stop */
|
|
615
|
+
}
|
|
616
|
+
async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
|
|
617
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
618
|
+
const isClaude = model.startsWith("claude");
|
|
619
|
+
const url = isClaude ? `${base}/chat/completions` : `${base}/responses`;
|
|
620
|
+
const body = isClaude
|
|
621
|
+
? JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 1 })
|
|
622
|
+
: JSON.stringify({ model, input: "ping", max_output_tokens: 16 });
|
|
623
|
+
try {
|
|
624
|
+
const response = await fetchImpl(url, {
|
|
625
|
+
method: "POST",
|
|
626
|
+
headers: {
|
|
627
|
+
Authorization: `Bearer ${token}`,
|
|
628
|
+
"Content-Type": "application/json",
|
|
629
|
+
},
|
|
630
|
+
body,
|
|
631
|
+
});
|
|
632
|
+
if (response.ok)
|
|
633
|
+
return { ok: true };
|
|
634
|
+
let detail = `HTTP ${response.status}`;
|
|
635
|
+
try {
|
|
636
|
+
const json = await response.json();
|
|
637
|
+
/* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
|
|
638
|
+
if (typeof json.error === "string")
|
|
639
|
+
detail = json.error;
|
|
640
|
+
else if (typeof json.error === "object" && json.error !== null) {
|
|
641
|
+
const errObj = json.error;
|
|
642
|
+
if (typeof errObj.message === "string")
|
|
643
|
+
detail = errObj.message;
|
|
644
|
+
}
|
|
645
|
+
else if (typeof json.message === "string")
|
|
646
|
+
detail = json.message;
|
|
647
|
+
/* v8 ignore stop */
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// response body not JSON — keep HTTP status
|
|
651
|
+
}
|
|
652
|
+
return { ok: false, error: detail };
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
/* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
|
|
656
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
657
|
+
}
|
|
170
658
|
}
|
|
171
659
|
function parseHatchCommand(args) {
|
|
172
660
|
let agentName;
|
|
@@ -223,7 +711,7 @@ function parseHatchCommand(args) {
|
|
|
223
711
|
}
|
|
224
712
|
}
|
|
225
713
|
if (providerRaw && !isAgentProvider(providerRaw)) {
|
|
226
|
-
throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex.");
|
|
714
|
+
throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex|github-copilot.");
|
|
227
715
|
}
|
|
228
716
|
const provider = providerRaw && isAgentProvider(providerRaw) ? providerRaw : undefined;
|
|
229
717
|
return {
|
|
@@ -235,20 +723,480 @@ function parseHatchCommand(args) {
|
|
|
235
723
|
migrationPath,
|
|
236
724
|
};
|
|
237
725
|
}
|
|
726
|
+
function parseTaskCommand(args) {
|
|
727
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
728
|
+
const [sub, ...rest] = cleaned;
|
|
729
|
+
if (!sub)
|
|
730
|
+
throw new Error(`Usage\n${usage()}`);
|
|
731
|
+
if (sub === "board") {
|
|
732
|
+
const status = rest[0];
|
|
733
|
+
return status
|
|
734
|
+
? { kind: "task.board", status, ...(agent ? { agent } : {}) }
|
|
735
|
+
: { kind: "task.board", ...(agent ? { agent } : {}) };
|
|
736
|
+
}
|
|
737
|
+
if (sub === "create") {
|
|
738
|
+
const title = rest[0];
|
|
739
|
+
if (!title)
|
|
740
|
+
throw new Error(`Usage\n${usage()}`);
|
|
741
|
+
let type;
|
|
742
|
+
for (let i = 1; i < rest.length; i++) {
|
|
743
|
+
if (rest[i] === "--type" && rest[i + 1]) {
|
|
744
|
+
type = rest[i + 1];
|
|
745
|
+
i += 1;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return type
|
|
749
|
+
? { kind: "task.create", title, type, ...(agent ? { agent } : {}) }
|
|
750
|
+
: { kind: "task.create", title, ...(agent ? { agent } : {}) };
|
|
751
|
+
}
|
|
752
|
+
if (sub === "update") {
|
|
753
|
+
const id = rest[0];
|
|
754
|
+
const status = rest[1];
|
|
755
|
+
if (!id || !status)
|
|
756
|
+
throw new Error(`Usage\n${usage()}`);
|
|
757
|
+
return { kind: "task.update", id, status, ...(agent ? { agent } : {}) };
|
|
758
|
+
}
|
|
759
|
+
if (sub === "show") {
|
|
760
|
+
const id = rest[0];
|
|
761
|
+
if (!id)
|
|
762
|
+
throw new Error(`Usage\n${usage()}`);
|
|
763
|
+
return { kind: "task.show", id, ...(agent ? { agent } : {}) };
|
|
764
|
+
}
|
|
765
|
+
if (sub === "actionable")
|
|
766
|
+
return { kind: "task.actionable", ...(agent ? { agent } : {}) };
|
|
767
|
+
if (sub === "deps")
|
|
768
|
+
return { kind: "task.deps", ...(agent ? { agent } : {}) };
|
|
769
|
+
if (sub === "sessions")
|
|
770
|
+
return { kind: "task.sessions", ...(agent ? { agent } : {}) };
|
|
771
|
+
if (sub === "fix") {
|
|
772
|
+
// fix --safe | fix --all | fix <id> [--option N] | fix (dry-run)
|
|
773
|
+
if (rest.length === 0)
|
|
774
|
+
return { kind: "task.fix", mode: "dry-run", ...(agent ? { agent } : {}) };
|
|
775
|
+
const first = rest[0];
|
|
776
|
+
if (first === "--safe" || first === "--all") {
|
|
777
|
+
return { kind: "task.fix", mode: "safe", ...(agent ? { agent } : {}) };
|
|
778
|
+
}
|
|
779
|
+
// first arg is an issue ID (contains a colon, e.g. schema-missing-kind:one-shots/foo.md)
|
|
780
|
+
const issueId = first;
|
|
781
|
+
let option;
|
|
782
|
+
for (let i = 1; i < rest.length; i++) {
|
|
783
|
+
if (rest[i] === "--option" && rest[i + 1]) {
|
|
784
|
+
option = parseInt(rest[i + 1], 10);
|
|
785
|
+
i += 1;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
kind: "task.fix",
|
|
790
|
+
mode: "single",
|
|
791
|
+
issueId,
|
|
792
|
+
...(option !== undefined ? { option } : {}),
|
|
793
|
+
...(agent ? { agent } : {}),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
throw new Error(`Usage\n${usage()}`);
|
|
797
|
+
}
|
|
798
|
+
function extractFacingFlag(args) {
|
|
799
|
+
const idx = args.indexOf("--facing");
|
|
800
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
801
|
+
return { rest: args };
|
|
802
|
+
const value = args[idx + 1];
|
|
803
|
+
if (value !== "human" && value !== "agent") {
|
|
804
|
+
throw new Error(`--facing must be 'human' or 'agent'`);
|
|
805
|
+
}
|
|
806
|
+
const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
|
|
807
|
+
return { facing: value, rest };
|
|
808
|
+
}
|
|
809
|
+
function parseAuthCommand(args) {
|
|
810
|
+
const first = args[0];
|
|
811
|
+
// Support both positional (`auth switch`) and flag (`auth --switch`) forms
|
|
812
|
+
if (first === "verify" || first === "switch" || first === "--verify" || first === "--switch") {
|
|
813
|
+
const subcommand = first.replace(/^--/, "");
|
|
814
|
+
const { agent, rest: afterAgent } = extractAgentFlag(args.slice(1));
|
|
815
|
+
const { facing, rest } = extractFacingFlag(afterAgent);
|
|
816
|
+
let provider;
|
|
817
|
+
/* v8 ignore start -- provider flag parsing: branches tested via CLI parsing tests @preserve */
|
|
818
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
819
|
+
if (rest[i] === "--provider") {
|
|
820
|
+
const value = rest[i + 1];
|
|
821
|
+
if (!isAgentProvider(value))
|
|
822
|
+
throw new Error(`Usage\n${usage()}`);
|
|
823
|
+
provider = value;
|
|
824
|
+
i += 1;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/* v8 ignore stop */
|
|
829
|
+
/* v8 ignore next -- defensive: agent always provided in tests @preserve */
|
|
830
|
+
if (!agent)
|
|
831
|
+
throw new Error(`Usage\n${usage()}`);
|
|
832
|
+
if (subcommand === "switch") {
|
|
833
|
+
if (!provider)
|
|
834
|
+
throw new Error(`auth switch requires --provider.\n${usage()}`);
|
|
835
|
+
return facing ? { kind: "auth.switch", agent, provider, facing } : { kind: "auth.switch", agent, provider };
|
|
836
|
+
}
|
|
837
|
+
return provider ? { kind: "auth.verify", agent, provider } : { kind: "auth.verify", agent };
|
|
838
|
+
}
|
|
839
|
+
const { agent, rest } = extractAgentFlag(args);
|
|
840
|
+
let provider;
|
|
841
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
842
|
+
if (rest[i] === "--provider") {
|
|
843
|
+
const value = rest[i + 1];
|
|
844
|
+
if (!isAgentProvider(value))
|
|
845
|
+
throw new Error(`Usage\n${usage()}`);
|
|
846
|
+
provider = value;
|
|
847
|
+
i += 1;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (!agent) {
|
|
852
|
+
throw new Error([
|
|
853
|
+
"Usage:",
|
|
854
|
+
" ouro auth --agent <name> [--provider <provider>] Set up credentials",
|
|
855
|
+
" ouro auth verify --agent <name> [--provider <p>] Verify credentials work",
|
|
856
|
+
" ouro auth switch --agent <name> --provider <p> Switch active provider",
|
|
857
|
+
].join("\n"));
|
|
858
|
+
}
|
|
859
|
+
return provider ? { kind: "auth.run", agent, provider } : { kind: "auth.run", agent };
|
|
860
|
+
}
|
|
861
|
+
function parseReminderCommand(args) {
|
|
862
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
863
|
+
const [sub, ...rest] = cleaned;
|
|
864
|
+
if (!sub)
|
|
865
|
+
throw new Error(`Usage\n${usage()}`);
|
|
866
|
+
if (sub === "create") {
|
|
867
|
+
const title = rest[0];
|
|
868
|
+
if (!title)
|
|
869
|
+
throw new Error(`Usage\n${usage()}`);
|
|
870
|
+
let body;
|
|
871
|
+
let scheduledAt;
|
|
872
|
+
let cadence;
|
|
873
|
+
let category;
|
|
874
|
+
let requester;
|
|
875
|
+
for (let i = 1; i < rest.length; i++) {
|
|
876
|
+
if (rest[i] === "--body" && rest[i + 1]) {
|
|
877
|
+
body = rest[i + 1];
|
|
878
|
+
i += 1;
|
|
879
|
+
}
|
|
880
|
+
else if (rest[i] === "--at" && rest[i + 1]) {
|
|
881
|
+
scheduledAt = rest[i + 1];
|
|
882
|
+
i += 1;
|
|
883
|
+
}
|
|
884
|
+
else if (rest[i] === "--cadence" && rest[i + 1]) {
|
|
885
|
+
cadence = rest[i + 1];
|
|
886
|
+
i += 1;
|
|
887
|
+
}
|
|
888
|
+
else if (rest[i] === "--category" && rest[i + 1]) {
|
|
889
|
+
category = rest[i + 1];
|
|
890
|
+
i += 1;
|
|
891
|
+
}
|
|
892
|
+
else if (rest[i] === "--requester" && rest[i + 1]) {
|
|
893
|
+
requester = rest[i + 1];
|
|
894
|
+
i += 1;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (!body)
|
|
898
|
+
throw new Error(`Usage\n${usage()}`);
|
|
899
|
+
if (!scheduledAt && !cadence)
|
|
900
|
+
throw new Error(`Usage\n${usage()}`);
|
|
901
|
+
return {
|
|
902
|
+
kind: "reminder.create",
|
|
903
|
+
title,
|
|
904
|
+
body,
|
|
905
|
+
...(scheduledAt ? { scheduledAt } : {}),
|
|
906
|
+
...(cadence ? { cadence } : {}),
|
|
907
|
+
...(category ? { category } : {}),
|
|
908
|
+
...(requester ? { requester } : {}),
|
|
909
|
+
...(agent ? { agent } : {}),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
throw new Error(`Usage\n${usage()}`);
|
|
913
|
+
}
|
|
914
|
+
function parseSessionCommand(args) {
|
|
915
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
916
|
+
const [sub] = cleaned;
|
|
917
|
+
if (!sub)
|
|
918
|
+
throw new Error(`Usage\n${usage()}`);
|
|
919
|
+
if (sub === "list")
|
|
920
|
+
return { kind: "session.list", ...(agent ? { agent } : {}) };
|
|
921
|
+
throw new Error(`Usage\n${usage()}`);
|
|
922
|
+
}
|
|
923
|
+
function parseAttentionCommand(args) {
|
|
924
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
925
|
+
const sub = cleaned[0];
|
|
926
|
+
if (sub === "show" && cleaned[1]) {
|
|
927
|
+
return { kind: "attention.show", id: cleaned[1], ...(agent ? { agent } : {}) };
|
|
928
|
+
}
|
|
929
|
+
if (sub === "history") {
|
|
930
|
+
return { kind: "attention.history", ...(agent ? { agent } : {}) };
|
|
931
|
+
}
|
|
932
|
+
return { kind: "attention.list", ...(agent ? { agent } : {}) };
|
|
933
|
+
}
|
|
934
|
+
function parseThoughtsCommand(args) {
|
|
935
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
936
|
+
let last;
|
|
937
|
+
let json = false;
|
|
938
|
+
let follow = false;
|
|
939
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
940
|
+
if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
|
|
941
|
+
last = Number.parseInt(cleaned[i + 1], 10);
|
|
942
|
+
i++;
|
|
943
|
+
}
|
|
944
|
+
if (cleaned[i] === "--json")
|
|
945
|
+
json = true;
|
|
946
|
+
if (cleaned[i] === "--follow" || cleaned[i] === "-f")
|
|
947
|
+
follow = true;
|
|
948
|
+
}
|
|
949
|
+
return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
|
|
950
|
+
}
|
|
951
|
+
function parseFriendCommand(args) {
|
|
952
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
953
|
+
const [sub, ...rest] = cleaned;
|
|
954
|
+
if (!sub)
|
|
955
|
+
throw new Error(`Usage\n${usage()}`);
|
|
956
|
+
if (sub === "list")
|
|
957
|
+
return { kind: "friend.list", ...(agent ? { agent } : {}) };
|
|
958
|
+
if (sub === "show") {
|
|
959
|
+
const friendId = rest[0];
|
|
960
|
+
if (!friendId)
|
|
961
|
+
throw new Error(`Usage\n${usage()}`);
|
|
962
|
+
return { kind: "friend.show", friendId, ...(agent ? { agent } : {}) };
|
|
963
|
+
}
|
|
964
|
+
if (sub === "create") {
|
|
965
|
+
let name;
|
|
966
|
+
let trustLevel;
|
|
967
|
+
for (let i = 0; i < rest.length; i++) {
|
|
968
|
+
if (rest[i] === "--name" && rest[i + 1]) {
|
|
969
|
+
name = rest[i + 1];
|
|
970
|
+
i += 1;
|
|
971
|
+
}
|
|
972
|
+
else if (rest[i] === "--trust" && rest[i + 1]) {
|
|
973
|
+
trustLevel = rest[i + 1];
|
|
974
|
+
i += 1;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (!name)
|
|
978
|
+
throw new Error(`Usage\n${usage()}`);
|
|
979
|
+
return {
|
|
980
|
+
kind: "friend.create",
|
|
981
|
+
name,
|
|
982
|
+
...(trustLevel ? { trustLevel } : {}),
|
|
983
|
+
...(agent ? { agent } : {}),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
if (sub === "update") {
|
|
987
|
+
const friendId = rest[0];
|
|
988
|
+
if (!friendId)
|
|
989
|
+
throw new Error(`Usage: ouro friend update <id> --trust <level>`);
|
|
990
|
+
let trustLevel;
|
|
991
|
+
/* v8 ignore start -- flag parsing loop: tested via CLI parsing tests @preserve */
|
|
992
|
+
for (let i = 1; i < rest.length; i++) {
|
|
993
|
+
if (rest[i] === "--trust" && rest[i + 1]) {
|
|
994
|
+
trustLevel = rest[i + 1];
|
|
995
|
+
i += 1;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
/* v8 ignore stop */
|
|
999
|
+
const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
|
|
1000
|
+
if (!trustLevel || !VALID_TRUST_LEVELS.has(trustLevel)) {
|
|
1001
|
+
throw new Error(`Usage: ouro friend update <id> --trust <stranger|acquaintance|friend|family>`);
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
kind: "friend.update",
|
|
1005
|
+
friendId,
|
|
1006
|
+
trustLevel: trustLevel,
|
|
1007
|
+
...(agent ? { agent } : {}),
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
if (sub === "link")
|
|
1011
|
+
return parseLinkCommand(rest, "friend.link");
|
|
1012
|
+
if (sub === "unlink")
|
|
1013
|
+
return parseLinkCommand(rest, "friend.unlink");
|
|
1014
|
+
throw new Error(`Usage\n${usage()}`);
|
|
1015
|
+
}
|
|
1016
|
+
function parseConfigCommand(args) {
|
|
1017
|
+
const { agent, rest: afterAgent } = extractAgentFlag(args);
|
|
1018
|
+
const { facing, rest: cleaned } = extractFacingFlag(afterAgent);
|
|
1019
|
+
const [sub, ...rest] = cleaned;
|
|
1020
|
+
if (!sub)
|
|
1021
|
+
throw new Error(`Usage\n${usage()}`);
|
|
1022
|
+
if (sub === "model") {
|
|
1023
|
+
if (!agent)
|
|
1024
|
+
throw new Error("--agent is required for config model");
|
|
1025
|
+
const modelName = rest[0];
|
|
1026
|
+
if (!modelName)
|
|
1027
|
+
throw new Error(`Usage: ouro config model --agent <name> <model-name>`);
|
|
1028
|
+
return facing ? { kind: "config.model", agent, modelName, facing } : { kind: "config.model", agent, modelName };
|
|
1029
|
+
}
|
|
1030
|
+
if (sub === "models") {
|
|
1031
|
+
if (!agent)
|
|
1032
|
+
throw new Error("--agent is required for config models");
|
|
1033
|
+
return { kind: "config.models", agent };
|
|
1034
|
+
}
|
|
1035
|
+
throw new Error(`Usage\n${usage()}`);
|
|
1036
|
+
}
|
|
1037
|
+
function parseMcpCommand(args) {
|
|
1038
|
+
const [sub, ...rest] = args;
|
|
1039
|
+
if (!sub)
|
|
1040
|
+
throw new Error(`Usage\n${usage()}`);
|
|
1041
|
+
if (sub === "list")
|
|
1042
|
+
return { kind: "mcp.list" };
|
|
1043
|
+
if (sub === "call") {
|
|
1044
|
+
const server = rest[0];
|
|
1045
|
+
const tool = rest[1];
|
|
1046
|
+
if (!server || !tool)
|
|
1047
|
+
throw new Error(`Usage\n${usage()}`);
|
|
1048
|
+
const argsIdx = rest.indexOf("--args");
|
|
1049
|
+
const mcpArgs = argsIdx !== -1 && rest[argsIdx + 1] ? rest[argsIdx + 1] : undefined;
|
|
1050
|
+
return { kind: "mcp.call", server, tool, ...(mcpArgs ? { args: mcpArgs } : {}) };
|
|
1051
|
+
}
|
|
1052
|
+
throw new Error(`Usage\n${usage()}`);
|
|
1053
|
+
}
|
|
1054
|
+
function parseMcpServeCommand(args) {
|
|
1055
|
+
let agent;
|
|
1056
|
+
let friendId;
|
|
1057
|
+
let socketOverride;
|
|
1058
|
+
for (let i = 0; i < args.length; i++) {
|
|
1059
|
+
if (args[i] === "--agent" && args[i + 1]) {
|
|
1060
|
+
agent = args[++i];
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (args[i] === "--friend" && args[i + 1]) {
|
|
1064
|
+
friendId = args[++i];
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (args[i] === "--socket" && args[i + 1]) {
|
|
1068
|
+
socketOverride = args[++i];
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (!agent)
|
|
1073
|
+
throw new Error("mcp-serve requires --agent <name>");
|
|
1074
|
+
return { kind: "mcp-serve", agent, ...(friendId ? { friendId } : {}), ...(socketOverride ? { socketOverride } : {}) };
|
|
1075
|
+
}
|
|
1076
|
+
function parseSetupCommand(args) {
|
|
1077
|
+
let tool;
|
|
1078
|
+
let agent;
|
|
1079
|
+
for (let i = 0; i < args.length; i++) {
|
|
1080
|
+
if (args[i] === "--tool" && args[i + 1]) {
|
|
1081
|
+
tool = args[++i];
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (args[i] === "--agent" && args[i + 1]) {
|
|
1085
|
+
agent = args[++i];
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!tool)
|
|
1090
|
+
throw new Error("setup requires --tool (claude-code | codex)");
|
|
1091
|
+
if (tool !== "claude-code" && tool !== "codex")
|
|
1092
|
+
throw new Error(`Unknown tool: ${tool}. Supported: claude-code, codex`);
|
|
1093
|
+
if (!agent)
|
|
1094
|
+
throw new Error("setup requires --agent <name>");
|
|
1095
|
+
return { kind: "setup", tool, agent };
|
|
1096
|
+
}
|
|
238
1097
|
function parseOuroCommand(args) {
|
|
239
1098
|
const [head, second] = args;
|
|
240
1099
|
if (!head)
|
|
241
1100
|
return { kind: "daemon.up" };
|
|
1101
|
+
if (head === "--agent" && second) {
|
|
1102
|
+
return parseOuroCommand(args.slice(2));
|
|
1103
|
+
}
|
|
1104
|
+
if (head === "hook") {
|
|
1105
|
+
const hookArgs = args.slice(1);
|
|
1106
|
+
let event;
|
|
1107
|
+
let hookAgent;
|
|
1108
|
+
for (let i = 0; i < hookArgs.length; i++) {
|
|
1109
|
+
if (hookArgs[i] === "--agent" && hookArgs[i + 1]) {
|
|
1110
|
+
hookAgent = hookArgs[++i];
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
/* v8 ignore start -- false branch: extra positional args after event are ignored */
|
|
1114
|
+
if (!event) {
|
|
1115
|
+
event = hookArgs[i];
|
|
1116
|
+
}
|
|
1117
|
+
/* v8 ignore stop */
|
|
1118
|
+
}
|
|
1119
|
+
if (!event)
|
|
1120
|
+
throw new Error("hook requires an event name (session-start, stop, post-tool-use)");
|
|
1121
|
+
if (!hookAgent)
|
|
1122
|
+
throw new Error("hook requires --agent <name>");
|
|
1123
|
+
return { kind: "hook", event, agent: hookAgent };
|
|
1124
|
+
}
|
|
242
1125
|
if (head === "up")
|
|
243
1126
|
return { kind: "daemon.up" };
|
|
244
|
-
if (head === "
|
|
1127
|
+
if (head === "dev") {
|
|
1128
|
+
const devArgs = args.slice(1);
|
|
1129
|
+
let repoPath;
|
|
1130
|
+
let clone = false;
|
|
1131
|
+
let clonePath;
|
|
1132
|
+
for (let i = 0; i < devArgs.length; i++) {
|
|
1133
|
+
if (devArgs[i] === "--repo-path" && devArgs[i + 1]) {
|
|
1134
|
+
repoPath = devArgs[++i];
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (devArgs[i] === "--clone") {
|
|
1138
|
+
clone = true;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (devArgs[i] === "--clone-path" && devArgs[i + 1]) {
|
|
1142
|
+
clonePath = devArgs[++i];
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return { kind: "daemon.dev", repoPath, clone, clonePath };
|
|
1147
|
+
}
|
|
1148
|
+
if (head === "rollback")
|
|
1149
|
+
return { kind: "rollback", ...(second ? { version: second } : {}) };
|
|
1150
|
+
if (head === "versions")
|
|
1151
|
+
return { kind: "versions" };
|
|
1152
|
+
if (head === "stop" || head === "down")
|
|
245
1153
|
return { kind: "daemon.stop" };
|
|
246
1154
|
if (head === "status")
|
|
247
1155
|
return { kind: "daemon.status" };
|
|
248
1156
|
if (head === "logs")
|
|
249
1157
|
return { kind: "daemon.logs" };
|
|
1158
|
+
if (head === "outlook")
|
|
1159
|
+
return { kind: "outlook", ...(args.includes("--json") ? { json: true } : {}) };
|
|
250
1160
|
if (head === "hatch")
|
|
251
1161
|
return parseHatchCommand(args.slice(1));
|
|
1162
|
+
if (head === "auth")
|
|
1163
|
+
return parseAuthCommand(args.slice(1));
|
|
1164
|
+
if (head === "task")
|
|
1165
|
+
return parseTaskCommand(args.slice(1));
|
|
1166
|
+
if (head === "reminder")
|
|
1167
|
+
return parseReminderCommand(args.slice(1));
|
|
1168
|
+
if (head === "habit")
|
|
1169
|
+
return parseHabitCommand(args.slice(1));
|
|
1170
|
+
if (head === "friend")
|
|
1171
|
+
return parseFriendCommand(args.slice(1));
|
|
1172
|
+
if (head === "config")
|
|
1173
|
+
return parseConfigCommand(args.slice(1));
|
|
1174
|
+
if (head === "mcp")
|
|
1175
|
+
return parseMcpCommand(args.slice(1));
|
|
1176
|
+
if (head === "whoami") {
|
|
1177
|
+
const { agent } = extractAgentFlag(args.slice(1));
|
|
1178
|
+
return { kind: "whoami", ...(agent ? { agent } : {}) };
|
|
1179
|
+
}
|
|
1180
|
+
if (head === "session")
|
|
1181
|
+
return parseSessionCommand(args.slice(1));
|
|
1182
|
+
if (head === "changelog") {
|
|
1183
|
+
const sliced = args.slice(1);
|
|
1184
|
+
const { agent, rest: remaining } = extractAgentFlag(sliced);
|
|
1185
|
+
let from;
|
|
1186
|
+
const fromIdx = remaining.indexOf("--from");
|
|
1187
|
+
if (fromIdx !== -1 && remaining[fromIdx + 1]) {
|
|
1188
|
+
from = remaining[fromIdx + 1];
|
|
1189
|
+
}
|
|
1190
|
+
return { kind: "changelog", ...(from ? { from } : {}), ...(agent ? { agent } : {}) };
|
|
1191
|
+
}
|
|
1192
|
+
if (head === "thoughts")
|
|
1193
|
+
return parseThoughtsCommand(args.slice(1));
|
|
1194
|
+
if (head === "attention")
|
|
1195
|
+
return parseAttentionCommand(args.slice(1));
|
|
1196
|
+
if (head === "inner") {
|
|
1197
|
+
const { agent } = extractAgentFlag(args.slice(1));
|
|
1198
|
+
return { kind: "inner.status", ...(agent ? { agent } : {}) };
|
|
1199
|
+
}
|
|
252
1200
|
if (head === "chat") {
|
|
253
1201
|
if (!second)
|
|
254
1202
|
throw new Error(`Usage\n${usage()}`);
|
|
@@ -260,92 +1208,56 @@ function parseOuroCommand(args) {
|
|
|
260
1208
|
return parsePokeCommand(args.slice(1));
|
|
261
1209
|
if (head === "link")
|
|
262
1210
|
return parseLinkCommand(args.slice(1));
|
|
1211
|
+
if (head === "mcp-serve")
|
|
1212
|
+
return parseMcpServeCommand(args.slice(1));
|
|
1213
|
+
if (head === "setup")
|
|
1214
|
+
return parseSetupCommand(args.slice(1));
|
|
263
1215
|
throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
|
|
264
1216
|
}
|
|
265
|
-
function defaultSendCommand(socketPath, command) {
|
|
266
|
-
return new Promise((resolve, reject) => {
|
|
267
|
-
const client = net.createConnection(socketPath);
|
|
268
|
-
let raw = "";
|
|
269
|
-
client.on("connect", () => {
|
|
270
|
-
client.write(JSON.stringify(command));
|
|
271
|
-
client.end();
|
|
272
|
-
});
|
|
273
|
-
client.on("data", (chunk) => {
|
|
274
|
-
raw += chunk.toString("utf-8");
|
|
275
|
-
});
|
|
276
|
-
client.on("error", reject);
|
|
277
|
-
client.on("end", () => {
|
|
278
|
-
const trimmed = raw.trim();
|
|
279
|
-
if (trimmed.length === 0 && command.kind === "daemon.stop") {
|
|
280
|
-
resolve({ ok: true, message: "daemon stopped" });
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (trimmed.length === 0) {
|
|
284
|
-
reject(new Error("Daemon returned empty response."));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
try {
|
|
288
|
-
const parsed = JSON.parse(trimmed);
|
|
289
|
-
resolve(parsed);
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
reject(error);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
1217
|
function defaultStartDaemonProcess(socketPath) {
|
|
298
1218
|
const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
1219
|
+
// Redirect stdio to /dev/null via file descriptors — using 'ignore' causes EPIPE
|
|
1220
|
+
// when the daemon's logging system writes to stderr after the parent exits.
|
|
1221
|
+
const outFd = fs.openSync(os.devNull, "w");
|
|
1222
|
+
const errFd = fs.openSync(os.devNull, "w");
|
|
299
1223
|
const child = (0, child_process_1.spawn)("node", [entry, "--socket", socketPath], {
|
|
300
1224
|
detached: true,
|
|
301
|
-
stdio: "ignore",
|
|
1225
|
+
stdio: ["ignore", outFd, errFd],
|
|
302
1226
|
});
|
|
303
1227
|
child.unref();
|
|
1228
|
+
// Don't close fds — the child process needs them. They'll be cleaned up when the parent exits.
|
|
304
1229
|
return Promise.resolve({ pid: child.pid ?? null });
|
|
305
1230
|
}
|
|
306
1231
|
function defaultWriteStdout(text) {
|
|
307
1232
|
// eslint-disable-next-line no-console -- terminal UX: CLI command output
|
|
308
1233
|
console.log(text);
|
|
309
1234
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
1235
|
+
/**
|
|
1236
|
+
* Read the runtimeVersion from the first .ouro bundle's bundle-meta.json.
|
|
1237
|
+
* Returns undefined if none found or unreadable.
|
|
1238
|
+
*/
|
|
1239
|
+
function readFirstBundleMetaVersion(bundlesRoot) {
|
|
1240
|
+
try {
|
|
1241
|
+
if (!fs.existsSync(bundlesRoot))
|
|
1242
|
+
return undefined;
|
|
1243
|
+
const entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
|
|
1244
|
+
for (const entry of entries) {
|
|
1245
|
+
/* v8 ignore next -- skip non-.ouro dirs: tested via version-detect tests @preserve */
|
|
1246
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
|
|
1247
|
+
continue;
|
|
1248
|
+
const metaPath = path.join(bundlesRoot, entry.name, "bundle-meta.json");
|
|
1249
|
+
if (!fs.existsSync(metaPath))
|
|
1250
|
+
continue;
|
|
1251
|
+
const raw = fs.readFileSync(metaPath, "utf-8");
|
|
1252
|
+
const meta = JSON.parse(raw);
|
|
1253
|
+
if (meta.runtimeVersion)
|
|
1254
|
+
return meta.runtimeVersion;
|
|
326
1255
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
raw += chunk.toString("utf-8");
|
|
333
|
-
});
|
|
334
|
-
client.on("error", () => finalize(false));
|
|
335
|
-
client.on("end", () => {
|
|
336
|
-
if (raw.trim().length === 0) {
|
|
337
|
-
finalize(false);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
try {
|
|
341
|
-
JSON.parse(raw);
|
|
342
|
-
finalize(true);
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
finalize(false);
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
});
|
|
1256
|
+
}
|
|
1257
|
+
catch {
|
|
1258
|
+
// Best effort — return undefined on any error
|
|
1259
|
+
}
|
|
1260
|
+
return undefined;
|
|
349
1261
|
}
|
|
350
1262
|
function defaultCleanupStaleSocket(socketPath) {
|
|
351
1263
|
if (fs.existsSync(socketPath)) {
|
|
@@ -381,9 +1293,38 @@ function defaultFallbackPendingMessage(command) {
|
|
|
381
1293
|
});
|
|
382
1294
|
return pendingPath;
|
|
383
1295
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
1296
|
+
function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
1297
|
+
if (process.platform !== "darwin") {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
const homeDir = os.homedir();
|
|
1301
|
+
const launchdDeps = {
|
|
1302
|
+
exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
|
|
1303
|
+
writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
|
|
1304
|
+
removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
|
|
1305
|
+
existsFile: (filePath) => fs.existsSync(filePath),
|
|
1306
|
+
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
1307
|
+
homeDir,
|
|
1308
|
+
userUid: process.getuid?.() ?? 0,
|
|
1309
|
+
};
|
|
1310
|
+
const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
1311
|
+
/* v8 ignore next -- covered via mock in daemon-cli-defaults.test.ts; v8 on CI attributes the real fs.existsSync branch to the non-mock load @preserve */
|
|
1312
|
+
if (!fs.existsSync(entryPath)) {
|
|
1313
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1314
|
+
level: "warn",
|
|
1315
|
+
component: "daemon",
|
|
1316
|
+
event: "daemon.entry_path_missing",
|
|
1317
|
+
message: "entryPath does not exist on disk — plist may point to a stale location. Run 'ouro daemon install' from the correct location.",
|
|
1318
|
+
meta: { entryPath },
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const logDir = (0, identity_1.getAgentDaemonLogsDir)();
|
|
1322
|
+
(0, launchd_1.installLaunchAgent)(launchdDeps, {
|
|
1323
|
+
nodePath: process.execPath,
|
|
1324
|
+
entryPath,
|
|
1325
|
+
socketPath,
|
|
1326
|
+
logDir,
|
|
1327
|
+
envPath: process.env.PATH,
|
|
387
1328
|
});
|
|
388
1329
|
}
|
|
389
1330
|
async function defaultPromptInput(question) {
|
|
@@ -401,84 +1342,402 @@ async function defaultPromptInput(question) {
|
|
|
401
1342
|
}
|
|
402
1343
|
}
|
|
403
1344
|
function defaultListDiscoveredAgents() {
|
|
404
|
-
|
|
1345
|
+
return (0, agent_discovery_1.listEnabledBundleAgents)({
|
|
1346
|
+
bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
|
|
1347
|
+
readdirSync: fs.readdirSync,
|
|
1348
|
+
readFileSync: fs.readFileSync,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
function discoverExistingCredentials(secretsRoot) {
|
|
1352
|
+
const found = [];
|
|
405
1353
|
let entries;
|
|
406
1354
|
try {
|
|
407
|
-
entries = fs.readdirSync(
|
|
1355
|
+
entries = fs.readdirSync(secretsRoot, { withFileTypes: true });
|
|
408
1356
|
}
|
|
409
1357
|
catch {
|
|
410
|
-
return
|
|
1358
|
+
return found;
|
|
411
1359
|
}
|
|
412
|
-
const discovered = [];
|
|
413
1360
|
for (const entry of entries) {
|
|
414
|
-
if (!entry.isDirectory()
|
|
1361
|
+
if (!entry.isDirectory())
|
|
415
1362
|
continue;
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
let enabled = true;
|
|
1363
|
+
const secretsPath = path.join(secretsRoot, entry.name, "secrets.json");
|
|
1364
|
+
let raw;
|
|
419
1365
|
try {
|
|
420
|
-
|
|
421
|
-
const parsed = JSON.parse(raw);
|
|
422
|
-
if (typeof parsed.enabled === "boolean") {
|
|
423
|
-
enabled = parsed.enabled;
|
|
424
|
-
}
|
|
1366
|
+
raw = fs.readFileSync(secretsPath, "utf-8");
|
|
425
1367
|
}
|
|
426
1368
|
catch {
|
|
427
1369
|
continue;
|
|
428
1370
|
}
|
|
429
|
-
|
|
430
|
-
|
|
1371
|
+
let parsed;
|
|
1372
|
+
try {
|
|
1373
|
+
parsed = JSON.parse(raw);
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (!parsed.providers)
|
|
1379
|
+
continue;
|
|
1380
|
+
for (const [provName, provConfig] of Object.entries(parsed.providers)) {
|
|
1381
|
+
if (provName === "anthropic" && provConfig.setupToken) {
|
|
1382
|
+
found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken }, providerConfig: { ...provConfig } });
|
|
1383
|
+
}
|
|
1384
|
+
else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
|
|
1385
|
+
found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken }, providerConfig: { ...provConfig } });
|
|
1386
|
+
}
|
|
1387
|
+
else if (provName === "minimax" && provConfig.apiKey) {
|
|
1388
|
+
found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey }, providerConfig: { ...provConfig } });
|
|
1389
|
+
}
|
|
1390
|
+
else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
|
|
1391
|
+
found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
|
|
1392
|
+
}
|
|
431
1393
|
}
|
|
432
1394
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
442
|
-
if (alreadyLinked) {
|
|
443
|
-
return `identity already linked: ${command.provider}:${command.externalId}`;
|
|
444
|
-
}
|
|
445
|
-
const now = new Date().toISOString();
|
|
446
|
-
await friendStore.put(command.friendId, {
|
|
447
|
-
...current,
|
|
448
|
-
externalIds: [
|
|
449
|
-
...current.externalIds,
|
|
450
|
-
{
|
|
451
|
-
provider: command.provider,
|
|
452
|
-
externalId: command.externalId,
|
|
453
|
-
linkedAt: now,
|
|
454
|
-
},
|
|
455
|
-
],
|
|
456
|
-
updatedAt: now,
|
|
1395
|
+
// Deduplicate by provider+credential value (keep first seen)
|
|
1396
|
+
const seen = new Set();
|
|
1397
|
+
return found.filter((cred) => {
|
|
1398
|
+
const key = `${cred.provider}:${JSON.stringify(cred.credentials)}`;
|
|
1399
|
+
if (seen.has(key))
|
|
1400
|
+
return false;
|
|
1401
|
+
seen.add(key);
|
|
1402
|
+
return true;
|
|
457
1403
|
});
|
|
458
|
-
return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
|
|
459
1404
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1405
|
+
/* v8 ignore start -- integration: interactive terminal specialist session @preserve */
|
|
1406
|
+
async function defaultRunSerpentGuide() {
|
|
1407
|
+
const { runCliSession } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
1408
|
+
const { patchRuntimeConfig } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
1409
|
+
const { setAgentName, setAgentConfigOverride } = await Promise.resolve().then(() => __importStar(require("../identity")));
|
|
1410
|
+
const readlinePromises = await Promise.resolve().then(() => __importStar(require("readline/promises")));
|
|
1411
|
+
const crypto = await Promise.resolve().then(() => __importStar(require("crypto")));
|
|
1412
|
+
// Phase 1: cold CLI — collect provider/credentials with a simple readline
|
|
1413
|
+
const coldRl = readlinePromises.createInterface({ input: process.stdin, output: process.stdout });
|
|
1414
|
+
const coldPrompt = async (q) => {
|
|
1415
|
+
const answer = await coldRl.question(q);
|
|
1416
|
+
return answer.trim();
|
|
1417
|
+
};
|
|
1418
|
+
let providerRaw;
|
|
1419
|
+
let credentials = {};
|
|
1420
|
+
let providerConfig = {};
|
|
1421
|
+
const tempDir = path.join(os.tmpdir(), `ouro-hatch-${crypto.randomUUID()}`);
|
|
1422
|
+
try {
|
|
1423
|
+
const secretsRoot = path.join(os.homedir(), ".agentsecrets");
|
|
1424
|
+
const discovered = discoverExistingCredentials(secretsRoot);
|
|
1425
|
+
const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
|
|
1426
|
+
const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
|
|
1427
|
+
// Default models per provider (used when entering new credentials)
|
|
1428
|
+
const defaultModels = {
|
|
1429
|
+
anthropic: "claude-opus-4-6",
|
|
1430
|
+
minimax: "MiniMax-M2.7",
|
|
1431
|
+
"openai-codex": "gpt-5.4",
|
|
1432
|
+
"github-copilot": "claude-sonnet-4.6",
|
|
1433
|
+
azure: "",
|
|
1434
|
+
};
|
|
1435
|
+
// Scan environment variables for API keys
|
|
1436
|
+
const envKeys = [
|
|
1437
|
+
{ provider: "anthropic", envVar: "ANTHROPIC_API_KEY", credKey: "setupToken" },
|
|
1438
|
+
{ provider: "openai-codex", envVar: "OPENAI_API_KEY", credKey: "oauthAccessToken" },
|
|
1439
|
+
{ provider: "azure", envVar: "AZURE_OPENAI_API_KEY", credKey: "apiKey" },
|
|
1440
|
+
{ provider: "azure", envVar: "AZURE_OPENAI_KEY", credKey: "apiKey" },
|
|
1441
|
+
{ provider: "minimax", envVar: "MINIMAX_API_KEY", credKey: "apiKey" },
|
|
1442
|
+
{ provider: "github-copilot", envVar: "GITHUB_TOKEN", credKey: "token" },
|
|
1443
|
+
];
|
|
1444
|
+
const envDiscovered = [];
|
|
1445
|
+
for (const { provider, envVar, credKey } of envKeys) {
|
|
1446
|
+
const value = process.env[envVar];
|
|
1447
|
+
if (value) {
|
|
1448
|
+
const envCred = { [credKey]: value };
|
|
1449
|
+
// For Azure, also check for endpoint and deployment env vars
|
|
1450
|
+
if (provider === "azure") {
|
|
1451
|
+
const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
|
1452
|
+
const deployment = process.env.AZURE_OPENAI_DEPLOYMENT;
|
|
1453
|
+
if (endpoint)
|
|
1454
|
+
envCred.endpoint = endpoint;
|
|
1455
|
+
if (deployment)
|
|
1456
|
+
envCred.deployment = deployment;
|
|
1457
|
+
}
|
|
1458
|
+
const provCfg = { model: defaultModels[provider] };
|
|
1459
|
+
if (provider === "azure" && envCred.deployment)
|
|
1460
|
+
provCfg.deployment = envCred.deployment;
|
|
1461
|
+
envDiscovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg, envVar });
|
|
1462
|
+
discovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg });
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (discovered.length > 0) {
|
|
1466
|
+
process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
|
|
1467
|
+
process.stdout.write("i found existing API credentials:\n\n");
|
|
1468
|
+
const unique = [...new Map(discovered.map((d) => [`${d.provider}`, d])).values()];
|
|
1469
|
+
for (let i = 0; i < unique.length; i++) {
|
|
1470
|
+
const model = unique[i].providerConfig.model || unique[i].providerConfig.deployment || "";
|
|
1471
|
+
const modelLabel = model ? `, ${model}` : "";
|
|
1472
|
+
const envMatch = envDiscovered.find((e) => e.provider === unique[i].provider && unique[i].agentName === "env");
|
|
1473
|
+
const sourceLabel = envMatch ? `from env: $${envMatch.envVar}` : `from ${unique[i].agentName}`;
|
|
1474
|
+
process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (${sourceLabel})\n`);
|
|
1475
|
+
}
|
|
1476
|
+
process.stdout.write("\n");
|
|
1477
|
+
const choice = await coldPrompt("use one of these? enter number, or 'new' for a different key: ");
|
|
1478
|
+
const idx = parseInt(choice, 10) - 1;
|
|
1479
|
+
if (idx >= 0 && idx < unique.length) {
|
|
1480
|
+
providerRaw = unique[idx].provider;
|
|
1481
|
+
credentials = unique[idx].credentials;
|
|
1482
|
+
providerConfig = unique[idx].providerConfig;
|
|
1483
|
+
}
|
|
1484
|
+
else {
|
|
1485
|
+
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
|
|
1486
|
+
if (!isAgentProvider(pRaw)) {
|
|
1487
|
+
process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
|
|
1488
|
+
coldRl.close();
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
providerRaw = pRaw;
|
|
1492
|
+
providerConfig = { model: defaultModels[providerRaw] };
|
|
1493
|
+
if (providerRaw === "anthropic")
|
|
1494
|
+
credentials.setupToken = await coldPrompt("API key: ");
|
|
1495
|
+
if (providerRaw === "openai-codex")
|
|
1496
|
+
credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
|
|
1497
|
+
if (providerRaw === "minimax")
|
|
1498
|
+
credentials.apiKey = await coldPrompt("API key: ");
|
|
1499
|
+
if (providerRaw === "azure") {
|
|
1500
|
+
credentials.apiKey = await coldPrompt("API key: ");
|
|
1501
|
+
credentials.endpoint = await coldPrompt("endpoint: ");
|
|
1502
|
+
credentials.deployment = await coldPrompt("deployment: ");
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
|
|
1508
|
+
process.stdout.write("i need an API key to power our conversation.\n\n");
|
|
1509
|
+
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
|
|
1510
|
+
if (!isAgentProvider(pRaw)) {
|
|
1511
|
+
process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
|
|
1512
|
+
coldRl.close();
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
providerRaw = pRaw;
|
|
1516
|
+
providerConfig = { model: defaultModels[providerRaw] };
|
|
1517
|
+
if (providerRaw === "anthropic")
|
|
1518
|
+
credentials.setupToken = await coldPrompt("API key: ");
|
|
1519
|
+
if (providerRaw === "openai-codex")
|
|
1520
|
+
credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
|
|
1521
|
+
if (providerRaw === "minimax")
|
|
1522
|
+
credentials.apiKey = await coldPrompt("API key: ");
|
|
1523
|
+
if (providerRaw === "azure") {
|
|
1524
|
+
credentials.apiKey = await coldPrompt("API key: ");
|
|
1525
|
+
credentials.endpoint = await coldPrompt("endpoint: ");
|
|
1526
|
+
credentials.deployment = await coldPrompt("deployment: ");
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
coldRl.close();
|
|
1530
|
+
process.stdout.write("\n");
|
|
1531
|
+
// Phase 2: configure runtime for serpent guide
|
|
1532
|
+
const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "SerpentGuide.ouro");
|
|
1533
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
1534
|
+
const secretsRoot2 = path.join(os.homedir(), ".agentsecrets");
|
|
1535
|
+
// Suppress non-critical log noise during hatch (no secrets.json, etc.)
|
|
1536
|
+
const { setRuntimeLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
|
|
1537
|
+
const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
|
|
1538
|
+
setRuntimeLogger(createLogger({ level: "error" }));
|
|
1539
|
+
// Configure runtime: set agent identity + config override so runAgent
|
|
1540
|
+
// doesn't try to read from ~/AgentBundles/SerpentGuide.ouro/
|
|
1541
|
+
setAgentName("SerpentGuide");
|
|
1542
|
+
// Build specialist system prompt
|
|
1543
|
+
const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
|
|
1544
|
+
const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
|
|
1545
|
+
const identity = (0, specialist_orchestrator_1.pickRandomIdentity)(identitiesDir);
|
|
1546
|
+
// Load identity-specific spinner phrases (falls back to DEFAULT_AGENT_PHRASES)
|
|
1547
|
+
const { loadIdentityPhrases } = await Promise.resolve().then(() => __importStar(require("./specialist-orchestrator")));
|
|
1548
|
+
const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
|
|
1549
|
+
const resolvedModel = providerConfig.model || providerConfig.deployment || "";
|
|
1550
|
+
setAgentConfigOverride({
|
|
1551
|
+
version: 2,
|
|
1552
|
+
enabled: true,
|
|
1553
|
+
provider: providerRaw,
|
|
1554
|
+
humanFacing: { provider: providerRaw, model: resolvedModel },
|
|
1555
|
+
agentFacing: { provider: providerRaw, model: resolvedModel },
|
|
1556
|
+
phrases,
|
|
1557
|
+
});
|
|
1558
|
+
patchRuntimeConfig({
|
|
1559
|
+
providers: {
|
|
1560
|
+
[providerRaw]: { ...providerConfig, ...credentials },
|
|
1561
|
+
},
|
|
1562
|
+
});
|
|
1563
|
+
const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
|
|
1564
|
+
const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
|
|
1565
|
+
tempDir,
|
|
1566
|
+
provider: providerRaw,
|
|
1567
|
+
model: providerConfig.model ?? "",
|
|
1568
|
+
});
|
|
1569
|
+
// Build specialist tools
|
|
1570
|
+
const specialistTools = (0, specialist_tools_1.getSpecialistTools)();
|
|
1571
|
+
const specialistExecTool = (0, specialist_tools_1.createSpecialistExecTool)({
|
|
1572
|
+
tempDir,
|
|
1573
|
+
credentials,
|
|
1574
|
+
provider: providerRaw,
|
|
1575
|
+
bundlesRoot,
|
|
1576
|
+
secretsRoot: secretsRoot2,
|
|
1577
|
+
animationWriter: (text) => process.stdout.write(text),
|
|
1578
|
+
});
|
|
1579
|
+
// Run the serpent guide session via runCliSession
|
|
1580
|
+
const result = await runCliSession({
|
|
1581
|
+
agentName: "SerpentGuide",
|
|
1582
|
+
tools: specialistTools,
|
|
1583
|
+
execTool: specialistExecTool,
|
|
1584
|
+
exitOnToolCall: "complete_adoption",
|
|
1585
|
+
autoFirstTurn: true,
|
|
1586
|
+
banner: false,
|
|
1587
|
+
disableCommands: true,
|
|
1588
|
+
skipSystemPromptRefresh: true,
|
|
1589
|
+
messages: [
|
|
1590
|
+
{ role: "system", content: systemPrompt },
|
|
1591
|
+
{ role: "user", content: "hi" },
|
|
1592
|
+
],
|
|
1593
|
+
});
|
|
1594
|
+
if (result.exitReason === "tool_exit" && result.toolResult) {
|
|
1595
|
+
const parsed = typeof result.toolResult === "string" ? JSON.parse(result.toolResult) : result.toolResult;
|
|
1596
|
+
if (parsed.success && parsed.agentName) {
|
|
1597
|
+
return parsed.agentName;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
catch (err) {
|
|
1603
|
+
process.stderr.write(`\nouro hatch error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
1604
|
+
coldRl.close();
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
finally {
|
|
1608
|
+
// Clear specialist config/identity so the hatched agent gets its own
|
|
1609
|
+
setAgentConfigOverride(null);
|
|
1610
|
+
const { resetProviderRuntime } = await Promise.resolve().then(() => __importStar(require("../core")));
|
|
1611
|
+
resetProviderRuntime();
|
|
1612
|
+
const { resetConfigCache } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
1613
|
+
resetConfigCache();
|
|
1614
|
+
// Restore default logging
|
|
1615
|
+
const { setRuntimeLogger: restoreLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
|
|
1616
|
+
restoreLogger(null);
|
|
1617
|
+
// Clean up temp dir if it still exists
|
|
1618
|
+
try {
|
|
1619
|
+
if (fs.existsSync(tempDir)) {
|
|
1620
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
catch {
|
|
1624
|
+
// Best effort cleanup
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
/* v8 ignore stop */
|
|
1629
|
+
function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH) {
|
|
1630
|
+
return {
|
|
1631
|
+
socketPath,
|
|
1632
|
+
sendCommand: socket_client_1.sendDaemonCommand,
|
|
1633
|
+
startDaemonProcess: defaultStartDaemonProcess,
|
|
1634
|
+
writeStdout: defaultWriteStdout,
|
|
1635
|
+
checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
|
|
1636
|
+
cleanupStaleSocket: defaultCleanupStaleSocket,
|
|
1637
|
+
fallbackPendingMessage: defaultFallbackPendingMessage,
|
|
1638
|
+
listDiscoveredAgents: defaultListDiscoveredAgents,
|
|
1639
|
+
runHatchFlow: hatch_flow_1.runHatchFlow,
|
|
1640
|
+
promptInput: defaultPromptInput,
|
|
1641
|
+
runSerpentGuide: defaultRunSerpentGuide,
|
|
1642
|
+
runAuthFlow: auth_flow_1.runRuntimeAuthFlow,
|
|
1643
|
+
registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
|
|
1644
|
+
installOuroCommand: ouro_path_installer_1.installOuroCommand,
|
|
1645
|
+
/* v8 ignore start -- self-healing: ensures active symlink matches running runtime version @preserve */
|
|
1646
|
+
ensureCurrentVersionInstalled: () => {
|
|
1647
|
+
const linkedVersion = (0, ouro_version_manager_1.getCurrentVersion)({});
|
|
1648
|
+
const version = (0, bundle_manifest_1.getPackageVersion)();
|
|
1649
|
+
if (linkedVersion === version)
|
|
1650
|
+
return;
|
|
1651
|
+
(0, ouro_version_manager_1.ensureLayout)({});
|
|
1652
|
+
const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
|
|
1653
|
+
const versionEntry = path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
|
|
1654
|
+
if (!fs.existsSync(versionEntry)) {
|
|
1655
|
+
(0, ouro_version_manager_1.installVersion)(version, {});
|
|
1656
|
+
}
|
|
1657
|
+
(0, ouro_version_manager_1.activateVersion)(version, {});
|
|
1658
|
+
},
|
|
1659
|
+
/* v8 ignore stop */
|
|
1660
|
+
/* v8 ignore start -- CLI version management defaults: integration code @preserve */
|
|
1661
|
+
checkForCliUpdate: async () => {
|
|
1662
|
+
const { checkForUpdate } = await Promise.resolve().then(() => __importStar(require("./update-checker")));
|
|
1663
|
+
return checkForUpdate((0, bundle_manifest_1.getPackageVersion)(), {
|
|
1664
|
+
fetchRegistryJson: async () => {
|
|
1665
|
+
const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli");
|
|
1666
|
+
return res.json();
|
|
1667
|
+
},
|
|
1668
|
+
distTag: "alpha",
|
|
1669
|
+
});
|
|
1670
|
+
},
|
|
1671
|
+
installCliVersion: async (version) => { (0, ouro_version_manager_1.installVersion)(version, {}); },
|
|
1672
|
+
activateCliVersion: (version) => { (0, ouro_version_manager_1.activateVersion)(version, {}); },
|
|
1673
|
+
getCurrentCliVersion: () => (0, ouro_version_manager_1.getCurrentVersion)({}),
|
|
1674
|
+
getPreviousCliVersion: () => (0, ouro_version_manager_1.getPreviousVersion)({}),
|
|
1675
|
+
listCliVersions: () => (0, ouro_version_manager_1.listInstalledVersions)({}),
|
|
1676
|
+
reExecFromNewVersion: (reArgs) => {
|
|
1677
|
+
const entry = path.join((0, ouro_version_manager_1.getOuroCliHome)(), "CurrentVersion", "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
|
|
1678
|
+
require("child_process").execFileSync("node", [entry, ...reArgs], { stdio: "inherit" });
|
|
1679
|
+
process.exit(0);
|
|
1680
|
+
},
|
|
1681
|
+
/* v8 ignore stop */
|
|
1682
|
+
syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
|
|
1683
|
+
ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
|
|
1684
|
+
ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
|
|
1685
|
+
/* v8 ignore start -- dev-mode defaults: tests inject mocks for mode detection and binary resolution @preserve */
|
|
1686
|
+
detectMode: () => (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()),
|
|
1687
|
+
getInstalledBinaryPath: () => {
|
|
1688
|
+
const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
|
|
1689
|
+
const binaryPath = path.join(cliHome, "bin", "ouro");
|
|
1690
|
+
return fs.existsSync(binaryPath) ? binaryPath : null;
|
|
1691
|
+
},
|
|
1692
|
+
execInstalledBinary: (binaryPath, binArgs) => {
|
|
1693
|
+
const { execFileSync } = require("child_process");
|
|
1694
|
+
execFileSync(binaryPath, binArgs, { stdio: "inherit" });
|
|
1695
|
+
process.exit(0);
|
|
1696
|
+
},
|
|
1697
|
+
/* v8 ignore stop */
|
|
1698
|
+
/* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
|
|
1699
|
+
startChat: async (agentName) => {
|
|
1700
|
+
const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
1701
|
+
await main(agentName);
|
|
1702
|
+
},
|
|
1703
|
+
scanSessions: async () => {
|
|
1704
|
+
const agentName = (0, identity_1.getAgentName)();
|
|
1705
|
+
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
1706
|
+
return (0, session_activity_1.listSessionActivity)({
|
|
1707
|
+
sessionsDir: path.join(agentRoot, "state", "sessions"),
|
|
1708
|
+
friendsDir: path.join(agentRoot, "friends"),
|
|
1709
|
+
agentName,
|
|
1710
|
+
}).map((entry) => ({
|
|
1711
|
+
friendId: entry.friendId,
|
|
1712
|
+
friendName: entry.friendName,
|
|
1713
|
+
channel: entry.channel,
|
|
1714
|
+
lastActivity: entry.lastActivityAt,
|
|
1715
|
+
}));
|
|
479
1716
|
},
|
|
480
1717
|
};
|
|
481
1718
|
}
|
|
1719
|
+
function formatMcpResponse(command, response) {
|
|
1720
|
+
if (command.kind === "mcp.list") {
|
|
1721
|
+
const allTools = response.data;
|
|
1722
|
+
if (!allTools || allTools.length === 0) {
|
|
1723
|
+
return response.message ?? "no tools available from connected MCP servers";
|
|
1724
|
+
}
|
|
1725
|
+
const lines = [];
|
|
1726
|
+
for (const entry of allTools) {
|
|
1727
|
+
lines.push(`[${entry.server}]`);
|
|
1728
|
+
for (const tool of entry.tools) {
|
|
1729
|
+
lines.push(` ${tool.name}: ${tool.description}`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return lines.join("\n");
|
|
1733
|
+
}
|
|
1734
|
+
// mcp.call
|
|
1735
|
+
const result = response.data;
|
|
1736
|
+
if (!result) {
|
|
1737
|
+
return response.message ?? "no result";
|
|
1738
|
+
}
|
|
1739
|
+
return result.content.map((c) => c.text).join("\n");
|
|
1740
|
+
}
|
|
482
1741
|
function toDaemonCommand(command) {
|
|
483
1742
|
return command;
|
|
484
1743
|
}
|
|
@@ -486,28 +1745,17 @@ async function resolveHatchInput(command, deps) {
|
|
|
486
1745
|
const prompt = deps.promptInput;
|
|
487
1746
|
const agentName = command.agentName ?? (prompt ? await prompt("Hatchling name: ") : "");
|
|
488
1747
|
const humanName = command.humanName ?? (prompt ? await prompt("Your name: ") : os.userInfo().username);
|
|
489
|
-
const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex): ") : "");
|
|
1748
|
+
const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex|github-copilot): ") : "");
|
|
490
1749
|
if (!agentName || !humanName || !isAgentProvider(providerRaw)) {
|
|
491
1750
|
throw new Error(`Usage\n${usage()}`);
|
|
492
1751
|
}
|
|
493
|
-
const credentials =
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
if (providerRaw === "minimax" && !credentials.apiKey && prompt) {
|
|
501
|
-
credentials.apiKey = await prompt("MiniMax API key: ");
|
|
502
|
-
}
|
|
503
|
-
if (providerRaw === "azure") {
|
|
504
|
-
if (!credentials.apiKey && prompt)
|
|
505
|
-
credentials.apiKey = await prompt("Azure API key: ");
|
|
506
|
-
if (!credentials.endpoint && prompt)
|
|
507
|
-
credentials.endpoint = await prompt("Azure endpoint: ");
|
|
508
|
-
if (!credentials.deployment && prompt)
|
|
509
|
-
credentials.deployment = await prompt("Azure deployment: ");
|
|
510
|
-
}
|
|
1752
|
+
const credentials = await (0, auth_flow_1.resolveHatchCredentials)({
|
|
1753
|
+
agentName,
|
|
1754
|
+
provider: providerRaw,
|
|
1755
|
+
credentials: command.credentials,
|
|
1756
|
+
promptInput: prompt,
|
|
1757
|
+
runAuthFlow: deps.runAuthFlow,
|
|
1758
|
+
});
|
|
511
1759
|
return {
|
|
512
1760
|
agentName,
|
|
513
1761
|
humanName,
|
|
@@ -533,12 +1781,411 @@ async function registerOuroBundleTypeNonBlocking(deps) {
|
|
|
533
1781
|
});
|
|
534
1782
|
}
|
|
535
1783
|
}
|
|
1784
|
+
async function performSystemSetup(deps) {
|
|
1785
|
+
// Install ouro command to PATH (non-blocking)
|
|
1786
|
+
if (deps.installOuroCommand) {
|
|
1787
|
+
try {
|
|
1788
|
+
const installResult = deps.installOuroCommand();
|
|
1789
|
+
/* v8 ignore next -- old-launcher repair hint: fires when stale ~/.local/bin/ouro is fixed @preserve */
|
|
1790
|
+
if (installResult.repairedOldLauncher) {
|
|
1791
|
+
deps.writeStdout("repaired stale ouro launcher at ~/.local/bin/ouro");
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
catch (error) {
|
|
1795
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1796
|
+
level: "warn",
|
|
1797
|
+
component: "daemon",
|
|
1798
|
+
event: "daemon.system_setup_ouro_cmd_error",
|
|
1799
|
+
message: "failed to install ouro command to PATH",
|
|
1800
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
// Self-healing: ensure current version is installed in ~/.ouro-cli/ layout.
|
|
1805
|
+
// Handles the case where the wrapper exists but CurrentVersion is missing
|
|
1806
|
+
// (e.g., first run after migration from old npx wrapper).
|
|
1807
|
+
if (deps.ensureCurrentVersionInstalled) {
|
|
1808
|
+
try {
|
|
1809
|
+
deps.ensureCurrentVersionInstalled();
|
|
1810
|
+
}
|
|
1811
|
+
catch (error) {
|
|
1812
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1813
|
+
level: "warn",
|
|
1814
|
+
component: "daemon",
|
|
1815
|
+
event: "daemon.system_setup_version_install_error",
|
|
1816
|
+
message: "failed to ensure current version installed",
|
|
1817
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error) },
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (deps.syncGlobalOuroBotWrapper) {
|
|
1822
|
+
try {
|
|
1823
|
+
await Promise.resolve(deps.syncGlobalOuroBotWrapper());
|
|
1824
|
+
}
|
|
1825
|
+
catch (error) {
|
|
1826
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1827
|
+
level: "warn",
|
|
1828
|
+
component: "daemon",
|
|
1829
|
+
event: "daemon.system_setup_ouro_bot_wrapper_error",
|
|
1830
|
+
message: "failed to sync global ouro.bot wrapper",
|
|
1831
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
// Ensure skill-management skill is available
|
|
1836
|
+
if (deps.ensureSkillManagement) {
|
|
1837
|
+
try {
|
|
1838
|
+
await deps.ensureSkillManagement();
|
|
1839
|
+
/* v8 ignore start -- defensive: ensureSkillManagement handles its own errors internally @preserve */
|
|
1840
|
+
}
|
|
1841
|
+
catch (error) {
|
|
1842
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1843
|
+
level: "warn",
|
|
1844
|
+
component: "daemon",
|
|
1845
|
+
event: "daemon.system_setup_skill_management_error",
|
|
1846
|
+
message: "failed to ensure skill-management skill",
|
|
1847
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
/* v8 ignore stop */
|
|
1851
|
+
}
|
|
1852
|
+
// Register .ouro bundle type (UTI on macOS)
|
|
1853
|
+
await registerOuroBundleTypeNonBlocking(deps);
|
|
1854
|
+
}
|
|
1855
|
+
function executeTaskCommand(command, taskMod) {
|
|
1856
|
+
if (command.kind === "task.board") {
|
|
1857
|
+
if (command.status) {
|
|
1858
|
+
const lines = taskMod.boardStatus(command.status);
|
|
1859
|
+
return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
|
|
1860
|
+
}
|
|
1861
|
+
const board = taskMod.getBoard();
|
|
1862
|
+
return board.full || board.compact || "no tasks found";
|
|
1863
|
+
}
|
|
1864
|
+
if (command.kind === "task.create") {
|
|
1865
|
+
try {
|
|
1866
|
+
const created = taskMod.createTask({
|
|
1867
|
+
title: command.title,
|
|
1868
|
+
type: command.type ?? "one-shot",
|
|
1869
|
+
category: "general",
|
|
1870
|
+
body: "",
|
|
1871
|
+
});
|
|
1872
|
+
return `created: ${created}`;
|
|
1873
|
+
}
|
|
1874
|
+
catch (error) {
|
|
1875
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
if (command.kind === "task.update") {
|
|
1879
|
+
const result = taskMod.updateStatus(command.id, command.status);
|
|
1880
|
+
if (!result.ok) {
|
|
1881
|
+
return `error: ${result.reason ?? "status update failed"}`;
|
|
1882
|
+
}
|
|
1883
|
+
const archivedSuffix = result.archived && result.archived.length > 0
|
|
1884
|
+
? ` | archived: ${result.archived.join(", ")}`
|
|
1885
|
+
: "";
|
|
1886
|
+
return `updated: ${command.id} -> ${result.to}${archivedSuffix}`;
|
|
1887
|
+
}
|
|
1888
|
+
if (command.kind === "task.show") {
|
|
1889
|
+
const task = taskMod.getTask(command.id);
|
|
1890
|
+
if (!task)
|
|
1891
|
+
return `task not found: ${command.id}`;
|
|
1892
|
+
return [
|
|
1893
|
+
`title: ${task.title}`,
|
|
1894
|
+
`type: ${task.type}`,
|
|
1895
|
+
`status: ${task.status}`,
|
|
1896
|
+
`category: ${task.category}`,
|
|
1897
|
+
`created: ${task.created}`,
|
|
1898
|
+
`updated: ${task.updated}`,
|
|
1899
|
+
`path: ${task.path}`,
|
|
1900
|
+
task.body ? `\n${task.body}` : "",
|
|
1901
|
+
].filter(Boolean).join("\n");
|
|
1902
|
+
}
|
|
1903
|
+
if (command.kind === "task.actionable") {
|
|
1904
|
+
const lines = taskMod.boardAction();
|
|
1905
|
+
return lines.length > 0 ? lines.join("\n") : "no action required";
|
|
1906
|
+
}
|
|
1907
|
+
if (command.kind === "task.deps") {
|
|
1908
|
+
const lines = taskMod.boardDeps();
|
|
1909
|
+
return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
|
|
1910
|
+
}
|
|
1911
|
+
if (command.kind === "task.fix") {
|
|
1912
|
+
try {
|
|
1913
|
+
const fixOptions = {
|
|
1914
|
+
mode: command.mode,
|
|
1915
|
+
...(command.issueId ? { issueId: command.issueId } : {}),
|
|
1916
|
+
...(command.option !== undefined ? { option: command.option } : {}),
|
|
1917
|
+
};
|
|
1918
|
+
const result = taskMod.fix(fixOptions);
|
|
1919
|
+
if (command.mode === "dry-run") {
|
|
1920
|
+
if (result.remaining.length === 0) {
|
|
1921
|
+
return `task health: clean`;
|
|
1922
|
+
}
|
|
1923
|
+
const safeIssues = result.remaining.filter((i) => i.confidence === "safe");
|
|
1924
|
+
const reviewIssues = result.remaining.filter((i) => i.confidence === "needs_review");
|
|
1925
|
+
const lines = [`${result.remaining.length} issues found`];
|
|
1926
|
+
if (safeIssues.length > 0) {
|
|
1927
|
+
lines.push("", `safe fixes (${safeIssues.length}):`);
|
|
1928
|
+
for (const issue of safeIssues) {
|
|
1929
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (reviewIssues.length > 0) {
|
|
1933
|
+
lines.push("", `needs review (${reviewIssues.length}):`);
|
|
1934
|
+
for (const issue of reviewIssues) {
|
|
1935
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
lines.push("", `task health: ${result.health}`);
|
|
1939
|
+
return lines.join("\n");
|
|
1940
|
+
}
|
|
1941
|
+
// safe, single, or --all modes: show what was done
|
|
1942
|
+
const lines = [];
|
|
1943
|
+
if (result.applied.length > 0) {
|
|
1944
|
+
lines.push(`${result.applied.length} applied:`);
|
|
1945
|
+
for (const issue of result.applied) {
|
|
1946
|
+
lines.push(` ${issue.code}:${issue.target}`);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (result.remaining.length > 0) {
|
|
1950
|
+
lines.push(`${result.remaining.length} remaining:`);
|
|
1951
|
+
for (const issue of result.remaining) {
|
|
1952
|
+
lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if (result.applied.length === 0 && result.remaining.length === 0) {
|
|
1956
|
+
lines.push("no issues");
|
|
1957
|
+
}
|
|
1958
|
+
lines.push(`task health: ${result.health}`);
|
|
1959
|
+
return lines.join("\n");
|
|
1960
|
+
}
|
|
1961
|
+
catch (error) {
|
|
1962
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
// command.kind === "task.sessions"
|
|
1966
|
+
const lines = taskMod.boardSessions();
|
|
1967
|
+
return lines.length > 0 ? lines.join("\n") : "no active sessions";
|
|
1968
|
+
}
|
|
1969
|
+
const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
|
|
1970
|
+
/* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
|
|
1971
|
+
function higherTrust(a, b) {
|
|
1972
|
+
const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
|
|
1973
|
+
const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
|
|
1974
|
+
return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
|
|
1975
|
+
}
|
|
1976
|
+
/* v8 ignore stop */
|
|
1977
|
+
async function executeFriendCommand(command, store) {
|
|
1978
|
+
if (command.kind === "friend.list") {
|
|
1979
|
+
const listAll = store.listAll;
|
|
1980
|
+
if (!listAll)
|
|
1981
|
+
return "friend store does not support listing";
|
|
1982
|
+
const friends = await listAll.call(store);
|
|
1983
|
+
if (friends.length === 0)
|
|
1984
|
+
return "no friends found";
|
|
1985
|
+
const lines = friends.map((f) => {
|
|
1986
|
+
const trust = f.trustLevel ?? "unknown";
|
|
1987
|
+
return `${f.id} ${f.name} ${trust}`;
|
|
1988
|
+
});
|
|
1989
|
+
return lines.join("\n");
|
|
1990
|
+
}
|
|
1991
|
+
if (command.kind === "friend.show") {
|
|
1992
|
+
const record = await store.get(command.friendId);
|
|
1993
|
+
if (!record)
|
|
1994
|
+
return `friend not found: ${command.friendId}`;
|
|
1995
|
+
return JSON.stringify(record, null, 2);
|
|
1996
|
+
}
|
|
1997
|
+
if (command.kind === "friend.create") {
|
|
1998
|
+
const now = new Date().toISOString();
|
|
1999
|
+
const id = (0, crypto_1.randomUUID)();
|
|
2000
|
+
const trustLevel = (command.trustLevel ?? "acquaintance");
|
|
2001
|
+
await store.put(id, {
|
|
2002
|
+
id,
|
|
2003
|
+
name: command.name,
|
|
2004
|
+
trustLevel,
|
|
2005
|
+
externalIds: [],
|
|
2006
|
+
tenantMemberships: [],
|
|
2007
|
+
toolPreferences: {},
|
|
2008
|
+
notes: {},
|
|
2009
|
+
totalTokens: 0,
|
|
2010
|
+
createdAt: now,
|
|
2011
|
+
updatedAt: now,
|
|
2012
|
+
schemaVersion: 1,
|
|
2013
|
+
});
|
|
2014
|
+
return `created: ${id} (${command.name}, ${trustLevel})`;
|
|
2015
|
+
}
|
|
2016
|
+
if (command.kind === "friend.update") {
|
|
2017
|
+
const current = await store.get(command.friendId);
|
|
2018
|
+
if (!current)
|
|
2019
|
+
return `friend not found: ${command.friendId}`;
|
|
2020
|
+
const now = new Date().toISOString();
|
|
2021
|
+
await store.put(command.friendId, {
|
|
2022
|
+
...current,
|
|
2023
|
+
trustLevel: command.trustLevel,
|
|
2024
|
+
role: command.trustLevel,
|
|
2025
|
+
updatedAt: now,
|
|
2026
|
+
});
|
|
2027
|
+
return `updated: ${command.friendId} → trust=${command.trustLevel}`;
|
|
2028
|
+
}
|
|
2029
|
+
if (command.kind === "friend.link") {
|
|
2030
|
+
const current = await store.get(command.friendId);
|
|
2031
|
+
if (!current)
|
|
2032
|
+
return `friend not found: ${command.friendId}`;
|
|
2033
|
+
const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
2034
|
+
if (alreadyLinked)
|
|
2035
|
+
return `identity already linked: ${command.provider}:${command.externalId}`;
|
|
2036
|
+
const now = new Date().toISOString();
|
|
2037
|
+
const newExternalIds = [
|
|
2038
|
+
...current.externalIds,
|
|
2039
|
+
{ provider: command.provider, externalId: command.externalId, linkedAt: now },
|
|
2040
|
+
];
|
|
2041
|
+
// Orphan cleanup: check if another friend has this externalId
|
|
2042
|
+
const orphan = await store.findByExternalId(command.provider, command.externalId);
|
|
2043
|
+
let mergeMessage = "";
|
|
2044
|
+
let mergedNotes = { ...current.notes };
|
|
2045
|
+
let mergedTrust = current.trustLevel;
|
|
2046
|
+
let orphanExternalIds = [];
|
|
2047
|
+
if (orphan && orphan.id !== command.friendId) {
|
|
2048
|
+
// Merge orphan's notes (target's notes take priority)
|
|
2049
|
+
mergedNotes = { ...orphan.notes, ...current.notes };
|
|
2050
|
+
// Keep higher trust level
|
|
2051
|
+
mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
|
|
2052
|
+
// Collect orphan's other externalIds (excluding the one being linked)
|
|
2053
|
+
orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
|
|
2054
|
+
await store.delete(orphan.id);
|
|
2055
|
+
mergeMessage = ` (merged orphan ${orphan.id})`;
|
|
2056
|
+
}
|
|
2057
|
+
await store.put(command.friendId, {
|
|
2058
|
+
...current,
|
|
2059
|
+
externalIds: [...newExternalIds, ...orphanExternalIds],
|
|
2060
|
+
notes: mergedNotes,
|
|
2061
|
+
trustLevel: mergedTrust,
|
|
2062
|
+
updatedAt: now,
|
|
2063
|
+
});
|
|
2064
|
+
return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
|
|
2065
|
+
}
|
|
2066
|
+
// command.kind === "friend.unlink"
|
|
2067
|
+
const current = await store.get(command.friendId);
|
|
2068
|
+
if (!current)
|
|
2069
|
+
return `friend not found: ${command.friendId}`;
|
|
2070
|
+
const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
2071
|
+
if (idx === -1)
|
|
2072
|
+
return `identity not linked: ${command.provider}:${command.externalId}`;
|
|
2073
|
+
const now = new Date().toISOString();
|
|
2074
|
+
const filtered = current.externalIds.filter((_, i) => i !== idx);
|
|
2075
|
+
await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
|
|
2076
|
+
return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
|
|
2077
|
+
}
|
|
2078
|
+
function executeReminderCommand(command, taskMod) {
|
|
2079
|
+
try {
|
|
2080
|
+
const created = taskMod.createTask({
|
|
2081
|
+
title: command.title,
|
|
2082
|
+
type: command.cadence ? "ongoing" : "one-shot",
|
|
2083
|
+
category: command.category ?? "reminder",
|
|
2084
|
+
body: command.body,
|
|
2085
|
+
scheduledAt: command.scheduledAt,
|
|
2086
|
+
cadence: command.cadence,
|
|
2087
|
+
requester: command.requester,
|
|
2088
|
+
});
|
|
2089
|
+
return `created: ${created}`;
|
|
2090
|
+
}
|
|
2091
|
+
catch (error) {
|
|
2092
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
|
|
2096
|
+
function getDevConfigPath() {
|
|
2097
|
+
return path.join((0, ouro_version_manager_1.getOuroCliHome)(), "dev-config.json");
|
|
2098
|
+
}
|
|
2099
|
+
function readPersistedDevPath() {
|
|
2100
|
+
try {
|
|
2101
|
+
const data = JSON.parse(fs.readFileSync(getDevConfigPath(), "utf-8"));
|
|
2102
|
+
return typeof data.repoPath === "string" ? data.repoPath : null;
|
|
2103
|
+
}
|
|
2104
|
+
catch {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function persistDevPath(repoPath) {
|
|
2109
|
+
try {
|
|
2110
|
+
fs.mkdirSync(path.dirname(getDevConfigPath()), { recursive: true });
|
|
2111
|
+
fs.writeFileSync(getDevConfigPath(), JSON.stringify({ repoPath }, null, 2));
|
|
2112
|
+
}
|
|
2113
|
+
catch { /* best effort */ }
|
|
2114
|
+
}
|
|
2115
|
+
function resolveDevRepoCwd(command, checkExists, deps) {
|
|
2116
|
+
// 1. Explicit --repo-path: use it and persist for next time
|
|
2117
|
+
if (command.repoPath) {
|
|
2118
|
+
const resolved = path.resolve(command.repoPath);
|
|
2119
|
+
persistDevPath(resolved);
|
|
2120
|
+
return resolved;
|
|
2121
|
+
}
|
|
2122
|
+
// 2. Clone request
|
|
2123
|
+
if (command.clone)
|
|
2124
|
+
return resolveClonePath(command, checkExists, deps);
|
|
2125
|
+
// 3. If test/internal deps provide getRepoCwd, use it directly
|
|
2126
|
+
if (deps.getRepoCwd) {
|
|
2127
|
+
return deps.getRepoCwd();
|
|
2128
|
+
}
|
|
2129
|
+
// 4. Check CWD — if we're inside a harness repo, use it
|
|
2130
|
+
const cwd = process.cwd();
|
|
2131
|
+
if (checkExists(path.join(cwd, ".git")) && checkExists(path.join(cwd, "src", "heart", "daemon"))) {
|
|
2132
|
+
persistDevPath(cwd);
|
|
2133
|
+
return cwd;
|
|
2134
|
+
}
|
|
2135
|
+
// 5. Read persisted path from last --repo-path
|
|
2136
|
+
const persisted = readPersistedDevPath();
|
|
2137
|
+
if (persisted && checkExists(path.join(persisted, ".git"))) {
|
|
2138
|
+
deps.writeStdout(`using remembered dev path: ${persisted}`);
|
|
2139
|
+
return persisted;
|
|
2140
|
+
}
|
|
2141
|
+
// 6. Fall back to getRepoRoot (works when running from installed binary → resolves to npm package)
|
|
2142
|
+
return (0, identity_1.getRepoRoot)();
|
|
2143
|
+
}
|
|
2144
|
+
/* v8 ignore stop */
|
|
2145
|
+
/* v8 ignore start -- clone/build: requires real git clone + npm install on disk @preserve */
|
|
2146
|
+
function resolveClonePath(command, checkExists, deps) {
|
|
2147
|
+
const cloneTarget = command.clonePath
|
|
2148
|
+
? path.resolve(command.clonePath)
|
|
2149
|
+
: path.join(os.homedir(), "Projects", "ouroboros");
|
|
2150
|
+
if (!checkExists(path.join(cloneTarget, ".git"))) {
|
|
2151
|
+
deps.writeStdout(`cloning ouroboros to ${cloneTarget}...`);
|
|
2152
|
+
try {
|
|
2153
|
+
(0, child_process_1.execSync)(`git clone ${identity_1.HARNESS_CANONICAL_REPO_URL} "${cloneTarget}"`, { stdio: "inherit" });
|
|
2154
|
+
}
|
|
2155
|
+
catch {
|
|
2156
|
+
throw new Error(`clone failed. check your network and try again, or clone manually and use --repo-path.`);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
else {
|
|
2160
|
+
deps.writeStdout(`repo already exists at ${cloneTarget}, pulling latest...`);
|
|
2161
|
+
try {
|
|
2162
|
+
(0, child_process_1.execSync)("git pull --ff-only", { cwd: cloneTarget, stdio: "inherit" });
|
|
2163
|
+
}
|
|
2164
|
+
catch {
|
|
2165
|
+
deps.writeStdout("pull failed (may have local changes). continuing with existing code.");
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
deps.writeStdout("building...");
|
|
2169
|
+
try {
|
|
2170
|
+
(0, child_process_1.execSync)("npm install && npm run build", { cwd: cloneTarget, stdio: "inherit" });
|
|
2171
|
+
}
|
|
2172
|
+
catch {
|
|
2173
|
+
throw new Error(`build failed in ${cloneTarget}. check the output above.`);
|
|
2174
|
+
}
|
|
2175
|
+
return cloneTarget;
|
|
2176
|
+
}
|
|
2177
|
+
/* v8 ignore stop */
|
|
536
2178
|
async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
537
|
-
if (args.
|
|
2179
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
538
2180
|
const text = usage();
|
|
539
2181
|
deps.writeStdout(text);
|
|
540
2182
|
return text;
|
|
541
2183
|
}
|
|
2184
|
+
if (args.length === 1 && (args[0] === "-v" || args[0] === "--version")) {
|
|
2185
|
+
const text = formatVersionOutput();
|
|
2186
|
+
deps.writeStdout(text);
|
|
2187
|
+
return text;
|
|
2188
|
+
}
|
|
542
2189
|
let command;
|
|
543
2190
|
try {
|
|
544
2191
|
command = parseOuroCommand(args);
|
|
@@ -556,7 +2203,20 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
556
2203
|
}
|
|
557
2204
|
if (args.length === 0) {
|
|
558
2205
|
const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : defaultListDiscoveredAgents());
|
|
559
|
-
if (discovered.length === 0) {
|
|
2206
|
+
if (discovered.length === 0 && deps.runSerpentGuide) {
|
|
2207
|
+
// System setup first — ouro command, subagents, UTI — before the interactive specialist
|
|
2208
|
+
await performSystemSetup(deps);
|
|
2209
|
+
const hatchlingName = await deps.runSerpentGuide();
|
|
2210
|
+
if (!hatchlingName) {
|
|
2211
|
+
return "";
|
|
2212
|
+
}
|
|
2213
|
+
await ensureDaemonRunning(deps);
|
|
2214
|
+
if (deps.startChat) {
|
|
2215
|
+
await deps.startChat(hatchlingName);
|
|
2216
|
+
}
|
|
2217
|
+
return "";
|
|
2218
|
+
}
|
|
2219
|
+
else if (discovered.length === 0) {
|
|
560
2220
|
command = { kind: "hatch.start" };
|
|
561
2221
|
}
|
|
562
2222
|
else if (discovered.length === 1) {
|
|
@@ -596,34 +2256,1077 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
596
2256
|
meta: { kind: command.kind },
|
|
597
2257
|
});
|
|
598
2258
|
if (command.kind === "daemon.up") {
|
|
2259
|
+
// ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
|
|
2260
|
+
/* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
|
|
599
2261
|
try {
|
|
600
|
-
|
|
2262
|
+
const devConfigPath = getDevConfigPath();
|
|
2263
|
+
if (fs.existsSync(devConfigPath)) {
|
|
2264
|
+
fs.unlinkSync(devConfigPath);
|
|
2265
|
+
}
|
|
601
2266
|
}
|
|
602
|
-
catch
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
2267
|
+
catch { /* best effort */ }
|
|
2268
|
+
/* v8 ignore stop */
|
|
2269
|
+
// ── dev mode delegation: ouro up from a dev repo delegates to installed binary ──
|
|
2270
|
+
// Only runs when detectMode is explicitly injected (via createDefaultOuroCliDeps or tests)
|
|
2271
|
+
if (deps.detectMode) {
|
|
2272
|
+
const runtimeMode = deps.detectMode();
|
|
2273
|
+
if (runtimeMode === "dev") {
|
|
2274
|
+
/* v8 ignore next -- defensive: getInstalledBinaryPath always injected in tests @preserve */
|
|
2275
|
+
const installedBinary = deps.getInstalledBinaryPath ? deps.getInstalledBinaryPath() : null;
|
|
2276
|
+
if (installedBinary) {
|
|
2277
|
+
deps.writeStdout("delegating to installed ouro...");
|
|
2278
|
+
/* v8 ignore next 3 -- defensive: execInstalledBinary always injected; missing branch unreachable @preserve */
|
|
2279
|
+
if (deps.execInstalledBinary) {
|
|
2280
|
+
deps.execInstalledBinary(installedBinary, args);
|
|
2281
|
+
}
|
|
2282
|
+
/* v8 ignore next 2 -- unreachable after exec replaces process @preserve */
|
|
2283
|
+
return "";
|
|
2284
|
+
}
|
|
2285
|
+
const message = "no installed version found. run: npx @ouro.bot/cli@alpha";
|
|
2286
|
+
deps.writeStdout(message);
|
|
2287
|
+
return message;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
|
|
2291
|
+
// ── versioned CLI update check ──
|
|
2292
|
+
if (deps.checkForCliUpdate) {
|
|
2293
|
+
let pendingReExec = false;
|
|
2294
|
+
try {
|
|
2295
|
+
const updateResult = await deps.checkForCliUpdate();
|
|
2296
|
+
if (updateResult.available && updateResult.latestVersion) {
|
|
2297
|
+
/* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
|
|
2298
|
+
const currentVersion = linkedVersionBeforeUp ?? "unknown";
|
|
2299
|
+
await deps.installCliVersion(updateResult.latestVersion);
|
|
2300
|
+
deps.activateCliVersion(updateResult.latestVersion);
|
|
2301
|
+
deps.writeStdout(`ouro updated to ${updateResult.latestVersion} (was ${currentVersion})`);
|
|
2302
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
|
|
2303
|
+
/* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
|
|
2304
|
+
if (changelogCommand) {
|
|
2305
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2306
|
+
}
|
|
2307
|
+
pendingReExec = true;
|
|
2308
|
+
}
|
|
2309
|
+
/* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
|
|
2310
|
+
}
|
|
2311
|
+
catch (error) {
|
|
2312
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2313
|
+
level: "warn",
|
|
2314
|
+
component: "daemon",
|
|
2315
|
+
event: "daemon.cli_update_check_error",
|
|
2316
|
+
message: "CLI update check failed",
|
|
2317
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
/* v8 ignore stop */
|
|
2321
|
+
if (pendingReExec) {
|
|
2322
|
+
deps.reExecFromNewVersion(args);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
await performSystemSetup(deps);
|
|
2326
|
+
const linkedVersionAfterSetup = deps.getCurrentCliVersion?.() ?? null;
|
|
2327
|
+
const runtimeVersion = (0, bundle_manifest_1.getPackageVersion)();
|
|
2328
|
+
if (linkedVersionBeforeUp && linkedVersionBeforeUp !== runtimeVersion && linkedVersionAfterSetup === runtimeVersion) {
|
|
2329
|
+
deps.writeStdout(`ouro updated to ${runtimeVersion} (was ${linkedVersionBeforeUp})`);
|
|
2330
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(linkedVersionBeforeUp, runtimeVersion);
|
|
2331
|
+
if (changelogCommand) {
|
|
2332
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
// Run update hooks before starting daemon so user sees the output
|
|
2336
|
+
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
2337
|
+
(0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
|
|
2338
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
2339
|
+
const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
|
|
2340
|
+
// Snapshot the previous CLI version from the first bundle-meta before
|
|
2341
|
+
// hooks overwrite it. This detects when npx downloaded a newer CLI.
|
|
2342
|
+
const previousCliVersion = readFirstBundleMetaVersion(bundlesRoot);
|
|
2343
|
+
const updateSummary = await (0, update_hooks_1.applyPendingUpdates)(bundlesRoot, currentVersion);
|
|
2344
|
+
// Notify about CLI binary update (npx downloaded a new version).
|
|
2345
|
+
// Skip when the symlink already points to the running version — that
|
|
2346
|
+
// means path 1 (checkForCliUpdate + reExecFromNewVersion) already
|
|
2347
|
+
// printed the update message before re-exec.
|
|
2348
|
+
/* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
|
|
2349
|
+
if (previousCliVersion && previousCliVersion !== currentVersion && linkedVersionBeforeUp !== currentVersion) {
|
|
2350
|
+
deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
|
|
2351
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(previousCliVersion, currentVersion);
|
|
2352
|
+
/* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
|
|
2353
|
+
if (changelogCommand) {
|
|
2354
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
/* v8 ignore stop */
|
|
2358
|
+
if (updateSummary.updated.length > 0) {
|
|
2359
|
+
const agents = updateSummary.updated.map((e) => e.agent);
|
|
2360
|
+
const from = updateSummary.updated[0].from;
|
|
2361
|
+
const to = updateSummary.updated[0].to;
|
|
2362
|
+
const fromStr = from ? ` (was ${from})` : "";
|
|
2363
|
+
const count = agents.length;
|
|
2364
|
+
deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
|
|
610
2365
|
}
|
|
611
|
-
await registerOuroBundleTypeNonBlocking(deps);
|
|
612
2366
|
const daemonResult = await ensureDaemonRunning(deps);
|
|
613
2367
|
deps.writeStdout(daemonResult.message);
|
|
2368
|
+
// Persist boot startup AFTER daemon is running — bootstrap is safe now
|
|
2369
|
+
// because the daemon socket exists, so launchd's KeepAlive registers
|
|
2370
|
+
// for crash recovery without starting a competing process.
|
|
2371
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
2372
|
+
try {
|
|
2373
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
2374
|
+
}
|
|
2375
|
+
catch (error) {
|
|
2376
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2377
|
+
level: "warn",
|
|
2378
|
+
component: "daemon",
|
|
2379
|
+
event: "daemon.system_setup_launchd_error",
|
|
2380
|
+
message: "failed to persist daemon boot startup",
|
|
2381
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
614
2385
|
return daemonResult.message;
|
|
615
2386
|
}
|
|
2387
|
+
if (command.kind === "daemon.dev") {
|
|
2388
|
+
/* v8 ignore next -- defensive: existsSync always injected in tests @preserve */
|
|
2389
|
+
const checkExists = deps.existsSync ?? fs.existsSync;
|
|
2390
|
+
/* v8 ignore next -- repo resolution dispatched to v8-ignored helper @preserve */
|
|
2391
|
+
let repoCwd = resolveDevRepoCwd(command, checkExists, deps);
|
|
2392
|
+
let entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2393
|
+
if (!checkExists(entryPath) || !checkExists(path.join(repoCwd, ".git"))) {
|
|
2394
|
+
if (command.repoPath) {
|
|
2395
|
+
// Explicit --repo-path didn't have a valid repo — error
|
|
2396
|
+
const message = `no harness repo found at ${repoCwd}. run npm run build first.`;
|
|
2397
|
+
deps.writeStdout(message);
|
|
2398
|
+
return message;
|
|
2399
|
+
}
|
|
2400
|
+
/* v8 ignore start -- auto-clone: interactive prompt + existing repo discovery + real git/npm @preserve */
|
|
2401
|
+
const defaultClonePath = path.join(os.homedir(), "Projects", "ouroboros");
|
|
2402
|
+
if (checkExists(path.join(defaultClonePath, ".git"))) {
|
|
2403
|
+
deps.writeStdout(`found existing repo at ${defaultClonePath}`);
|
|
2404
|
+
try {
|
|
2405
|
+
repoCwd = resolveClonePath({ clonePath: defaultClonePath }, checkExists, deps);
|
|
2406
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2407
|
+
}
|
|
2408
|
+
catch (err) {
|
|
2409
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2410
|
+
deps.writeStdout(message);
|
|
2411
|
+
return message;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
else if (deps.promptInput) {
|
|
2415
|
+
deps.writeStdout("no harness repo found.");
|
|
2416
|
+
const answer = await deps.promptInput(`already have a checkout? enter its path, or press enter to clone to ${defaultClonePath}: `);
|
|
2417
|
+
const cloneTarget = answer.trim() || defaultClonePath;
|
|
2418
|
+
try {
|
|
2419
|
+
repoCwd = resolveClonePath({ clonePath: cloneTarget }, checkExists, deps);
|
|
2420
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2421
|
+
}
|
|
2422
|
+
catch (err) {
|
|
2423
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2424
|
+
deps.writeStdout(message);
|
|
2425
|
+
return message;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
const message = `no harness repo found. run: ouro dev --repo-path /path/to/ouroboros`;
|
|
2430
|
+
deps.writeStdout(message);
|
|
2431
|
+
return message;
|
|
2432
|
+
}
|
|
2433
|
+
/* v8 ignore stop */
|
|
2434
|
+
}
|
|
2435
|
+
// Auto-build: always rebuild in dev mode so dist/ matches source
|
|
2436
|
+
/* v8 ignore start -- dev auto-build: execSync in repo cwd, tested manually @preserve */
|
|
2437
|
+
deps.writeStdout(`building from ${repoCwd}...`);
|
|
2438
|
+
try {
|
|
2439
|
+
(0, child_process_1.execSync)("npm run build", { cwd: repoCwd, stdio: "inherit" });
|
|
2440
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2441
|
+
}
|
|
2442
|
+
catch {
|
|
2443
|
+
const message = `build failed in ${repoCwd}. fix compilation errors and retry.`;
|
|
2444
|
+
deps.writeStdout(message);
|
|
2445
|
+
return message;
|
|
2446
|
+
}
|
|
2447
|
+
/* v8 ignore stop */
|
|
2448
|
+
/* v8 ignore start -- defensive: ensureDaemonBootPersistence always injected in tests @preserve */
|
|
2449
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
2450
|
+
try {
|
|
2451
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
2452
|
+
/* v8 ignore next -- defensive: boot persistence error should not block dev mode @preserve */
|
|
2453
|
+
}
|
|
2454
|
+
catch (error) {
|
|
2455
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2456
|
+
level: "warn",
|
|
2457
|
+
component: "daemon",
|
|
2458
|
+
event: "daemon.dev_boot_persistence_error",
|
|
2459
|
+
message: "failed to persist daemon boot startup in dev mode",
|
|
2460
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
/* v8 ignore stop */
|
|
2464
|
+
}
|
|
2465
|
+
// Disable launchd KeepAlive before killing — prevents the installed daemon from respawning
|
|
2466
|
+
/* v8 ignore start -- dev launchd disable: requires real launchctl + plist on disk @preserve */
|
|
2467
|
+
const launchdDevDeps = {
|
|
2468
|
+
exec: (cmd) => { (0, child_process_1.execSync)(cmd); },
|
|
2469
|
+
existsFile: (p) => fs.existsSync(p),
|
|
2470
|
+
removeFile: (p) => { try {
|
|
2471
|
+
fs.unlinkSync(p);
|
|
2472
|
+
}
|
|
2473
|
+
catch { /* best effort */ } },
|
|
2474
|
+
homeDir: os.homedir(),
|
|
2475
|
+
userUid: process.getuid?.() ?? 0,
|
|
2476
|
+
};
|
|
2477
|
+
if ((0, launchd_1.isDaemonInstalled)(launchdDevDeps)) {
|
|
2478
|
+
(0, launchd_1.uninstallLaunchAgent)(launchdDevDeps);
|
|
2479
|
+
deps.writeStdout("disabled launchd auto-restart for dev mode");
|
|
2480
|
+
}
|
|
2481
|
+
/* v8 ignore stop */
|
|
2482
|
+
// Always force-restart in dev mode — you rebuilt, you want this code running
|
|
2483
|
+
/* v8 ignore start -- dev force-restart: socket alive/stop/spawn tested via integration; tests inject mocks @preserve */
|
|
2484
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
2485
|
+
if (alive) {
|
|
2486
|
+
try {
|
|
2487
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
2488
|
+
}
|
|
2489
|
+
catch { /* already stopping */ }
|
|
2490
|
+
}
|
|
2491
|
+
deps.cleanupStaleSocket(deps.socketPath);
|
|
2492
|
+
const devEntry = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2493
|
+
const startDevDaemon = deps.startDaemonProcess === defaultStartDaemonProcess
|
|
2494
|
+
? async (sp) => {
|
|
2495
|
+
const child = (0, child_process_1.spawn)("node", [devEntry, "--socket", sp], { detached: true, stdio: "ignore" });
|
|
2496
|
+
child.unref();
|
|
2497
|
+
return { pid: child.pid ?? null };
|
|
2498
|
+
}
|
|
2499
|
+
: deps.startDaemonProcess;
|
|
2500
|
+
/* v8 ignore stop */
|
|
2501
|
+
const started = await startDevDaemon(deps.socketPath);
|
|
2502
|
+
/* v8 ignore next -- defensive: pid is null only when spawn fails silently @preserve */
|
|
2503
|
+
const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})\nrun 'ouro up' to return to production mode`;
|
|
2504
|
+
deps.writeStdout(message);
|
|
2505
|
+
return message;
|
|
2506
|
+
}
|
|
2507
|
+
// ── rollback command (local, no daemon socket needed for symlinks) ──
|
|
2508
|
+
/* v8 ignore start -- rollback/versions: tested via daemon-cli-rollback/versions tests @preserve */
|
|
2509
|
+
if (command.kind === "rollback") {
|
|
2510
|
+
const currentVersion = deps.getCurrentCliVersion?.() ?? "unknown";
|
|
2511
|
+
if (command.version) {
|
|
2512
|
+
// Rollback to a specific version
|
|
2513
|
+
const installed = deps.listCliVersions?.() ?? [];
|
|
2514
|
+
if (!installed.includes(command.version)) {
|
|
2515
|
+
try {
|
|
2516
|
+
await deps.installCliVersion(command.version);
|
|
2517
|
+
}
|
|
2518
|
+
catch (error) {
|
|
2519
|
+
const message = `failed to install version ${command.version}: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
2520
|
+
deps.writeStdout(message);
|
|
2521
|
+
return message;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
deps.activateCliVersion(command.version);
|
|
2525
|
+
}
|
|
2526
|
+
else {
|
|
2527
|
+
// Rollback to previous version
|
|
2528
|
+
const previousVersion = deps.getPreviousCliVersion?.();
|
|
2529
|
+
if (!previousVersion) {
|
|
2530
|
+
const message = "no previous version to roll back to";
|
|
2531
|
+
deps.writeStdout(message);
|
|
2532
|
+
return message;
|
|
2533
|
+
}
|
|
2534
|
+
deps.activateCliVersion(previousVersion);
|
|
2535
|
+
command = { ...command, version: previousVersion };
|
|
2536
|
+
}
|
|
2537
|
+
// Stop daemon (non-fatal if not running)
|
|
2538
|
+
try {
|
|
2539
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
2540
|
+
}
|
|
2541
|
+
catch {
|
|
2542
|
+
// Daemon may not be running — that's fine
|
|
2543
|
+
}
|
|
2544
|
+
const message = `rolled back to ${command.version} (was ${currentVersion})`;
|
|
2545
|
+
deps.writeStdout(message);
|
|
2546
|
+
return message;
|
|
2547
|
+
}
|
|
2548
|
+
// ── versions command (local install list + published update truth, no daemon socket needed) ──
|
|
2549
|
+
if (command.kind === "versions") {
|
|
2550
|
+
const versions = deps.listCliVersions?.() ?? [];
|
|
2551
|
+
const current = deps.getCurrentCliVersion?.();
|
|
2552
|
+
const previous = deps.getPreviousCliVersion?.();
|
|
2553
|
+
const localSection = versions.length === 0
|
|
2554
|
+
? "no versions installed"
|
|
2555
|
+
: versions.map((v) => {
|
|
2556
|
+
let line = v;
|
|
2557
|
+
if (v === current)
|
|
2558
|
+
line += " * current";
|
|
2559
|
+
if (v === previous)
|
|
2560
|
+
line += " (previous)";
|
|
2561
|
+
return line;
|
|
2562
|
+
}).join("\n");
|
|
2563
|
+
const sections = [localSection];
|
|
2564
|
+
if (deps.checkForCliUpdate) {
|
|
2565
|
+
try {
|
|
2566
|
+
const updateResult = await deps.checkForCliUpdate();
|
|
2567
|
+
if (updateResult.latestVersion) {
|
|
2568
|
+
sections.push(`published alpha: ${updateResult.latestVersion} (${updateResult.available ? "update available" : "up to date"})`);
|
|
2569
|
+
}
|
|
2570
|
+
else if (updateResult.error) {
|
|
2571
|
+
sections.push(`published alpha: unavailable (${updateResult.error})`);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
catch (err) {
|
|
2575
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2576
|
+
sections.push(`published alpha: unavailable (${reason})`);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
const message = sections.join("\n\n");
|
|
2580
|
+
deps.writeStdout(message);
|
|
2581
|
+
return message;
|
|
2582
|
+
}
|
|
2583
|
+
/* v8 ignore stop */
|
|
616
2584
|
if (command.kind === "daemon.logs" && deps.tailLogs) {
|
|
617
2585
|
deps.tailLogs();
|
|
618
2586
|
return "";
|
|
619
2587
|
}
|
|
620
|
-
if (command.kind === "
|
|
621
|
-
|
|
622
|
-
|
|
2588
|
+
if (command.kind === "outlook") {
|
|
2589
|
+
let status;
|
|
2590
|
+
try {
|
|
2591
|
+
status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
|
|
2592
|
+
/* v8 ignore start — error path: daemon not running */
|
|
2593
|
+
}
|
|
2594
|
+
catch {
|
|
2595
|
+
const message = "daemon unavailable — start with `ouro up` first";
|
|
2596
|
+
deps.writeStdout(message);
|
|
2597
|
+
return message;
|
|
2598
|
+
}
|
|
2599
|
+
/* v8 ignore stop */
|
|
2600
|
+
const payload = parseStatusPayload(status.data);
|
|
2601
|
+
/* v8 ignore start -- ?? branch: outlookUrl always present in test fixtures */
|
|
2602
|
+
const outlookUrl = payload?.overview.outlookUrl ?? "unavailable";
|
|
2603
|
+
/* v8 ignore stop */
|
|
2604
|
+
if (!command.json) {
|
|
2605
|
+
deps.writeStdout(outlookUrl);
|
|
2606
|
+
return outlookUrl;
|
|
2607
|
+
}
|
|
2608
|
+
/* v8 ignore start — error path: outlook URL not available */
|
|
2609
|
+
if (outlookUrl === "unavailable") {
|
|
2610
|
+
deps.writeStdout(outlookUrl);
|
|
2611
|
+
return outlookUrl;
|
|
2612
|
+
}
|
|
2613
|
+
/* v8 ignore stop */
|
|
2614
|
+
/* v8 ignore start -- ?? branch: tests always inject fetchImpl */
|
|
2615
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
2616
|
+
/* v8 ignore stop */
|
|
2617
|
+
const response = await fetchImpl(`${outlookUrl}/api/machine`);
|
|
2618
|
+
const data = await response.json();
|
|
2619
|
+
const text = JSON.stringify(data, null, 2);
|
|
2620
|
+
deps.writeStdout(text);
|
|
2621
|
+
return text;
|
|
2622
|
+
}
|
|
2623
|
+
// ── hook: handle Claude Code lifecycle hooks ──
|
|
2624
|
+
/* v8 ignore start -- hook handler: reads real stdin, sends to real daemon @preserve */
|
|
2625
|
+
if (command.kind === "hook") {
|
|
2626
|
+
let stdinData = "";
|
|
2627
|
+
try {
|
|
2628
|
+
stdinData = require("fs").readFileSync(0, "utf-8");
|
|
2629
|
+
}
|
|
2630
|
+
catch { /* no stdin */ }
|
|
2631
|
+
let event = {};
|
|
2632
|
+
try {
|
|
2633
|
+
event = JSON.parse(stdinData);
|
|
2634
|
+
}
|
|
2635
|
+
catch { /* malformed */ }
|
|
2636
|
+
const eventType = command.event;
|
|
2637
|
+
const sessionId = event.session_id ?? "unknown";
|
|
2638
|
+
const cwd = event.cwd ?? "";
|
|
2639
|
+
// Build notification content based on event type
|
|
2640
|
+
let content;
|
|
2641
|
+
if (eventType === "session-start") {
|
|
2642
|
+
const model = event.model ?? "";
|
|
2643
|
+
const source = event.source ?? "";
|
|
2644
|
+
content = `[Claude Code session started: ${sessionId}, cwd: ${cwd}${model ? `, model: ${model}` : ""}${source ? `, source: ${source}` : ""}]`;
|
|
2645
|
+
}
|
|
2646
|
+
else if (eventType === "stop") {
|
|
2647
|
+
const lastMsg = event.last_assistant_message ?? "";
|
|
2648
|
+
content = `[Claude Code session ended: ${sessionId}${lastMsg ? `, last: ${lastMsg.slice(0, 200)}` : ""}]`;
|
|
2649
|
+
}
|
|
2650
|
+
else if (eventType === "post-tool-use") {
|
|
2651
|
+
const toolName = event.tool_name ?? "";
|
|
2652
|
+
content = `[Claude Code used ${toolName} in session ${sessionId}]`;
|
|
2653
|
+
}
|
|
2654
|
+
else {
|
|
2655
|
+
content = `[Claude Code hook: ${eventType} in session ${sessionId}]`;
|
|
2656
|
+
}
|
|
2657
|
+
// Send to the specific agent configured for this hook
|
|
2658
|
+
try {
|
|
2659
|
+
await deps.sendCommand(deps.socketPath, { kind: "message.send", from: `claude-code:${sessionId}`, to: command.agent, content }).catch(() => { });
|
|
2660
|
+
await deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: command.agent }).catch(() => { });
|
|
2661
|
+
}
|
|
2662
|
+
catch { /* daemon not running — silent */ }
|
|
2663
|
+
// Output for Claude Code hook system
|
|
2664
|
+
deps.writeStdout(JSON.stringify({ continue: true }));
|
|
2665
|
+
return JSON.stringify({ continue: true });
|
|
2666
|
+
}
|
|
2667
|
+
/* v8 ignore stop */
|
|
2668
|
+
// ── setup: configure dev tool integration ──
|
|
2669
|
+
if (command.kind === "setup") {
|
|
2670
|
+
const { tool, agent: setupAgent } = command;
|
|
2671
|
+
const sourceRoot = (0, identity_1.getRepoRoot)();
|
|
2672
|
+
const runtimeMode = (0, runtime_mode_1.detectRuntimeMode)(sourceRoot);
|
|
2673
|
+
const mcpServeCommand = runtimeMode === "dev"
|
|
2674
|
+
? `node ${path.join(sourceRoot, "dist", "heart", "daemon", "ouro-bot-entry.js")} mcp-serve --agent ${setupAgent}`
|
|
2675
|
+
: `ouro mcp-serve --agent ${setupAgent}`;
|
|
2676
|
+
if (tool === "claude-code") {
|
|
2677
|
+
// 1. Register MCP server with Claude Code
|
|
2678
|
+
const mcpAddCmd = `claude mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
|
|
2679
|
+
(0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
|
|
2680
|
+
// 2. Write hooks config to ~/.claude/settings.json
|
|
2681
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
2682
|
+
let settings = {};
|
|
2683
|
+
if (fs.existsSync(settingsPath)) {
|
|
2684
|
+
try {
|
|
2685
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
2686
|
+
}
|
|
2687
|
+
catch { /* start fresh */ }
|
|
2688
|
+
}
|
|
2689
|
+
// Use `ouro hook <event>` — resolves the right code based on dev vs installed mode.
|
|
2690
|
+
// Bare `ouro` works because ouro is on PATH via ~/.ouro-cli/bin/.
|
|
2691
|
+
settings.hooks = {
|
|
2692
|
+
...(settings.hooks ?? {}),
|
|
2693
|
+
SessionStart: [{ hooks: [{ type: "command", command: `ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
|
|
2694
|
+
Stop: [{ hooks: [{ type: "command", command: `ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
|
|
2695
|
+
PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
|
|
2696
|
+
};
|
|
2697
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
2698
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
2699
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2700
|
+
component: "daemon",
|
|
2701
|
+
event: "daemon.setup_complete",
|
|
2702
|
+
message: "dev tool setup complete",
|
|
2703
|
+
meta: { tool, agent: setupAgent, runtimeMode },
|
|
2704
|
+
});
|
|
2705
|
+
// 3. Write conversation formatting instructions to ~/.claude/CLAUDE.md
|
|
2706
|
+
const claudeMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
2707
|
+
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`;
|
|
2708
|
+
let existingClaudeMd = "";
|
|
2709
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
2710
|
+
existingClaudeMd = fs.readFileSync(claudeMdPath, "utf-8");
|
|
2711
|
+
}
|
|
2712
|
+
if (!existingClaudeMd.includes("Agent conversations (ouro)")) {
|
|
2713
|
+
fs.writeFileSync(claudeMdPath, existingClaudeMd + agentInstructions);
|
|
2714
|
+
}
|
|
2715
|
+
const message = `setup complete: claude-code + ${setupAgent}\n MCP server registered\n hooks configured\n conversation formatting instructions added`;
|
|
2716
|
+
deps.writeStdout(message);
|
|
2717
|
+
return message;
|
|
2718
|
+
}
|
|
2719
|
+
else {
|
|
2720
|
+
// tool === "codex" (parseSetupCommand validates tool, so this is the only remaining option)
|
|
2721
|
+
const mcpAddCmd = `codex mcp add ouro-${setupAgent} -- ${mcpServeCommand}`;
|
|
2722
|
+
(0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
|
|
2723
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2724
|
+
component: "daemon",
|
|
2725
|
+
event: "daemon.setup_complete",
|
|
2726
|
+
message: "dev tool setup complete",
|
|
2727
|
+
meta: { tool, agent: setupAgent, runtimeMode },
|
|
2728
|
+
});
|
|
2729
|
+
const message = `setup complete: codex + ${setupAgent}\n MCP server registered`;
|
|
2730
|
+
deps.writeStdout(message);
|
|
2731
|
+
return message;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
/* v8 ignore start — mcp-serve block binds to process.stdin/stdout; tested via mcp-server unit tests */
|
|
2735
|
+
// ── mcp-serve: start MCP server in-process on stdin/stdout ──
|
|
2736
|
+
if (command.kind === "mcp-serve") {
|
|
2737
|
+
const { createMcpServer } = await Promise.resolve().then(() => __importStar(require("./mcp-server")));
|
|
2738
|
+
const friendId = command.friendId ?? `local-${os.userInfo().username}`;
|
|
2739
|
+
const mcpSocketPath = command.socketOverride ?? deps.socketPath;
|
|
2740
|
+
const server = createMcpServer({
|
|
2741
|
+
agent: command.agent,
|
|
2742
|
+
friendId,
|
|
2743
|
+
socketPath: mcpSocketPath,
|
|
2744
|
+
stdin: process.stdin,
|
|
2745
|
+
stdout: process.stdout,
|
|
2746
|
+
});
|
|
2747
|
+
server.start();
|
|
2748
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2749
|
+
component: "daemon",
|
|
2750
|
+
event: "daemon.mcp_serve_started",
|
|
2751
|
+
message: "MCP server started via CLI",
|
|
2752
|
+
meta: { agent: command.agent, friendId },
|
|
2753
|
+
});
|
|
2754
|
+
// Keep process alive until stdin closes
|
|
2755
|
+
await new Promise((resolve) => {
|
|
2756
|
+
process.stdin.on("end", () => {
|
|
2757
|
+
server.stop();
|
|
2758
|
+
resolve();
|
|
2759
|
+
});
|
|
2760
|
+
});
|
|
2761
|
+
return "";
|
|
2762
|
+
}
|
|
2763
|
+
/* v8 ignore stop */
|
|
2764
|
+
// ── mcp subcommands (routed through daemon socket) ──
|
|
2765
|
+
if (command.kind === "mcp.list" || command.kind === "mcp.call") {
|
|
2766
|
+
const daemonCommand = toDaemonCommand(command);
|
|
2767
|
+
let response;
|
|
2768
|
+
try {
|
|
2769
|
+
response = await deps.sendCommand(deps.socketPath, daemonCommand);
|
|
2770
|
+
}
|
|
2771
|
+
catch {
|
|
2772
|
+
const message = "daemon unavailable — start with `ouro up` first";
|
|
2773
|
+
deps.writeStdout(message);
|
|
2774
|
+
return message;
|
|
2775
|
+
}
|
|
2776
|
+
if (!response.ok) {
|
|
2777
|
+
const message = response.error ?? "unknown error";
|
|
2778
|
+
deps.writeStdout(message);
|
|
2779
|
+
return message;
|
|
2780
|
+
}
|
|
2781
|
+
const message = formatMcpResponse(command, response);
|
|
2782
|
+
deps.writeStdout(message);
|
|
2783
|
+
return message;
|
|
2784
|
+
}
|
|
2785
|
+
// ── task subcommands (local, no daemon socket needed) ──
|
|
2786
|
+
if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
|
|
2787
|
+
command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
|
|
2788
|
+
command.kind === "task.sessions" || command.kind === "task.fix") {
|
|
2789
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2790
|
+
const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
|
|
2791
|
+
/* v8 ignore stop */
|
|
2792
|
+
const message = executeTaskCommand(command, taskMod);
|
|
2793
|
+
deps.writeStdout(message);
|
|
2794
|
+
return message;
|
|
2795
|
+
}
|
|
2796
|
+
// ── reminder subcommands (local, no daemon socket needed) ──
|
|
2797
|
+
if (command.kind === "reminder.create") {
|
|
2798
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2799
|
+
const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
|
|
2800
|
+
/* v8 ignore stop */
|
|
2801
|
+
const message = executeReminderCommand(command, taskMod);
|
|
2802
|
+
deps.writeStdout(message);
|
|
2803
|
+
return message;
|
|
2804
|
+
}
|
|
2805
|
+
// ── habit subcommands (local, no daemon socket needed) ──
|
|
2806
|
+
if (command.kind === "habit.list" || command.kind === "habit.create") {
|
|
2807
|
+
const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("./habit-parser")));
|
|
2808
|
+
/* v8 ignore start -- production default: uses real bundle root @preserve */
|
|
2809
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
2810
|
+
const bundleRoot = deps.agentBundleRoot ?? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
|
|
2811
|
+
/* v8 ignore stop */
|
|
2812
|
+
const habitsDir = path.join(bundleRoot, "habits");
|
|
2813
|
+
if (command.kind === "habit.list") {
|
|
2814
|
+
let files;
|
|
2815
|
+
try {
|
|
2816
|
+
files = fs.readdirSync(habitsDir).filter((f) => f.endsWith(".md") && f !== "README.md");
|
|
2817
|
+
}
|
|
2818
|
+
catch {
|
|
2819
|
+
const message = "no habits found";
|
|
2820
|
+
deps.writeStdout(message);
|
|
2821
|
+
return message;
|
|
2822
|
+
}
|
|
2823
|
+
if (files.length === 0) {
|
|
2824
|
+
const message = "no habits found";
|
|
2825
|
+
deps.writeStdout(message);
|
|
2826
|
+
return message;
|
|
2827
|
+
}
|
|
2828
|
+
const lines = [];
|
|
2829
|
+
for (const file of files) {
|
|
2830
|
+
const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
|
|
2831
|
+
const habit = parseHabitFile(content, path.join(habitsDir, file));
|
|
2832
|
+
const lastRunStr = habit.lastRun ?? "never";
|
|
2833
|
+
lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
|
|
2834
|
+
}
|
|
2835
|
+
const message = lines.join("\n");
|
|
2836
|
+
deps.writeStdout(message);
|
|
2837
|
+
return message;
|
|
2838
|
+
}
|
|
2839
|
+
// habit.create
|
|
2840
|
+
const filePath = path.join(habitsDir, `${command.name}.md`);
|
|
2841
|
+
if (fs.existsSync(filePath)) {
|
|
2842
|
+
const message = `error: habit '${command.name}' already exists`;
|
|
2843
|
+
deps.writeStdout(message);
|
|
2844
|
+
return message;
|
|
2845
|
+
}
|
|
2846
|
+
fs.mkdirSync(habitsDir, { recursive: true });
|
|
2847
|
+
const now = new Date().toISOString();
|
|
2848
|
+
const content = renderHabitFile({
|
|
2849
|
+
title: command.name,
|
|
2850
|
+
cadence: command.cadence ?? "null",
|
|
2851
|
+
status: "active",
|
|
2852
|
+
lastRun: now,
|
|
2853
|
+
created: now,
|
|
2854
|
+
}, `Habit: ${command.name}`);
|
|
2855
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
2856
|
+
const message = `created: ${filePath}`;
|
|
2857
|
+
deps.writeStdout(message);
|
|
2858
|
+
return message;
|
|
2859
|
+
}
|
|
2860
|
+
// ── friend subcommands (local, no daemon socket needed) ──
|
|
2861
|
+
if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
|
|
2862
|
+
command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
|
|
2863
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2864
|
+
let store = deps.friendStore;
|
|
2865
|
+
if (!store) {
|
|
2866
|
+
// Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
|
|
2867
|
+
const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
|
|
2868
|
+
const friendsDir = agentName
|
|
2869
|
+
? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
|
|
2870
|
+
: path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
|
|
2871
|
+
store = new store_file_1.FileFriendStore(friendsDir);
|
|
2872
|
+
}
|
|
2873
|
+
/* v8 ignore stop */
|
|
2874
|
+
const message = await executeFriendCommand(command, store);
|
|
2875
|
+
deps.writeStdout(message);
|
|
2876
|
+
return message;
|
|
2877
|
+
}
|
|
2878
|
+
// ── auth (local, no daemon socket needed) ──
|
|
2879
|
+
if (command.kind === "auth.run") {
|
|
2880
|
+
const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent).config.humanFacing.provider;
|
|
2881
|
+
/* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
|
|
2882
|
+
const authRunner = deps.runAuthFlow ?? auth_flow_1.runRuntimeAuthFlow;
|
|
2883
|
+
const result = await authRunner({
|
|
2884
|
+
agentName: command.agent,
|
|
2885
|
+
provider,
|
|
2886
|
+
promptInput: deps.promptInput,
|
|
2887
|
+
});
|
|
2888
|
+
// Behavior: ouro auth stores credentials only — does NOT switch provider.
|
|
2889
|
+
// Use `ouro auth switch` to change the active provider.
|
|
2890
|
+
deps.writeStdout(result.message);
|
|
2891
|
+
// Verify the credentials actually work by pinging the provider
|
|
2892
|
+
/* v8 ignore start -- integration: real API ping after auth @preserve */
|
|
2893
|
+
try {
|
|
2894
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2895
|
+
const status = await verifyProviderCredentials(provider, secrets.providers);
|
|
2896
|
+
deps.writeStdout(`${provider}: ${status}`);
|
|
2897
|
+
}
|
|
2898
|
+
catch {
|
|
2899
|
+
// Verification failure is non-blocking — credentials were saved regardless
|
|
2900
|
+
}
|
|
2901
|
+
/* v8 ignore stop */
|
|
2902
|
+
return result.message;
|
|
2903
|
+
}
|
|
2904
|
+
// ── auth verify (local, no daemon socket needed) ──
|
|
2905
|
+
/* v8 ignore start -- auth verify/switch: tested in daemon-cli.test.ts but v8 traces differ in CI @preserve */
|
|
2906
|
+
if (command.kind === "auth.verify") {
|
|
2907
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2908
|
+
const providers = secrets.providers;
|
|
2909
|
+
if (command.provider) {
|
|
2910
|
+
const status = await verifyProviderCredentials(command.provider, providers);
|
|
2911
|
+
const message = `${command.provider}: ${status}`;
|
|
2912
|
+
deps.writeStdout(message);
|
|
2913
|
+
return message;
|
|
2914
|
+
}
|
|
2915
|
+
const lines = [];
|
|
2916
|
+
for (const p of Object.keys(providers)) {
|
|
2917
|
+
const status = await verifyProviderCredentials(p, providers);
|
|
2918
|
+
lines.push(`${p}: ${status}`);
|
|
2919
|
+
}
|
|
2920
|
+
const message = lines.join("\n");
|
|
2921
|
+
deps.writeStdout(message);
|
|
2922
|
+
return message;
|
|
2923
|
+
}
|
|
2924
|
+
// ── auth switch (local, no daemon socket needed) ──
|
|
2925
|
+
if (command.kind === "auth.switch") {
|
|
2926
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2927
|
+
const providerSecrets = secrets.providers[command.provider];
|
|
2928
|
+
if (!providerSecrets || !hasStoredCredentials(command.provider, providerSecrets)) {
|
|
2929
|
+
const message = `no credentials stored for ${command.provider}. Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`;
|
|
2930
|
+
deps.writeStdout(message);
|
|
2931
|
+
return message;
|
|
2932
|
+
}
|
|
2933
|
+
// Verify credentials actually work before switching
|
|
2934
|
+
const status = await verifyProviderCredentials(command.provider, secrets.providers);
|
|
2935
|
+
if (!status.startsWith("ok")) {
|
|
2936
|
+
const message = `${command.provider}: ${status}. fix credentials with \`ouro auth --agent ${command.agent} --provider ${command.provider}\` before switching.`;
|
|
2937
|
+
deps.writeStdout(message);
|
|
2938
|
+
return message;
|
|
2939
|
+
}
|
|
2940
|
+
if (command.facing) {
|
|
2941
|
+
(0, auth_flow_1.writeAgentProviderSelection)(command.agent, command.facing, command.provider);
|
|
2942
|
+
}
|
|
2943
|
+
else {
|
|
2944
|
+
(0, auth_flow_1.writeAgentProviderSelection)(command.agent, "human", command.provider);
|
|
2945
|
+
(0, auth_flow_1.writeAgentProviderSelection)(command.agent, "agent", command.provider);
|
|
2946
|
+
}
|
|
2947
|
+
const message = `switched ${command.agent} to ${command.provider} (verified working)`;
|
|
2948
|
+
deps.writeStdout(message);
|
|
2949
|
+
return message;
|
|
2950
|
+
}
|
|
2951
|
+
/* v8 ignore stop */
|
|
2952
|
+
// ── config models (local, no daemon socket needed) ──
|
|
2953
|
+
/* v8 ignore start -- config models: tested via daemon-cli.test.ts @preserve */
|
|
2954
|
+
if (command.kind === "config.models") {
|
|
2955
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
|
|
2956
|
+
const provider = config.humanFacing.provider;
|
|
2957
|
+
if (provider !== "github-copilot") {
|
|
2958
|
+
const message = `model listing not available for ${provider} — check provider documentation.`;
|
|
2959
|
+
deps.writeStdout(message);
|
|
2960
|
+
return message;
|
|
2961
|
+
}
|
|
2962
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2963
|
+
const ghConfig = secrets.providers["github-copilot"];
|
|
2964
|
+
if (!ghConfig.githubToken || !ghConfig.baseUrl) {
|
|
2965
|
+
throw new Error(`github-copilot credentials not configured. Run \`ouro auth --agent ${command.agent} --provider github-copilot\` first.`);
|
|
2966
|
+
}
|
|
2967
|
+
const fetchFn = deps.fetchImpl ?? fetch;
|
|
2968
|
+
const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
|
|
2969
|
+
if (models.length === 0) {
|
|
2970
|
+
const message = "no models found";
|
|
2971
|
+
deps.writeStdout(message);
|
|
2972
|
+
return message;
|
|
2973
|
+
}
|
|
2974
|
+
const lines = ["available models:"];
|
|
2975
|
+
for (const m of models) {
|
|
2976
|
+
const caps = m.capabilities?.length ? ` (${m.capabilities.join(", ")})` : "";
|
|
2977
|
+
lines.push(` ${m.id}${caps}`);
|
|
2978
|
+
}
|
|
2979
|
+
const message = lines.join("\n");
|
|
2980
|
+
deps.writeStdout(message);
|
|
2981
|
+
return message;
|
|
2982
|
+
}
|
|
2983
|
+
/* v8 ignore stop */
|
|
2984
|
+
// ── config model (local, no daemon socket needed) ──
|
|
2985
|
+
/* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
|
|
2986
|
+
if (command.kind === "config.model") {
|
|
2987
|
+
const facing = command.facing ?? "human";
|
|
2988
|
+
// Validate model availability for github-copilot before writing
|
|
2989
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
|
|
2990
|
+
const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
|
|
2991
|
+
if (facingConfig.provider === "github-copilot") {
|
|
2992
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2993
|
+
const ghConfig = secrets.providers["github-copilot"];
|
|
2994
|
+
if (ghConfig.githubToken && ghConfig.baseUrl) {
|
|
2995
|
+
const fetchFn = deps.fetchImpl ?? fetch;
|
|
2996
|
+
try {
|
|
2997
|
+
const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
|
|
2998
|
+
const available = models.map((m) => m.id);
|
|
2999
|
+
if (available.length > 0 && !available.includes(command.modelName)) {
|
|
3000
|
+
const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
|
|
3001
|
+
deps.writeStdout(message);
|
|
3002
|
+
return message;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
catch {
|
|
3006
|
+
// Catalog validation failed — fall through to ping test
|
|
3007
|
+
}
|
|
3008
|
+
// Ping test: verify the model actually works before switching
|
|
3009
|
+
const pingResult = await pingGithubCopilotModel(ghConfig.baseUrl, ghConfig.githubToken, command.modelName, fetchFn);
|
|
3010
|
+
if (!pingResult.ok) {
|
|
3011
|
+
const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
|
|
3012
|
+
deps.writeStdout(message);
|
|
3013
|
+
return message;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, facing, command.modelName);
|
|
3018
|
+
const message = previousModel
|
|
3019
|
+
? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
|
|
3020
|
+
: `set ${command.agent} model on ${provider}: ${command.modelName}`;
|
|
3021
|
+
deps.writeStdout(message);
|
|
3022
|
+
return message;
|
|
3023
|
+
}
|
|
3024
|
+
/* v8 ignore stop */
|
|
3025
|
+
// ── whoami (local, no daemon socket needed) ──
|
|
3026
|
+
if (command.kind === "whoami") {
|
|
3027
|
+
if (command.agent) {
|
|
3028
|
+
const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
|
|
3029
|
+
const message = [
|
|
3030
|
+
`agent: ${command.agent}`,
|
|
3031
|
+
`home: ${agentRoot}`,
|
|
3032
|
+
`bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
|
|
3033
|
+
].join("\n");
|
|
3034
|
+
deps.writeStdout(message);
|
|
3035
|
+
return message;
|
|
3036
|
+
}
|
|
3037
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
3038
|
+
try {
|
|
3039
|
+
const info = deps.whoamiInfo
|
|
3040
|
+
? deps.whoamiInfo()
|
|
3041
|
+
: {
|
|
3042
|
+
agentName: (0, identity_1.getAgentName)(),
|
|
3043
|
+
homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
|
|
3044
|
+
bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
|
|
3045
|
+
};
|
|
3046
|
+
const message = [
|
|
3047
|
+
`agent: ${info.agentName}`,
|
|
3048
|
+
`home: ${info.homePath}`,
|
|
3049
|
+
`bones: ${info.bonesVersion}`,
|
|
3050
|
+
].join("\n");
|
|
3051
|
+
deps.writeStdout(message);
|
|
3052
|
+
return message;
|
|
3053
|
+
}
|
|
3054
|
+
catch {
|
|
3055
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
3056
|
+
deps.writeStdout(message);
|
|
3057
|
+
return message;
|
|
3058
|
+
}
|
|
3059
|
+
/* v8 ignore stop */
|
|
3060
|
+
}
|
|
3061
|
+
// ── changelog (local, no daemon socket needed) ──
|
|
3062
|
+
if (command.kind === "changelog") {
|
|
3063
|
+
try {
|
|
3064
|
+
const changelogPath = deps.getChangelogPath
|
|
3065
|
+
? deps.getChangelogPath()
|
|
3066
|
+
: (0, bundle_manifest_1.getChangelogPath)();
|
|
3067
|
+
const raw = fs.readFileSync(changelogPath, "utf-8");
|
|
3068
|
+
const parsed = JSON.parse(raw);
|
|
3069
|
+
const entries = Array.isArray(parsed) ? parsed : (parsed.versions ?? []);
|
|
3070
|
+
let filtered = entries;
|
|
3071
|
+
if (command.from) {
|
|
3072
|
+
const fromVersion = command.from;
|
|
3073
|
+
filtered = entries.filter((e) => semver.valid(e.version) && semver.gt(e.version, fromVersion));
|
|
3074
|
+
}
|
|
3075
|
+
if (filtered.length === 0) {
|
|
3076
|
+
const message = "no changelog entries found.";
|
|
3077
|
+
deps.writeStdout(message);
|
|
3078
|
+
return message;
|
|
3079
|
+
}
|
|
3080
|
+
const lines = [];
|
|
3081
|
+
for (const entry of filtered) {
|
|
3082
|
+
lines.push(`## ${entry.version}${entry.date ? ` (${entry.date})` : ""}`);
|
|
3083
|
+
if (entry.changes) {
|
|
3084
|
+
for (const change of entry.changes) {
|
|
3085
|
+
lines.push(`- ${change}`);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
lines.push("");
|
|
3089
|
+
}
|
|
3090
|
+
const message = lines.join("\n").trim();
|
|
3091
|
+
deps.writeStdout(message);
|
|
3092
|
+
return message;
|
|
3093
|
+
}
|
|
3094
|
+
catch {
|
|
3095
|
+
const message = "no changelog entries found.";
|
|
3096
|
+
deps.writeStdout(message);
|
|
3097
|
+
return message;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
// ── thoughts (local, no daemon socket needed) ──
|
|
3101
|
+
if (command.kind === "thoughts") {
|
|
3102
|
+
try {
|
|
3103
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
3104
|
+
const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
|
|
3105
|
+
const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
|
|
3106
|
+
if (command.json) {
|
|
3107
|
+
try {
|
|
3108
|
+
const raw = fs.readFileSync(sessionFilePath, "utf-8");
|
|
3109
|
+
deps.writeStdout(raw);
|
|
3110
|
+
return raw;
|
|
3111
|
+
}
|
|
3112
|
+
catch {
|
|
3113
|
+
const message = "no inner dialog session found";
|
|
3114
|
+
deps.writeStdout(message);
|
|
3115
|
+
return message;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
|
|
3119
|
+
const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
|
|
3120
|
+
deps.writeStdout(message);
|
|
3121
|
+
if (command.follow) {
|
|
3122
|
+
deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
|
|
3123
|
+
/* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
|
|
3124
|
+
const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
|
|
3125
|
+
deps.writeStdout("\n" + formatted);
|
|
3126
|
+
});
|
|
3127
|
+
/* v8 ignore stop */
|
|
3128
|
+
// Block until process exit; cleanup watcher on SIGINT/SIGTERM
|
|
3129
|
+
return new Promise((resolve) => {
|
|
3130
|
+
const cleanup = () => { stop(); resolve(message); };
|
|
3131
|
+
process.once("SIGINT", cleanup);
|
|
3132
|
+
process.once("SIGTERM", cleanup);
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
return message;
|
|
3136
|
+
}
|
|
3137
|
+
catch {
|
|
3138
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
3139
|
+
deps.writeStdout(message);
|
|
3140
|
+
return message;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
// ── attention queue (local, no daemon socket needed) ──
|
|
3144
|
+
/* v8 ignore start -- CLI attention handler: requires real obligation store on disk @preserve */
|
|
3145
|
+
if (command.kind === "attention.list" || command.kind === "attention.show" || command.kind === "attention.history") {
|
|
3146
|
+
try {
|
|
3147
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
3148
|
+
const { listActiveObligations, readObligation } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
|
|
3149
|
+
if (command.kind === "attention.list") {
|
|
3150
|
+
const obligations = listActiveObligations(agentName);
|
|
3151
|
+
if (obligations.length === 0) {
|
|
3152
|
+
const message = "nothing held — attention queue is empty";
|
|
3153
|
+
deps.writeStdout(message);
|
|
3154
|
+
return message;
|
|
3155
|
+
}
|
|
3156
|
+
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})`);
|
|
3157
|
+
const message = lines.join("\n");
|
|
3158
|
+
deps.writeStdout(message);
|
|
3159
|
+
return message;
|
|
3160
|
+
}
|
|
3161
|
+
if (command.kind === "attention.show") {
|
|
3162
|
+
const obligation = readObligation(agentName, command.id);
|
|
3163
|
+
if (!obligation) {
|
|
3164
|
+
const message = `no obligation found with id ${command.id}`;
|
|
3165
|
+
deps.writeStdout(message);
|
|
3166
|
+
return message;
|
|
3167
|
+
}
|
|
3168
|
+
const message = JSON.stringify(obligation, null, 2);
|
|
3169
|
+
deps.writeStdout(message);
|
|
3170
|
+
return message;
|
|
3171
|
+
}
|
|
3172
|
+
// attention.history: show returned obligations
|
|
3173
|
+
const { getObligationsDir } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
|
|
3174
|
+
const obligationsDir = getObligationsDir(agentName);
|
|
3175
|
+
let entries = [];
|
|
3176
|
+
try {
|
|
3177
|
+
entries = fs.readdirSync(obligationsDir);
|
|
3178
|
+
}
|
|
3179
|
+
catch { /* empty */ }
|
|
3180
|
+
const returned = entries
|
|
3181
|
+
.filter((e) => e.endsWith(".json"))
|
|
3182
|
+
.map((e) => { try {
|
|
3183
|
+
return JSON.parse(fs.readFileSync(path.join(obligationsDir, e), "utf-8"));
|
|
3184
|
+
}
|
|
3185
|
+
catch {
|
|
3186
|
+
return null;
|
|
3187
|
+
} })
|
|
3188
|
+
.filter((o) => o?.status === "returned")
|
|
3189
|
+
.sort((a, b) => (b.returnedAt ?? 0) - (a.returnedAt ?? 0))
|
|
3190
|
+
.slice(0, 20);
|
|
3191
|
+
if (returned.length === 0) {
|
|
3192
|
+
const message = "no surfacing history yet";
|
|
3193
|
+
deps.writeStdout(message);
|
|
3194
|
+
return message;
|
|
3195
|
+
}
|
|
3196
|
+
const lines = returned.map((o) => {
|
|
3197
|
+
const when = o.returnedAt ? new Date(o.returnedAt).toISOString() : "unknown";
|
|
3198
|
+
return `[${o.id}] → ${o.origin.friendId} via ${o.returnTarget ?? "unknown"} at ${when}`;
|
|
3199
|
+
});
|
|
3200
|
+
const message = lines.join("\n");
|
|
3201
|
+
deps.writeStdout(message);
|
|
3202
|
+
return message;
|
|
3203
|
+
}
|
|
3204
|
+
catch {
|
|
3205
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
3206
|
+
deps.writeStdout(message);
|
|
3207
|
+
return message;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
/* v8 ignore stop */
|
|
3211
|
+
// ── inner dialog status (local, no daemon socket needed) ──
|
|
3212
|
+
/* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
|
|
3213
|
+
if (command.kind === "inner.status") {
|
|
3214
|
+
try {
|
|
3215
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
3216
|
+
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
3217
|
+
const { buildInnerStatusOutput } = await Promise.resolve().then(() => __importStar(require("./inner-status")));
|
|
3218
|
+
const { sessionPath: getSessionPath } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
3219
|
+
const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
|
|
3220
|
+
const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../repertoire/tasks/parser")));
|
|
3221
|
+
const { listActiveObligations } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
|
|
3222
|
+
// Read runtime state
|
|
3223
|
+
const innerSessionPath = getSessionPath("inner-dialog", "inner", "session");
|
|
3224
|
+
const runtimeJsonPath = path.join(path.dirname(innerSessionPath), "runtime.json");
|
|
3225
|
+
let runtimeState = null;
|
|
3226
|
+
try {
|
|
3227
|
+
const raw = fs.readFileSync(runtimeJsonPath, "utf-8");
|
|
3228
|
+
runtimeState = JSON.parse(raw);
|
|
3229
|
+
}
|
|
3230
|
+
catch { /* missing or corrupt — will show "unknown" */ }
|
|
3231
|
+
// Read journal files
|
|
3232
|
+
const journalDir = path.join(agentRoot, "journal");
|
|
3233
|
+
let journalFiles = [];
|
|
3234
|
+
try {
|
|
3235
|
+
const entries = fs.readdirSync(journalDir, { withFileTypes: true });
|
|
3236
|
+
journalFiles = entries
|
|
3237
|
+
.filter((e) => e.isFile() && !e.name.startsWith("."))
|
|
3238
|
+
.map((e) => {
|
|
3239
|
+
const stat = fs.statSync(path.join(journalDir, e.name));
|
|
3240
|
+
return { name: e.name, mtimeMs: stat.mtimeMs };
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
catch { /* missing dir — will show (empty) */ }
|
|
3244
|
+
// Read heartbeat cadence
|
|
3245
|
+
let heartbeat = null;
|
|
3246
|
+
try {
|
|
3247
|
+
const habitsDir = path.join(agentRoot, "habits");
|
|
3248
|
+
const heartbeatPath = path.join(habitsDir, "heartbeat.md");
|
|
3249
|
+
let cadenceMs = DEFAULT_CADENCE_MS;
|
|
3250
|
+
if (fs.existsSync(heartbeatPath)) {
|
|
3251
|
+
const content = fs.readFileSync(heartbeatPath, "utf-8");
|
|
3252
|
+
const lines = content.split(/\r?\n/);
|
|
3253
|
+
if (lines[0]?.trim() === "---") {
|
|
3254
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
3255
|
+
if (closing !== -1) {
|
|
3256
|
+
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
3257
|
+
const frontmatter = parseFrontmatter(rawFrontmatter);
|
|
3258
|
+
const parsed = parseCadenceMs(frontmatter.cadence);
|
|
3259
|
+
if (parsed !== null)
|
|
3260
|
+
cadenceMs = parsed;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
let lastCompletedAt = null;
|
|
3265
|
+
if (runtimeState?.lastCompletedAt) {
|
|
3266
|
+
const ms = new Date(runtimeState.lastCompletedAt).getTime();
|
|
3267
|
+
if (!Number.isNaN(ms))
|
|
3268
|
+
lastCompletedAt = ms;
|
|
3269
|
+
}
|
|
3270
|
+
heartbeat = { cadenceMs, lastCompletedAt };
|
|
3271
|
+
}
|
|
3272
|
+
catch { /* no habits — heartbeat unknown */ }
|
|
3273
|
+
// Attention count
|
|
3274
|
+
const activeObligations = listActiveObligations(agentName);
|
|
3275
|
+
const message = buildInnerStatusOutput({
|
|
3276
|
+
agentName,
|
|
3277
|
+
runtimeState,
|
|
3278
|
+
journalFiles,
|
|
3279
|
+
heartbeat,
|
|
3280
|
+
attentionCount: activeObligations.length,
|
|
3281
|
+
now: Date.now(),
|
|
3282
|
+
});
|
|
3283
|
+
deps.writeStdout(message);
|
|
3284
|
+
return message;
|
|
3285
|
+
}
|
|
3286
|
+
catch {
|
|
3287
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
3288
|
+
deps.writeStdout(message);
|
|
3289
|
+
return message;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
/* v8 ignore stop */
|
|
3293
|
+
// ── session list (local, no daemon socket needed) ──
|
|
3294
|
+
if (command.kind === "session.list") {
|
|
3295
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
3296
|
+
const scanner = deps.scanSessions ?? (async () => []);
|
|
3297
|
+
/* v8 ignore stop */
|
|
3298
|
+
const sessions = await scanner();
|
|
3299
|
+
if (sessions.length === 0) {
|
|
3300
|
+
const message = "no active sessions";
|
|
3301
|
+
deps.writeStdout(message);
|
|
3302
|
+
return message;
|
|
3303
|
+
}
|
|
3304
|
+
const lines = sessions.map((s) => `${s.friendId} ${s.friendName} ${s.channel} ${s.lastActivity}`);
|
|
3305
|
+
const message = lines.join("\n");
|
|
623
3306
|
deps.writeStdout(message);
|
|
624
3307
|
return message;
|
|
625
3308
|
}
|
|
3309
|
+
if (command.kind === "chat.connect" && deps.startChat) {
|
|
3310
|
+
await ensureDaemonRunning(deps);
|
|
3311
|
+
await deps.startChat(command.agent);
|
|
3312
|
+
return "";
|
|
3313
|
+
}
|
|
626
3314
|
if (command.kind === "hatch.start") {
|
|
3315
|
+
// Route through serpent guide when no explicit hatch args were provided
|
|
3316
|
+
const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
|
|
3317
|
+
if (deps.runSerpentGuide && !hasExplicitHatchArgs) {
|
|
3318
|
+
// System setup first — ouro command, subagents, UTI — before the interactive specialist
|
|
3319
|
+
await performSystemSetup(deps);
|
|
3320
|
+
const hatchlingName = await deps.runSerpentGuide();
|
|
3321
|
+
if (!hatchlingName) {
|
|
3322
|
+
return "";
|
|
3323
|
+
}
|
|
3324
|
+
await ensureDaemonRunning(deps);
|
|
3325
|
+
if (deps.startChat) {
|
|
3326
|
+
await deps.startChat(hatchlingName);
|
|
3327
|
+
}
|
|
3328
|
+
return "";
|
|
3329
|
+
}
|
|
627
3330
|
const hatchRunner = deps.runHatchFlow;
|
|
628
3331
|
if (!hatchRunner) {
|
|
629
3332
|
const response = await deps.sendCommand(deps.socketPath, { kind: "hatch.start" });
|
|
@@ -633,19 +3336,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
633
3336
|
}
|
|
634
3337
|
const hatchInput = await resolveHatchInput(command, deps);
|
|
635
3338
|
const result = await hatchRunner(hatchInput);
|
|
636
|
-
|
|
637
|
-
await deps.installSubagents();
|
|
638
|
-
}
|
|
639
|
-
catch (error) {
|
|
640
|
-
(0, runtime_1.emitNervesEvent)({
|
|
641
|
-
level: "warn",
|
|
642
|
-
component: "daemon",
|
|
643
|
-
event: "daemon.subagent_install_error",
|
|
644
|
-
message: "subagent auto-install failed",
|
|
645
|
-
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
await registerOuroBundleTypeNonBlocking(deps);
|
|
3339
|
+
await performSystemSetup(deps);
|
|
649
3340
|
const daemonResult = await ensureDaemonRunning(deps);
|
|
650
3341
|
if (deps.startChat) {
|
|
651
3342
|
await deps.startChat(hatchInput.agentName);
|
|
@@ -667,9 +3358,22 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
667
3358
|
deps.writeStdout(message);
|
|
668
3359
|
return message;
|
|
669
3360
|
}
|
|
3361
|
+
if (command.kind === "daemon.status" && isDaemonUnavailableError(error)) {
|
|
3362
|
+
const message = daemonUnavailableStatusOutput(deps.socketPath, deps.healthFilePath);
|
|
3363
|
+
deps.writeStdout(message);
|
|
3364
|
+
return message;
|
|
3365
|
+
}
|
|
3366
|
+
if (command.kind === "daemon.stop" && isDaemonUnavailableError(error)) {
|
|
3367
|
+
const message = "daemon not running";
|
|
3368
|
+
deps.writeStdout(message);
|
|
3369
|
+
return message;
|
|
3370
|
+
}
|
|
670
3371
|
throw error;
|
|
671
3372
|
}
|
|
672
|
-
const
|
|
3373
|
+
const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
|
|
3374
|
+
const message = command.kind === "daemon.status"
|
|
3375
|
+
? formatDaemonStatusOutput(response, fallbackMessage)
|
|
3376
|
+
: fallbackMessage;
|
|
673
3377
|
deps.writeStdout(message);
|
|
674
3378
|
return message;
|
|
675
3379
|
}
|