@ouro.bot/cli 0.1.0-alpha.8 → 0.1.0-alpha.80

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.
Files changed (127) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +147 -205
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +462 -0
  7. package/dist/heart/active-work.js +218 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/commitments.js +89 -0
  12. package/dist/heart/config.js +68 -23
  13. package/dist/heart/core.js +452 -93
  14. package/dist/heart/cross-chat-delivery.js +146 -0
  15. package/dist/heart/daemon/agent-discovery.js +81 -0
  16. package/dist/heart/daemon/auth-flow.js +430 -0
  17. package/dist/heart/daemon/daemon-cli.js +1746 -247
  18. package/dist/heart/daemon/daemon-entry.js +55 -6
  19. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  20. package/dist/heart/daemon/daemon.js +216 -10
  21. package/dist/heart/daemon/hatch-animation.js +10 -3
  22. package/dist/heart/daemon/hatch-flow.js +7 -82
  23. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  24. package/dist/heart/daemon/launchd.js +159 -0
  25. package/dist/heart/daemon/log-tailer.js +4 -3
  26. package/dist/heart/daemon/message-router.js +17 -8
  27. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  28. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  29. package/dist/heart/daemon/ouro-entry.js +0 -0
  30. package/dist/heart/daemon/ouro-path-installer.js +260 -0
  31. package/dist/heart/daemon/ouro-uti.js +11 -2
  32. package/dist/heart/daemon/ouro-version-manager.js +164 -0
  33. package/dist/heart/daemon/process-manager.js +14 -1
  34. package/dist/heart/daemon/run-hooks.js +37 -0
  35. package/dist/heart/daemon/runtime-logging.js +58 -15
  36. package/dist/heart/daemon/runtime-metadata.js +219 -0
  37. package/dist/heart/daemon/runtime-mode.js +67 -0
  38. package/dist/heart/daemon/sense-manager.js +307 -0
  39. package/dist/heart/daemon/skill-management-installer.js +94 -0
  40. package/dist/heart/daemon/socket-client.js +202 -0
  41. package/dist/heart/daemon/specialist-orchestrator.js +53 -84
  42. package/dist/heart/daemon/specialist-prompt.js +63 -11
  43. package/dist/heart/daemon/specialist-tools.js +211 -60
  44. package/dist/heart/daemon/staged-restart.js +114 -0
  45. package/dist/heart/daemon/thoughts.js +507 -0
  46. package/dist/heart/daemon/update-checker.js +111 -0
  47. package/dist/heart/daemon/update-hooks.js +138 -0
  48. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  49. package/dist/heart/delegation.js +62 -0
  50. package/dist/heart/identity.js +126 -21
  51. package/dist/heart/kicks.js +1 -19
  52. package/dist/heart/model-capabilities.js +48 -0
  53. package/dist/heart/obligations.js +141 -0
  54. package/dist/heart/progress-story.js +42 -0
  55. package/dist/heart/providers/anthropic.js +74 -9
  56. package/dist/heart/providers/azure.js +86 -7
  57. package/dist/heart/providers/github-copilot.js +149 -0
  58. package/dist/heart/providers/minimax.js +4 -0
  59. package/dist/heart/providers/openai-codex.js +12 -3
  60. package/dist/heart/safe-workspace.js +228 -0
  61. package/dist/heart/sense-truth.js +61 -0
  62. package/dist/heart/session-activity.js +169 -0
  63. package/dist/heart/session-recall.js +116 -0
  64. package/dist/heart/streaming.js +100 -22
  65. package/dist/heart/target-resolution.js +123 -0
  66. package/dist/heart/turn-coordinator.js +28 -0
  67. package/dist/mind/associative-recall.js +14 -2
  68. package/dist/mind/bundle-manifest.js +70 -0
  69. package/dist/mind/context.js +27 -11
  70. package/dist/mind/first-impressions.js +16 -2
  71. package/dist/mind/friends/channel.js +35 -0
  72. package/dist/mind/friends/group-context.js +144 -0
  73. package/dist/mind/friends/store-file.js +19 -0
  74. package/dist/mind/friends/trust-explanation.js +74 -0
  75. package/dist/mind/friends/types.js +8 -0
  76. package/dist/mind/memory.js +27 -26
  77. package/dist/mind/pending.js +76 -9
  78. package/dist/mind/phrases.js +1 -0
  79. package/dist/mind/prompt.js +445 -77
  80. package/dist/mind/token-estimate.js +8 -12
  81. package/dist/nerves/cli-logging.js +15 -2
  82. package/dist/nerves/coverage/run-artifacts.js +1 -1
  83. package/dist/nerves/index.js +12 -0
  84. package/dist/repertoire/ado-client.js +4 -2
  85. package/dist/repertoire/coding/feedback.js +134 -0
  86. package/dist/repertoire/coding/index.js +4 -1
  87. package/dist/repertoire/coding/manager.js +62 -4
  88. package/dist/repertoire/coding/spawner.js +3 -3
  89. package/dist/repertoire/coding/tools.js +41 -2
  90. package/dist/repertoire/data/ado-endpoints.json +188 -0
  91. package/dist/repertoire/guardrails.js +290 -0
  92. package/dist/repertoire/mcp-client.js +254 -0
  93. package/dist/repertoire/mcp-manager.js +195 -0
  94. package/dist/repertoire/skills.js +3 -26
  95. package/dist/repertoire/tasks/board.js +12 -0
  96. package/dist/repertoire/tasks/index.js +23 -9
  97. package/dist/repertoire/tasks/transitions.js +1 -2
  98. package/dist/repertoire/tools-base.js +686 -251
  99. package/dist/repertoire/tools-bluebubbles.js +93 -0
  100. package/dist/repertoire/tools-teams.js +58 -25
  101. package/dist/repertoire/tools.js +95 -53
  102. package/dist/senses/bluebubbles-client.js +210 -5
  103. package/dist/senses/bluebubbles-entry.js +2 -0
  104. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  105. package/dist/senses/bluebubbles-media.js +339 -0
  106. package/dist/senses/bluebubbles-model.js +12 -4
  107. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  108. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  109. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  110. package/dist/senses/bluebubbles.js +894 -45
  111. package/dist/senses/cli-layout.js +187 -0
  112. package/dist/senses/cli.js +405 -156
  113. package/dist/senses/continuity.js +94 -0
  114. package/dist/senses/debug-activity.js +154 -0
  115. package/dist/senses/inner-dialog-worker.js +47 -18
  116. package/dist/senses/inner-dialog.js +377 -83
  117. package/dist/senses/pipeline.js +307 -0
  118. package/dist/senses/teams.js +573 -129
  119. package/dist/senses/trust-gate.js +112 -2
  120. package/package.json +14 -3
  121. package/subagents/README.md +4 -70
  122. package/dist/heart/daemon/specialist-session.js +0 -142
  123. package/dist/heart/daemon/subagent-installer.js +0 -125
  124. package/dist/inner-worker-entry.js +0 -4
  125. package/subagents/work-doer.md +0 -233
  126. package/subagents/work-merger.md +0 -624
  127. package/subagents/work-planner.md +0 -373
@@ -34,12 +34,16 @@ 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"));
45
49
  const identity_1 = require("../identity");
@@ -47,16 +51,201 @@ const runtime_1 = require("../../nerves/runtime");
47
51
  const store_file_1 = require("../../mind/friends/store-file");
48
52
  const types_1 = require("../../mind/friends/types");
49
53
  const ouro_uti_1 = require("./ouro-uti");
50
- const subagent_installer_1 = require("./subagent-installer");
54
+ const ouro_path_installer_1 = require("./ouro-path-installer");
55
+ const skill_management_installer_1 = require("./skill-management-installer");
51
56
  const hatch_flow_1 = require("./hatch-flow");
52
57
  const specialist_orchestrator_1 = require("./specialist-orchestrator");
58
+ const specialist_prompt_1 = require("./specialist-prompt");
59
+ const specialist_tools_1 = require("./specialist-tools");
60
+ const runtime_metadata_1 = require("./runtime-metadata");
61
+ const runtime_mode_1 = require("./runtime-mode");
62
+ const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
63
+ const agent_discovery_1 = require("./agent-discovery");
64
+ const update_hooks_1 = require("./update-hooks");
65
+ const bundle_meta_1 = require("./hooks/bundle-meta");
66
+ const bundle_manifest_1 = require("../../mind/bundle-manifest");
67
+ const tasks_1 = require("../../repertoire/tasks");
68
+ const thoughts_1 = require("./thoughts");
69
+ const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
70
+ const launchd_1 = require("./launchd");
71
+ const socket_client_1 = require("./socket-client");
72
+ const session_activity_1 = require("../session-activity");
73
+ const auth_flow_1 = require("./auth-flow");
74
+ function stringField(value) {
75
+ return typeof value === "string" ? value : null;
76
+ }
77
+ function numberField(value) {
78
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
79
+ }
80
+ function booleanField(value) {
81
+ return typeof value === "boolean" ? value : null;
82
+ }
83
+ function parseStatusPayload(data) {
84
+ if (!data || typeof data !== "object" || Array.isArray(data))
85
+ return null;
86
+ const raw = data;
87
+ const overview = raw.overview;
88
+ const senses = raw.senses;
89
+ const workers = raw.workers;
90
+ if (!overview || typeof overview !== "object" || Array.isArray(overview))
91
+ return null;
92
+ if (!Array.isArray(senses) || !Array.isArray(workers))
93
+ return null;
94
+ const parsedOverview = {
95
+ daemon: stringField(overview.daemon) ?? "unknown",
96
+ health: stringField(overview.health) ?? "unknown",
97
+ socketPath: stringField(overview.socketPath) ?? "unknown",
98
+ version: stringField(overview.version) ?? "unknown",
99
+ lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
100
+ repoRoot: stringField(overview.repoRoot) ?? "unknown",
101
+ configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
102
+ workerCount: numberField(overview.workerCount) ?? 0,
103
+ senseCount: numberField(overview.senseCount) ?? 0,
104
+ entryPath: stringField(overview.entryPath) ?? "unknown",
105
+ mode: stringField(overview.mode) ?? "unknown",
106
+ };
107
+ const parsedSenses = senses.map((entry) => {
108
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
109
+ return null;
110
+ const row = entry;
111
+ const agent = stringField(row.agent);
112
+ const sense = stringField(row.sense);
113
+ const status = stringField(row.status);
114
+ const detail = stringField(row.detail);
115
+ const enabled = booleanField(row.enabled);
116
+ if (!agent || !sense || !status || detail === null || enabled === null)
117
+ return null;
118
+ return {
119
+ agent,
120
+ sense,
121
+ label: stringField(row.label) ?? undefined,
122
+ enabled,
123
+ status,
124
+ detail,
125
+ };
126
+ });
127
+ const parsedWorkers = workers.map((entry) => {
128
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
129
+ return null;
130
+ const row = entry;
131
+ const agent = stringField(row.agent);
132
+ const worker = stringField(row.worker);
133
+ const status = stringField(row.status);
134
+ const restartCount = numberField(row.restartCount);
135
+ const hasPid = Object.prototype.hasOwnProperty.call(row, "pid");
136
+ const pid = row.pid === null ? null : numberField(row.pid);
137
+ const pidInvalid = !hasPid || (row.pid !== null && pid === null);
138
+ if (!agent || !worker || !status || restartCount === null || pidInvalid)
139
+ return null;
140
+ return {
141
+ agent,
142
+ worker,
143
+ status,
144
+ pid,
145
+ restartCount,
146
+ };
147
+ });
148
+ if (parsedSenses.some((row) => row === null) || parsedWorkers.some((row) => row === null))
149
+ return null;
150
+ return {
151
+ overview: parsedOverview,
152
+ senses: parsedSenses,
153
+ workers: parsedWorkers,
154
+ };
155
+ }
156
+ function humanizeSenseName(sense, label) {
157
+ if (label)
158
+ return label;
159
+ if (sense === "cli")
160
+ return "CLI";
161
+ if (sense === "bluebubbles")
162
+ return "BlueBubbles";
163
+ if (sense === "teams")
164
+ return "Teams";
165
+ return sense;
166
+ }
167
+ function formatTable(headers, rows) {
168
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
169
+ const renderRow = (row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
170
+ const divider = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
171
+ return [
172
+ renderRow(headers),
173
+ divider,
174
+ ...rows.map(renderRow),
175
+ ].join("\n");
176
+ }
177
+ function formatDaemonStatusOutput(response, fallback) {
178
+ const payload = parseStatusPayload(response.data);
179
+ if (!payload)
180
+ return fallback;
181
+ const overviewRows = [
182
+ ["Daemon", payload.overview.daemon],
183
+ ["Socket", payload.overview.socketPath],
184
+ ["Version", payload.overview.version],
185
+ ["Last Updated", payload.overview.lastUpdated],
186
+ ["Entry Path", payload.overview.entryPath],
187
+ ["Mode", payload.overview.mode],
188
+ ["Workers", String(payload.overview.workerCount)],
189
+ ["Senses", String(payload.overview.senseCount)],
190
+ ["Health", payload.overview.health],
191
+ ];
192
+ const senseRows = payload.senses.map((row) => [
193
+ row.agent,
194
+ humanizeSenseName(row.sense, row.label),
195
+ row.enabled ? "ON" : "OFF",
196
+ row.status,
197
+ row.detail,
198
+ ]);
199
+ const workerRows = payload.workers.map((row) => [
200
+ row.agent,
201
+ row.worker,
202
+ row.status,
203
+ row.pid === null ? "n/a" : String(row.pid),
204
+ String(row.restartCount),
205
+ ]);
206
+ return [
207
+ "Overview",
208
+ formatTable(["Item", "Value"], overviewRows),
209
+ "",
210
+ "Senses",
211
+ formatTable(["Agent", "Sense", "Enabled", "State", "Detail"], senseRows),
212
+ "",
213
+ "Workers",
214
+ formatTable(["Agent", "Worker", "State", "PID", "Restarts"], workerRows),
215
+ ].join("\n");
216
+ }
53
217
  async function ensureDaemonRunning(deps) {
54
218
  const alive = await deps.checkSocketAlive(deps.socketPath);
55
219
  if (alive) {
56
- return {
57
- alreadyRunning: true,
58
- message: `daemon already running (${deps.socketPath})`,
220
+ const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
221
+ let runningRuntimePromise = null;
222
+ const fetchRunningRuntimeMetadata = async () => {
223
+ runningRuntimePromise ??= (async () => {
224
+ const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
225
+ const payload = parseStatusPayload(status.data);
226
+ return {
227
+ version: payload?.overview.version ?? "unknown",
228
+ lastUpdated: payload?.overview.lastUpdated ?? "unknown",
229
+ repoRoot: payload?.overview.repoRoot ?? "unknown",
230
+ configFingerprint: payload?.overview.configFingerprint ?? "unknown",
231
+ };
232
+ })();
233
+ return runningRuntimePromise;
59
234
  };
235
+ return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
236
+ socketPath: deps.socketPath,
237
+ localVersion: localRuntime.version,
238
+ localLastUpdated: localRuntime.lastUpdated,
239
+ localRepoRoot: localRuntime.repoRoot,
240
+ localConfigFingerprint: localRuntime.configFingerprint,
241
+ fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
242
+ fetchRunningRuntimeMetadata,
243
+ stopDaemon: async () => {
244
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
245
+ },
246
+ cleanupStaleSocket: deps.cleanupStaleSocket,
247
+ startDaemonProcess: deps.startDaemonProcess,
248
+ });
60
249
  }
61
250
  deps.cleanupStaleSocket(deps.socketPath);
62
251
  const started = await deps.startDaemonProcess(deps.socketPath);
@@ -65,17 +254,95 @@ async function ensureDaemonRunning(deps) {
65
254
  message: `daemon started (pid ${started.pid ?? "unknown"})`,
66
255
  };
67
256
  }
257
+ /**
258
+ * Extract `--agent <name>` from an args array, returning the agent name and
259
+ * the remaining args with the flag pair removed.
260
+ */
261
+ function extractAgentFlag(args) {
262
+ const idx = args.indexOf("--agent");
263
+ if (idx === -1 || idx + 1 >= args.length)
264
+ return { rest: args };
265
+ const agent = args[idx + 1];
266
+ const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
267
+ return { agent, rest };
268
+ }
68
269
  function usage() {
69
270
  return [
70
271
  "Usage:",
71
272
  " ouro [up]",
72
- " ouro stop|status|logs|hatch",
273
+ " ouro stop|down|status|logs|hatch",
274
+ " ouro -v|--version",
275
+ " ouro config model --agent <name> <model-name>",
276
+ " ouro config models --agent <name>",
277
+ " ouro auth --agent <name> [--provider <provider>]",
278
+ " ouro auth verify --agent <name> [--provider <provider>]",
279
+ " ouro auth switch --agent <name> --provider <provider>",
73
280
  " ouro chat <agent>",
74
281
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
75
282
  " ouro poke <agent> --task <task-id>",
76
283
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
284
+ " ouro task board [<status>] [--agent <name>]",
285
+ " ouro task create <title> [--type <type>] [--agent <name>]",
286
+ " ouro task update <id> <status> [--agent <name>]",
287
+ " ouro task show <id> [--agent <name>]",
288
+ " ouro task actionable|deps|sessions [--agent <name>]",
289
+ " ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>] [--category <category>] [--agent <name>]",
290
+ " ouro friend list [--agent <name>]",
291
+ " ouro friend show <id> [--agent <name>]",
292
+ " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
293
+ " ouro friend update <id> --trust <level> [--agent <name>]",
294
+ " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
295
+ " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
296
+ " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
297
+ " ouro whoami [--agent <name>]",
298
+ " ouro session list [--agent <name>]",
299
+ " ouro mcp list",
300
+ " ouro mcp call <server> <tool> [--args '{...}']",
301
+ " ouro rollback [<version>]",
302
+ " ouro versions",
303
+ ].join("\n");
304
+ }
305
+ function formatVersionOutput() {
306
+ return (0, runtime_metadata_1.getRuntimeMetadata)().version;
307
+ }
308
+ function buildStoppedStatusPayload(socketPath) {
309
+ const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
310
+ const repoRoot = (0, identity_1.getRepoRoot)();
311
+ return {
312
+ overview: {
313
+ daemon: "stopped",
314
+ health: "warn",
315
+ socketPath,
316
+ version: metadata.version,
317
+ lastUpdated: metadata.lastUpdated,
318
+ repoRoot: metadata.repoRoot,
319
+ configFingerprint: metadata.configFingerprint,
320
+ workerCount: 0,
321
+ senseCount: 0,
322
+ entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
323
+ mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
324
+ },
325
+ senses: [],
326
+ workers: [],
327
+ };
328
+ }
329
+ function daemonUnavailableStatusOutput(socketPath) {
330
+ return [
331
+ formatDaemonStatusOutput({
332
+ ok: true,
333
+ summary: "daemon not running",
334
+ data: buildStoppedStatusPayload(socketPath),
335
+ }, "daemon not running"),
336
+ "",
337
+ "daemon not running; run `ouro up`",
77
338
  ].join("\n");
78
339
  }
340
+ function isDaemonUnavailableError(error) {
341
+ const code = typeof error === "object" && error !== null && "code" in error
342
+ ? String(error.code ?? "")
343
+ : "";
344
+ return code === "ENOENT" || code === "ECONNREFUSED";
345
+ }
79
346
  function parseMessageCommand(args) {
80
347
  let to;
81
348
  let sessionId;
@@ -127,7 +394,7 @@ function parsePokeCommand(args) {
127
394
  throw new Error(`Usage\n${usage()}`);
128
395
  return { kind: "task.poke", agent, taskId };
129
396
  }
130
- function parseLinkCommand(args) {
397
+ function parseLinkCommand(args, kind = "friend.link") {
131
398
  const agent = args[0];
132
399
  if (!agent)
133
400
  throw new Error(`Usage\n${usage()}`);
@@ -159,7 +426,7 @@ function parseLinkCommand(args) {
159
426
  throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation.`);
160
427
  }
161
428
  return {
162
- kind: "friend.link",
429
+ kind,
163
430
  agent,
164
431
  friendId,
165
432
  provider: providerRaw,
@@ -167,7 +434,132 @@ function parseLinkCommand(args) {
167
434
  };
168
435
  }
169
436
  function isAgentProvider(value) {
170
- return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex";
437
+ return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
438
+ }
439
+ /* v8 ignore start -- hasStoredCredentials: per-provider branches tested via auth switch tests @preserve */
440
+ function hasStoredCredentials(provider, providerSecrets) {
441
+ if (provider === "anthropic")
442
+ return !!providerSecrets.setupToken;
443
+ if (provider === "openai-codex")
444
+ return !!providerSecrets.oauthAccessToken;
445
+ if (provider === "github-copilot")
446
+ return !!providerSecrets.githubToken;
447
+ if (provider === "minimax")
448
+ return !!providerSecrets.apiKey;
449
+ // azure
450
+ return !!providerSecrets.endpoint && !!providerSecrets.apiKey;
451
+ }
452
+ /* v8 ignore stop */
453
+ /* v8 ignore start -- verifyProviderCredentials: per-provider branches tested via auth verify tests @preserve */
454
+ async function verifyProviderCredentials(provider, providers, fetchImpl = fetch) {
455
+ const p = providers[provider];
456
+ if (!p)
457
+ return "not configured";
458
+ if (provider === "anthropic") {
459
+ const token = p.setupToken || "";
460
+ if (!token)
461
+ return "failed (no token)";
462
+ if (token.startsWith("sk-ant-"))
463
+ return "ok";
464
+ return "failed (invalid token format)";
465
+ }
466
+ if (provider === "openai-codex") {
467
+ const token = p.oauthAccessToken || "";
468
+ return token ? "ok" : "failed (no token)";
469
+ }
470
+ if (provider === "github-copilot") {
471
+ const token = p.githubToken || "";
472
+ if (!token)
473
+ return "failed (no token)";
474
+ try {
475
+ const response = await fetchImpl("https://api.github.com/copilot_internal/user", {
476
+ headers: { Authorization: `Bearer ${token}` },
477
+ });
478
+ return response.ok ? "ok" : `failed (HTTP ${response.status})`;
479
+ }
480
+ catch (error) {
481
+ return `failed (${error.message})`;
482
+ }
483
+ }
484
+ if (provider === "minimax") {
485
+ const apiKey = p.apiKey || "";
486
+ return apiKey ? "ok" : "failed (no api key)";
487
+ }
488
+ // azure
489
+ const endpoint = p.endpoint || "";
490
+ const apiKey = p.apiKey || "";
491
+ if (!endpoint)
492
+ return "failed (no endpoint)";
493
+ if (!apiKey)
494
+ return "failed (no api key)";
495
+ return "ok";
496
+ }
497
+ async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
498
+ const url = `${baseUrl.replace(/\/+$/, "")}/models`;
499
+ const response = await fetchImpl(url, {
500
+ headers: { Authorization: `Bearer ${token}` },
501
+ });
502
+ if (!response.ok) {
503
+ throw new Error(`model listing failed (HTTP ${response.status})`);
504
+ }
505
+ const body = await response.json();
506
+ /* v8 ignore start -- response shape handling: tested via config-models.test.ts @preserve */
507
+ const items = Array.isArray(body) ? body : (body?.data ?? []);
508
+ return items.map((item) => {
509
+ const rec = item;
510
+ const capabilities = Array.isArray(rec.capabilities)
511
+ ? rec.capabilities.filter((c) => typeof c === "string")
512
+ : undefined;
513
+ return {
514
+ id: String(rec.id ?? rec.name ?? ""),
515
+ name: String(rec.name ?? rec.id ?? ""),
516
+ ...(capabilities ? { capabilities } : {}),
517
+ };
518
+ });
519
+ /* v8 ignore stop */
520
+ }
521
+ async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
522
+ const base = baseUrl.replace(/\/+$/, "");
523
+ const isClaude = model.startsWith("claude");
524
+ const url = isClaude ? `${base}/chat/completions` : `${base}/responses`;
525
+ const body = isClaude
526
+ ? JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 1 })
527
+ : JSON.stringify({ model, input: "ping", max_output_tokens: 16 });
528
+ try {
529
+ const response = await fetchImpl(url, {
530
+ method: "POST",
531
+ headers: {
532
+ Authorization: `Bearer ${token}`,
533
+ "Content-Type": "application/json",
534
+ },
535
+ body,
536
+ });
537
+ if (response.ok)
538
+ return { ok: true };
539
+ let detail = `HTTP ${response.status}`;
540
+ try {
541
+ const json = await response.json();
542
+ /* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
543
+ if (typeof json.error === "string")
544
+ detail = json.error;
545
+ else if (typeof json.error === "object" && json.error !== null) {
546
+ const errObj = json.error;
547
+ if (typeof errObj.message === "string")
548
+ detail = errObj.message;
549
+ }
550
+ else if (typeof json.message === "string")
551
+ detail = json.message;
552
+ /* v8 ignore stop */
553
+ }
554
+ catch {
555
+ // response body not JSON — keep HTTP status
556
+ }
557
+ return { ok: false, error: detail };
558
+ }
559
+ catch (err) {
560
+ /* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
561
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
562
+ }
171
563
  }
172
564
  function parseHatchCommand(args) {
173
565
  let agentName;
@@ -224,7 +616,7 @@ function parseHatchCommand(args) {
224
616
  }
225
617
  }
226
618
  if (providerRaw && !isAgentProvider(providerRaw)) {
227
- throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex.");
619
+ throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex|github-copilot.");
228
620
  }
229
621
  const provider = providerRaw && isAgentProvider(providerRaw) ? providerRaw : undefined;
230
622
  return {
@@ -236,13 +628,293 @@ function parseHatchCommand(args) {
236
628
  migrationPath,
237
629
  };
238
630
  }
631
+ function parseTaskCommand(args) {
632
+ const { agent, rest: cleaned } = extractAgentFlag(args);
633
+ const [sub, ...rest] = cleaned;
634
+ if (!sub)
635
+ throw new Error(`Usage\n${usage()}`);
636
+ if (sub === "board") {
637
+ const status = rest[0];
638
+ return status
639
+ ? { kind: "task.board", status, ...(agent ? { agent } : {}) }
640
+ : { kind: "task.board", ...(agent ? { agent } : {}) };
641
+ }
642
+ if (sub === "create") {
643
+ const title = rest[0];
644
+ if (!title)
645
+ throw new Error(`Usage\n${usage()}`);
646
+ let type;
647
+ for (let i = 1; i < rest.length; i++) {
648
+ if (rest[i] === "--type" && rest[i + 1]) {
649
+ type = rest[i + 1];
650
+ i += 1;
651
+ }
652
+ }
653
+ return type
654
+ ? { kind: "task.create", title, type, ...(agent ? { agent } : {}) }
655
+ : { kind: "task.create", title, ...(agent ? { agent } : {}) };
656
+ }
657
+ if (sub === "update") {
658
+ const id = rest[0];
659
+ const status = rest[1];
660
+ if (!id || !status)
661
+ throw new Error(`Usage\n${usage()}`);
662
+ return { kind: "task.update", id, status, ...(agent ? { agent } : {}) };
663
+ }
664
+ if (sub === "show") {
665
+ const id = rest[0];
666
+ if (!id)
667
+ throw new Error(`Usage\n${usage()}`);
668
+ return { kind: "task.show", id, ...(agent ? { agent } : {}) };
669
+ }
670
+ if (sub === "actionable")
671
+ return { kind: "task.actionable", ...(agent ? { agent } : {}) };
672
+ if (sub === "deps")
673
+ return { kind: "task.deps", ...(agent ? { agent } : {}) };
674
+ if (sub === "sessions")
675
+ return { kind: "task.sessions", ...(agent ? { agent } : {}) };
676
+ throw new Error(`Usage\n${usage()}`);
677
+ }
678
+ function parseAuthCommand(args) {
679
+ const first = args[0];
680
+ // Support both positional (`auth switch`) and flag (`auth --switch`) forms
681
+ if (first === "verify" || first === "switch" || first === "--verify" || first === "--switch") {
682
+ const subcommand = first.replace(/^--/, "");
683
+ const { agent, rest } = extractAgentFlag(args.slice(1));
684
+ let provider;
685
+ /* v8 ignore start -- provider flag parsing: branches tested via CLI parsing tests @preserve */
686
+ for (let i = 0; i < rest.length; i += 1) {
687
+ if (rest[i] === "--provider") {
688
+ const value = rest[i + 1];
689
+ if (!isAgentProvider(value))
690
+ throw new Error(`Usage\n${usage()}`);
691
+ provider = value;
692
+ i += 1;
693
+ continue;
694
+ }
695
+ }
696
+ /* v8 ignore stop */
697
+ /* v8 ignore next -- defensive: agent always provided in tests @preserve */
698
+ if (!agent)
699
+ throw new Error(`Usage\n${usage()}`);
700
+ if (subcommand === "switch") {
701
+ if (!provider)
702
+ throw new Error(`auth switch requires --provider.\n${usage()}`);
703
+ return { kind: "auth.switch", agent, provider };
704
+ }
705
+ return provider ? { kind: "auth.verify", agent, provider } : { kind: "auth.verify", agent };
706
+ }
707
+ const { agent, rest } = extractAgentFlag(args);
708
+ let provider;
709
+ for (let i = 0; i < rest.length; i += 1) {
710
+ if (rest[i] === "--provider") {
711
+ const value = rest[i + 1];
712
+ if (!isAgentProvider(value))
713
+ throw new Error(`Usage\n${usage()}`);
714
+ provider = value;
715
+ i += 1;
716
+ continue;
717
+ }
718
+ }
719
+ if (!agent)
720
+ throw new Error(`Usage\n${usage()}`);
721
+ return provider ? { kind: "auth.run", agent, provider } : { kind: "auth.run", agent };
722
+ }
723
+ function parseReminderCommand(args) {
724
+ const { agent, rest: cleaned } = extractAgentFlag(args);
725
+ const [sub, ...rest] = cleaned;
726
+ if (!sub)
727
+ throw new Error(`Usage\n${usage()}`);
728
+ if (sub === "create") {
729
+ const title = rest[0];
730
+ if (!title)
731
+ throw new Error(`Usage\n${usage()}`);
732
+ let body;
733
+ let scheduledAt;
734
+ let cadence;
735
+ let category;
736
+ let requester;
737
+ for (let i = 1; i < rest.length; i++) {
738
+ if (rest[i] === "--body" && rest[i + 1]) {
739
+ body = rest[i + 1];
740
+ i += 1;
741
+ }
742
+ else if (rest[i] === "--at" && rest[i + 1]) {
743
+ scheduledAt = rest[i + 1];
744
+ i += 1;
745
+ }
746
+ else if (rest[i] === "--cadence" && rest[i + 1]) {
747
+ cadence = rest[i + 1];
748
+ i += 1;
749
+ }
750
+ else if (rest[i] === "--category" && rest[i + 1]) {
751
+ category = rest[i + 1];
752
+ i += 1;
753
+ }
754
+ else if (rest[i] === "--requester" && rest[i + 1]) {
755
+ requester = rest[i + 1];
756
+ i += 1;
757
+ }
758
+ }
759
+ if (!body)
760
+ throw new Error(`Usage\n${usage()}`);
761
+ if (!scheduledAt && !cadence)
762
+ throw new Error(`Usage\n${usage()}`);
763
+ return {
764
+ kind: "reminder.create",
765
+ title,
766
+ body,
767
+ ...(scheduledAt ? { scheduledAt } : {}),
768
+ ...(cadence ? { cadence } : {}),
769
+ ...(category ? { category } : {}),
770
+ ...(requester ? { requester } : {}),
771
+ ...(agent ? { agent } : {}),
772
+ };
773
+ }
774
+ throw new Error(`Usage\n${usage()}`);
775
+ }
776
+ function parseSessionCommand(args) {
777
+ const { agent, rest: cleaned } = extractAgentFlag(args);
778
+ const [sub] = cleaned;
779
+ if (!sub)
780
+ throw new Error(`Usage\n${usage()}`);
781
+ if (sub === "list")
782
+ return { kind: "session.list", ...(agent ? { agent } : {}) };
783
+ throw new Error(`Usage\n${usage()}`);
784
+ }
785
+ function parseThoughtsCommand(args) {
786
+ const { agent, rest: cleaned } = extractAgentFlag(args);
787
+ let last;
788
+ let json = false;
789
+ let follow = false;
790
+ for (let i = 0; i < cleaned.length; i++) {
791
+ if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
792
+ last = Number.parseInt(cleaned[i + 1], 10);
793
+ i++;
794
+ }
795
+ if (cleaned[i] === "--json")
796
+ json = true;
797
+ if (cleaned[i] === "--follow" || cleaned[i] === "-f")
798
+ follow = true;
799
+ }
800
+ return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
801
+ }
802
+ function parseFriendCommand(args) {
803
+ const { agent, rest: cleaned } = extractAgentFlag(args);
804
+ const [sub, ...rest] = cleaned;
805
+ if (!sub)
806
+ throw new Error(`Usage\n${usage()}`);
807
+ if (sub === "list")
808
+ return { kind: "friend.list", ...(agent ? { agent } : {}) };
809
+ if (sub === "show") {
810
+ const friendId = rest[0];
811
+ if (!friendId)
812
+ throw new Error(`Usage\n${usage()}`);
813
+ return { kind: "friend.show", friendId, ...(agent ? { agent } : {}) };
814
+ }
815
+ if (sub === "create") {
816
+ let name;
817
+ let trustLevel;
818
+ for (let i = 0; i < rest.length; i++) {
819
+ if (rest[i] === "--name" && rest[i + 1]) {
820
+ name = rest[i + 1];
821
+ i += 1;
822
+ }
823
+ else if (rest[i] === "--trust" && rest[i + 1]) {
824
+ trustLevel = rest[i + 1];
825
+ i += 1;
826
+ }
827
+ }
828
+ if (!name)
829
+ throw new Error(`Usage\n${usage()}`);
830
+ return {
831
+ kind: "friend.create",
832
+ name,
833
+ ...(trustLevel ? { trustLevel } : {}),
834
+ ...(agent ? { agent } : {}),
835
+ };
836
+ }
837
+ if (sub === "update") {
838
+ const friendId = rest[0];
839
+ if (!friendId)
840
+ throw new Error(`Usage: ouro friend update <id> --trust <level>`);
841
+ let trustLevel;
842
+ /* v8 ignore start -- flag parsing loop: tested via CLI parsing tests @preserve */
843
+ for (let i = 1; i < rest.length; i++) {
844
+ if (rest[i] === "--trust" && rest[i + 1]) {
845
+ trustLevel = rest[i + 1];
846
+ i += 1;
847
+ }
848
+ }
849
+ /* v8 ignore stop */
850
+ const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
851
+ if (!trustLevel || !VALID_TRUST_LEVELS.has(trustLevel)) {
852
+ throw new Error(`Usage: ouro friend update <id> --trust <stranger|acquaintance|friend|family>`);
853
+ }
854
+ return {
855
+ kind: "friend.update",
856
+ friendId,
857
+ trustLevel: trustLevel,
858
+ ...(agent ? { agent } : {}),
859
+ };
860
+ }
861
+ if (sub === "link")
862
+ return parseLinkCommand(rest, "friend.link");
863
+ if (sub === "unlink")
864
+ return parseLinkCommand(rest, "friend.unlink");
865
+ throw new Error(`Usage\n${usage()}`);
866
+ }
867
+ function parseConfigCommand(args) {
868
+ const { agent, rest: cleaned } = extractAgentFlag(args);
869
+ const [sub, ...rest] = cleaned;
870
+ if (!sub)
871
+ throw new Error(`Usage\n${usage()}`);
872
+ if (sub === "model") {
873
+ if (!agent)
874
+ throw new Error("--agent is required for config model");
875
+ const modelName = rest[0];
876
+ if (!modelName)
877
+ throw new Error(`Usage: ouro config model --agent <name> <model-name>`);
878
+ return { kind: "config.model", agent, modelName };
879
+ }
880
+ if (sub === "models") {
881
+ if (!agent)
882
+ throw new Error("--agent is required for config models");
883
+ return { kind: "config.models", agent };
884
+ }
885
+ throw new Error(`Usage\n${usage()}`);
886
+ }
887
+ function parseMcpCommand(args) {
888
+ const [sub, ...rest] = args;
889
+ if (!sub)
890
+ throw new Error(`Usage\n${usage()}`);
891
+ if (sub === "list")
892
+ return { kind: "mcp.list" };
893
+ if (sub === "call") {
894
+ const server = rest[0];
895
+ const tool = rest[1];
896
+ if (!server || !tool)
897
+ throw new Error(`Usage\n${usage()}`);
898
+ const argsIdx = rest.indexOf("--args");
899
+ const mcpArgs = argsIdx !== -1 && rest[argsIdx + 1] ? rest[argsIdx + 1] : undefined;
900
+ return { kind: "mcp.call", server, tool, ...(mcpArgs ? { args: mcpArgs } : {}) };
901
+ }
902
+ throw new Error(`Usage\n${usage()}`);
903
+ }
239
904
  function parseOuroCommand(args) {
240
905
  const [head, second] = args;
241
906
  if (!head)
242
907
  return { kind: "daemon.up" };
908
+ if (head === "--agent" && second) {
909
+ return parseOuroCommand(args.slice(2));
910
+ }
243
911
  if (head === "up")
244
912
  return { kind: "daemon.up" };
245
- if (head === "stop")
913
+ if (head === "rollback")
914
+ return { kind: "rollback", ...(second ? { version: second } : {}) };
915
+ if (head === "versions")
916
+ return { kind: "versions" };
917
+ if (head === "stop" || head === "down")
246
918
  return { kind: "daemon.stop" };
247
919
  if (head === "status")
248
920
  return { kind: "daemon.status" };
@@ -250,6 +922,36 @@ function parseOuroCommand(args) {
250
922
  return { kind: "daemon.logs" };
251
923
  if (head === "hatch")
252
924
  return parseHatchCommand(args.slice(1));
925
+ if (head === "auth")
926
+ return parseAuthCommand(args.slice(1));
927
+ if (head === "task")
928
+ return parseTaskCommand(args.slice(1));
929
+ if (head === "reminder")
930
+ return parseReminderCommand(args.slice(1));
931
+ if (head === "friend")
932
+ return parseFriendCommand(args.slice(1));
933
+ if (head === "config")
934
+ return parseConfigCommand(args.slice(1));
935
+ if (head === "mcp")
936
+ return parseMcpCommand(args.slice(1));
937
+ if (head === "whoami") {
938
+ const { agent } = extractAgentFlag(args.slice(1));
939
+ return { kind: "whoami", ...(agent ? { agent } : {}) };
940
+ }
941
+ if (head === "session")
942
+ return parseSessionCommand(args.slice(1));
943
+ if (head === "changelog") {
944
+ const sliced = args.slice(1);
945
+ const { agent, rest: remaining } = extractAgentFlag(sliced);
946
+ let from;
947
+ const fromIdx = remaining.indexOf("--from");
948
+ if (fromIdx !== -1 && remaining[fromIdx + 1]) {
949
+ from = remaining[fromIdx + 1];
950
+ }
951
+ return { kind: "changelog", ...(from ? { from } : {}), ...(agent ? { agent } : {}) };
952
+ }
953
+ if (head === "thoughts")
954
+ return parseThoughtsCommand(args.slice(1));
253
955
  if (head === "chat") {
254
956
  if (!second)
255
957
  throw new Error(`Usage\n${usage()}`);
@@ -263,38 +965,6 @@ function parseOuroCommand(args) {
263
965
  return parseLinkCommand(args.slice(1));
264
966
  throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
265
967
  }
266
- function defaultSendCommand(socketPath, command) {
267
- return new Promise((resolve, reject) => {
268
- const client = net.createConnection(socketPath);
269
- let raw = "";
270
- client.on("connect", () => {
271
- client.write(JSON.stringify(command));
272
- client.end();
273
- });
274
- client.on("data", (chunk) => {
275
- raw += chunk.toString("utf-8");
276
- });
277
- client.on("error", reject);
278
- client.on("end", () => {
279
- const trimmed = raw.trim();
280
- if (trimmed.length === 0 && command.kind === "daemon.stop") {
281
- resolve({ ok: true, message: "daemon stopped" });
282
- return;
283
- }
284
- if (trimmed.length === 0) {
285
- reject(new Error("Daemon returned empty response."));
286
- return;
287
- }
288
- try {
289
- const parsed = JSON.parse(trimmed);
290
- resolve(parsed);
291
- }
292
- catch (error) {
293
- reject(error);
294
- }
295
- });
296
- });
297
- }
298
968
  function defaultStartDaemonProcess(socketPath) {
299
969
  const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
300
970
  const child = (0, child_process_1.spawn)("node", [entry, "--socket", socketPath], {
@@ -308,45 +978,32 @@ function defaultWriteStdout(text) {
308
978
  // eslint-disable-next-line no-console -- terminal UX: CLI command output
309
979
  console.log(text);
310
980
  }
311
- function defaultCheckSocketAlive(socketPath) {
312
- return new Promise((resolve) => {
313
- const client = net.createConnection(socketPath);
314
- let raw = "";
315
- let done = false;
316
- const finalize = (alive) => {
317
- if (done)
318
- return;
319
- done = true;
320
- resolve(alive);
321
- };
322
- if ("setTimeout" in client && typeof client.setTimeout === "function") {
323
- client.setTimeout(800, () => {
324
- client.destroy();
325
- finalize(false);
326
- });
981
+ /**
982
+ * Read the runtimeVersion from the first .ouro bundle's bundle-meta.json.
983
+ * Returns undefined if none found or unreadable.
984
+ */
985
+ function readFirstBundleMetaVersion(bundlesRoot) {
986
+ try {
987
+ if (!fs.existsSync(bundlesRoot))
988
+ return undefined;
989
+ const entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
990
+ for (const entry of entries) {
991
+ /* v8 ignore next -- skip non-.ouro dirs: tested via version-detect tests @preserve */
992
+ if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
993
+ continue;
994
+ const metaPath = path.join(bundlesRoot, entry.name, "bundle-meta.json");
995
+ if (!fs.existsSync(metaPath))
996
+ continue;
997
+ const raw = fs.readFileSync(metaPath, "utf-8");
998
+ const meta = JSON.parse(raw);
999
+ if (meta.runtimeVersion)
1000
+ return meta.runtimeVersion;
327
1001
  }
328
- client.on("connect", () => {
329
- client.write(JSON.stringify({ kind: "daemon.status" }));
330
- client.end();
331
- });
332
- client.on("data", (chunk) => {
333
- raw += chunk.toString("utf-8");
334
- });
335
- client.on("error", () => finalize(false));
336
- client.on("end", () => {
337
- if (raw.trim().length === 0) {
338
- finalize(false);
339
- return;
340
- }
341
- try {
342
- JSON.parse(raw);
343
- finalize(true);
344
- }
345
- catch {
346
- finalize(false);
347
- }
348
- });
349
- });
1002
+ }
1003
+ catch {
1004
+ // Best effort — return undefined on any error
1005
+ }
1006
+ return undefined;
350
1007
  }
351
1008
  function defaultCleanupStaleSocket(socketPath) {
352
1009
  if (fs.existsSync(socketPath)) {
@@ -382,9 +1039,38 @@ function defaultFallbackPendingMessage(command) {
382
1039
  });
383
1040
  return pendingPath;
384
1041
  }
385
- async function defaultInstallSubagents() {
386
- return (0, subagent_installer_1.installSubagentsForAvailableCli)({
387
- repoRoot: (0, identity_1.getRepoRoot)(),
1042
+ function defaultEnsureDaemonBootPersistence(socketPath) {
1043
+ if (process.platform !== "darwin") {
1044
+ return;
1045
+ }
1046
+ const homeDir = os.homedir();
1047
+ const launchdDeps = {
1048
+ exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
1049
+ writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
1050
+ removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
1051
+ existsFile: (filePath) => fs.existsSync(filePath),
1052
+ mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
1053
+ homeDir,
1054
+ userUid: process.getuid?.() ?? 0,
1055
+ };
1056
+ const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
1057
+ /* 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 */
1058
+ if (!fs.existsSync(entryPath)) {
1059
+ (0, runtime_1.emitNervesEvent)({
1060
+ level: "warn",
1061
+ component: "daemon",
1062
+ event: "daemon.entry_path_missing",
1063
+ message: "entryPath does not exist on disk — plist may point to a stale location. Run 'ouro daemon install' from the correct location.",
1064
+ meta: { entryPath },
1065
+ });
1066
+ }
1067
+ const logDir = (0, identity_1.getAgentDaemonLogsDir)();
1068
+ (0, launchd_1.installLaunchAgent)(launchdDeps, {
1069
+ nodePath: process.execPath,
1070
+ entryPath,
1071
+ socketPath,
1072
+ logDir,
1073
+ envPath: process.env.PATH,
388
1074
  });
389
1075
  }
390
1076
  async function defaultPromptInput(question) {
@@ -402,146 +1088,316 @@ async function defaultPromptInput(question) {
402
1088
  }
403
1089
  }
404
1090
  function defaultListDiscoveredAgents() {
405
- const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
1091
+ return (0, agent_discovery_1.listEnabledBundleAgents)({
1092
+ bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
1093
+ readdirSync: fs.readdirSync,
1094
+ readFileSync: fs.readFileSync,
1095
+ });
1096
+ }
1097
+ function discoverExistingCredentials(secretsRoot) {
1098
+ const found = [];
406
1099
  let entries;
407
1100
  try {
408
- entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
1101
+ entries = fs.readdirSync(secretsRoot, { withFileTypes: true });
409
1102
  }
410
1103
  catch {
411
- return [];
1104
+ return found;
412
1105
  }
413
- const discovered = [];
414
1106
  for (const entry of entries) {
415
- if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
1107
+ if (!entry.isDirectory())
416
1108
  continue;
417
- const agentName = entry.name.slice(0, -5);
418
- const configPath = path.join(bundlesRoot, entry.name, "agent.json");
419
- let enabled = true;
1109
+ const secretsPath = path.join(secretsRoot, entry.name, "secrets.json");
1110
+ let raw;
420
1111
  try {
421
- const raw = fs.readFileSync(configPath, "utf-8");
422
- const parsed = JSON.parse(raw);
423
- if (typeof parsed.enabled === "boolean") {
424
- enabled = parsed.enabled;
425
- }
1112
+ raw = fs.readFileSync(secretsPath, "utf-8");
426
1113
  }
427
1114
  catch {
428
1115
  continue;
429
1116
  }
430
- if (enabled) {
431
- discovered.push(agentName);
1117
+ let parsed;
1118
+ try {
1119
+ parsed = JSON.parse(raw);
1120
+ }
1121
+ catch {
1122
+ continue;
1123
+ }
1124
+ if (!parsed.providers)
1125
+ continue;
1126
+ for (const [provName, provConfig] of Object.entries(parsed.providers)) {
1127
+ if (provName === "anthropic" && provConfig.setupToken) {
1128
+ found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken }, providerConfig: { ...provConfig } });
1129
+ }
1130
+ else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
1131
+ found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken }, providerConfig: { ...provConfig } });
1132
+ }
1133
+ else if (provName === "minimax" && provConfig.apiKey) {
1134
+ found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey }, providerConfig: { ...provConfig } });
1135
+ }
1136
+ else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
1137
+ found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
1138
+ }
432
1139
  }
433
1140
  }
434
- return discovered.sort((left, right) => left.localeCompare(right));
435
- }
436
- async function defaultLinkFriendIdentity(command) {
437
- const friendStore = new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends"));
438
- const current = await friendStore.get(command.friendId);
439
- if (!current) {
440
- return `friend not found: ${command.friendId}`;
441
- }
442
- const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
443
- if (alreadyLinked) {
444
- return `identity already linked: ${command.provider}:${command.externalId}`;
445
- }
446
- const now = new Date().toISOString();
447
- await friendStore.put(command.friendId, {
448
- ...current,
449
- externalIds: [
450
- ...current.externalIds,
451
- {
452
- provider: command.provider,
453
- externalId: command.externalId,
454
- linkedAt: now,
455
- },
456
- ],
457
- updatedAt: now,
1141
+ // Deduplicate by provider+credential value (keep first seen)
1142
+ const seen = new Set();
1143
+ return found.filter((cred) => {
1144
+ const key = `${cred.provider}:${JSON.stringify(cred.credentials)}`;
1145
+ if (seen.has(key))
1146
+ return false;
1147
+ seen.add(key);
1148
+ return true;
458
1149
  });
459
- return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
460
1150
  }
461
- /* v8 ignore next 49 -- integration: interactive terminal specialist session @preserve */
1151
+ /* v8 ignore start -- integration: interactive terminal specialist session @preserve */
462
1152
  async function defaultRunAdoptionSpecialist() {
463
- const readline = await Promise.resolve().then(() => __importStar(require("readline/promises")));
464
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
465
- const prompt = async (q) => {
466
- const answer = await rl.question(q);
1153
+ const { runCliSession } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
1154
+ const { patchRuntimeConfig } = await Promise.resolve().then(() => __importStar(require("../config")));
1155
+ const { setAgentName, setAgentConfigOverride } = await Promise.resolve().then(() => __importStar(require("../identity")));
1156
+ const readlinePromises = await Promise.resolve().then(() => __importStar(require("readline/promises")));
1157
+ const crypto = await Promise.resolve().then(() => __importStar(require("crypto")));
1158
+ // Phase 1: cold CLI — collect provider/credentials with a simple readline
1159
+ const coldRl = readlinePromises.createInterface({ input: process.stdin, output: process.stdout });
1160
+ const coldPrompt = async (q) => {
1161
+ const answer = await coldRl.question(q);
467
1162
  return answer.trim();
468
1163
  };
1164
+ let providerRaw;
1165
+ let credentials = {};
1166
+ let providerConfig = {};
1167
+ const tempDir = path.join(os.tmpdir(), `ouro-hatch-${crypto.randomUUID()}`);
469
1168
  try {
470
- process.stdout.write("\nwelcome to ouro. let's get you set up.\n");
471
- process.stdout.write("i need an API key to power our conversation.\n\n");
472
- const providerRaw = await prompt("provider (anthropic/azure/minimax/openai-codex): ");
473
- if (!isAgentProvider(providerRaw)) {
474
- process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
475
- return null;
1169
+ const secretsRoot = path.join(os.homedir(), ".agentsecrets");
1170
+ const discovered = discoverExistingCredentials(secretsRoot);
1171
+ const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
1172
+ const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
1173
+ // Default models per provider (used when entering new credentials)
1174
+ const defaultModels = {
1175
+ anthropic: "claude-opus-4-6",
1176
+ minimax: "MiniMax-Text-01",
1177
+ "openai-codex": "gpt-5.4",
1178
+ "github-copilot": "claude-sonnet-4.6",
1179
+ azure: "",
1180
+ };
1181
+ if (discovered.length > 0) {
1182
+ process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
1183
+ process.stdout.write("i found existing API credentials:\n\n");
1184
+ const unique = [...new Map(discovered.map((d) => [`${d.provider}`, d])).values()];
1185
+ for (let i = 0; i < unique.length; i++) {
1186
+ const model = unique[i].providerConfig.model || unique[i].providerConfig.deployment || "";
1187
+ const modelLabel = model ? `, ${model}` : "";
1188
+ process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (from ${unique[i].agentName})\n`);
1189
+ }
1190
+ process.stdout.write("\n");
1191
+ const choice = await coldPrompt("use one of these? enter number, or 'new' for a different key: ");
1192
+ const idx = parseInt(choice, 10) - 1;
1193
+ if (idx >= 0 && idx < unique.length) {
1194
+ providerRaw = unique[idx].provider;
1195
+ credentials = unique[idx].credentials;
1196
+ providerConfig = unique[idx].providerConfig;
1197
+ }
1198
+ else {
1199
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
1200
+ if (!isAgentProvider(pRaw)) {
1201
+ process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
1202
+ coldRl.close();
1203
+ return null;
1204
+ }
1205
+ providerRaw = pRaw;
1206
+ providerConfig = { model: defaultModels[providerRaw] };
1207
+ if (providerRaw === "anthropic")
1208
+ credentials.setupToken = await coldPrompt("API key: ");
1209
+ if (providerRaw === "openai-codex")
1210
+ credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
1211
+ if (providerRaw === "minimax")
1212
+ credentials.apiKey = await coldPrompt("API key: ");
1213
+ if (providerRaw === "azure") {
1214
+ credentials.apiKey = await coldPrompt("API key: ");
1215
+ credentials.endpoint = await coldPrompt("endpoint: ");
1216
+ credentials.deployment = await coldPrompt("deployment: ");
1217
+ }
1218
+ }
476
1219
  }
477
- const credentials = {};
478
- if (providerRaw === "anthropic")
479
- credentials.setupToken = await prompt("API key: ");
480
- if (providerRaw === "openai-codex")
481
- credentials.oauthAccessToken = await prompt("OAuth token: ");
482
- if (providerRaw === "minimax")
483
- credentials.apiKey = await prompt("API key: ");
484
- if (providerRaw === "azure") {
485
- credentials.apiKey = await prompt("API key: ");
486
- credentials.endpoint = await prompt("endpoint: ");
487
- credentials.deployment = await prompt("deployment: ");
1220
+ else {
1221
+ process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
1222
+ process.stdout.write("i need an API key to power our conversation.\n\n");
1223
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
1224
+ if (!isAgentProvider(pRaw)) {
1225
+ process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
1226
+ coldRl.close();
1227
+ return null;
1228
+ }
1229
+ providerRaw = pRaw;
1230
+ providerConfig = { model: defaultModels[providerRaw] };
1231
+ if (providerRaw === "anthropic")
1232
+ credentials.setupToken = await coldPrompt("API key: ");
1233
+ if (providerRaw === "openai-codex")
1234
+ credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
1235
+ if (providerRaw === "minimax")
1236
+ credentials.apiKey = await coldPrompt("API key: ");
1237
+ if (providerRaw === "azure") {
1238
+ credentials.apiKey = await coldPrompt("API key: ");
1239
+ credentials.endpoint = await coldPrompt("endpoint: ");
1240
+ credentials.deployment = await coldPrompt("deployment: ");
1241
+ }
488
1242
  }
489
- rl.close();
1243
+ coldRl.close();
490
1244
  process.stdout.write("\n");
491
- // Locate the bundled AdoptionSpecialist.ouro shipped with the npm package
1245
+ // Phase 2: configure runtime for adoption specialist
492
1246
  const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
493
1247
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
494
- const secretsRoot = path.join(os.homedir(), ".agentsecrets");
495
- return await (0, specialist_orchestrator_1.runAdoptionSpecialist)({
496
- bundleSourceDir,
497
- bundlesRoot,
498
- secretsRoot,
1248
+ const secretsRoot2 = path.join(os.homedir(), ".agentsecrets");
1249
+ // Suppress non-critical log noise during adoption (no secrets.json, etc.)
1250
+ const { setRuntimeLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
1251
+ const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
1252
+ setRuntimeLogger(createLogger({ level: "error" }));
1253
+ // Configure runtime: set agent identity + config override so runAgent
1254
+ // doesn't try to read from ~/AgentBundles/AdoptionSpecialist.ouro/
1255
+ setAgentName("AdoptionSpecialist");
1256
+ // Build specialist system prompt
1257
+ const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
1258
+ const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
1259
+ const identity = (0, specialist_orchestrator_1.pickRandomIdentity)(identitiesDir);
1260
+ // Load identity-specific spinner phrases (falls back to DEFAULT_AGENT_PHRASES)
1261
+ const { loadIdentityPhrases } = await Promise.resolve().then(() => __importStar(require("./specialist-orchestrator")));
1262
+ const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
1263
+ setAgentConfigOverride({
1264
+ version: 1,
1265
+ enabled: true,
499
1266
  provider: providerRaw,
500
- credentials,
501
- humanName: os.userInfo().username,
502
- createReadline: () => {
503
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
504
- return { question: (q) => rl2.question(q), close: () => rl2.close() };
505
- },
506
- callbacks: {
507
- onModelStart: () => { },
508
- onModelStreamStart: () => { },
509
- onTextChunk: (text) => process.stdout.write(text),
510
- onReasoningChunk: () => { },
511
- onToolStart: () => { },
512
- onToolEnd: () => { },
513
- onError: (err) => process.stderr.write(`error: ${err.message}\n`),
1267
+ phrases,
1268
+ });
1269
+ patchRuntimeConfig({
1270
+ providers: {
1271
+ [providerRaw]: { ...providerConfig, ...credentials },
514
1272
  },
515
1273
  });
1274
+ const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
1275
+ const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
1276
+ tempDir,
1277
+ provider: providerRaw,
1278
+ });
1279
+ // Build specialist tools
1280
+ const specialistTools = (0, specialist_tools_1.getSpecialistTools)();
1281
+ const specialistExecTool = (0, specialist_tools_1.createSpecialistExecTool)({
1282
+ tempDir,
1283
+ credentials,
1284
+ provider: providerRaw,
1285
+ bundlesRoot,
1286
+ secretsRoot: secretsRoot2,
1287
+ animationWriter: (text) => process.stdout.write(text),
1288
+ });
1289
+ // Run the adoption specialist session via runCliSession
1290
+ const result = await runCliSession({
1291
+ agentName: "AdoptionSpecialist",
1292
+ tools: specialistTools,
1293
+ execTool: specialistExecTool,
1294
+ exitOnToolCall: "complete_adoption",
1295
+ autoFirstTurn: true,
1296
+ banner: false,
1297
+ disableCommands: true,
1298
+ skipSystemPromptRefresh: true,
1299
+ messages: [
1300
+ { role: "system", content: systemPrompt },
1301
+ { role: "user", content: "hi" },
1302
+ ],
1303
+ });
1304
+ if (result.exitReason === "tool_exit" && result.toolResult) {
1305
+ const parsed = typeof result.toolResult === "string" ? JSON.parse(result.toolResult) : result.toolResult;
1306
+ if (parsed.success && parsed.agentName) {
1307
+ return parsed.agentName;
1308
+ }
1309
+ }
1310
+ return null;
516
1311
  }
517
- catch {
518
- rl.close();
1312
+ catch (err) {
1313
+ process.stderr.write(`\nouro adoption error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
1314
+ coldRl.close();
519
1315
  return null;
520
1316
  }
1317
+ finally {
1318
+ // Clear specialist config/identity so the hatched agent gets its own
1319
+ setAgentConfigOverride(null);
1320
+ const { resetProviderRuntime } = await Promise.resolve().then(() => __importStar(require("../core")));
1321
+ resetProviderRuntime();
1322
+ const { resetConfigCache } = await Promise.resolve().then(() => __importStar(require("../config")));
1323
+ resetConfigCache();
1324
+ // Restore default logging
1325
+ const { setRuntimeLogger: restoreLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
1326
+ restoreLogger(null);
1327
+ // Clean up temp dir if it still exists
1328
+ try {
1329
+ if (fs.existsSync(tempDir)) {
1330
+ fs.rmSync(tempDir, { recursive: true, force: true });
1331
+ }
1332
+ }
1333
+ catch {
1334
+ // Best effort cleanup
1335
+ }
1336
+ }
521
1337
  }
522
- function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
1338
+ /* v8 ignore stop */
1339
+ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH) {
523
1340
  return {
524
1341
  socketPath,
525
- sendCommand: defaultSendCommand,
1342
+ sendCommand: socket_client_1.sendDaemonCommand,
526
1343
  startDaemonProcess: defaultStartDaemonProcess,
527
1344
  writeStdout: defaultWriteStdout,
528
- checkSocketAlive: defaultCheckSocketAlive,
1345
+ checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
529
1346
  cleanupStaleSocket: defaultCleanupStaleSocket,
530
1347
  fallbackPendingMessage: defaultFallbackPendingMessage,
531
- installSubagents: defaultInstallSubagents,
532
- linkFriendIdentity: defaultLinkFriendIdentity,
533
1348
  listDiscoveredAgents: defaultListDiscoveredAgents,
534
1349
  runHatchFlow: hatch_flow_1.runHatchFlow,
535
1350
  promptInput: defaultPromptInput,
536
1351
  runAdoptionSpecialist: defaultRunAdoptionSpecialist,
1352
+ runAuthFlow: auth_flow_1.runRuntimeAuthFlow,
537
1353
  registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
1354
+ installOuroCommand: ouro_path_installer_1.installOuroCommand,
1355
+ syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
1356
+ ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
1357
+ ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
538
1358
  /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
539
1359
  startChat: async (agentName) => {
540
1360
  const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
541
1361
  await main(agentName);
542
1362
  },
1363
+ scanSessions: async () => {
1364
+ const agentName = (0, identity_1.getAgentName)();
1365
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
1366
+ return (0, session_activity_1.listSessionActivity)({
1367
+ sessionsDir: path.join(agentRoot, "state", "sessions"),
1368
+ friendsDir: path.join(agentRoot, "friends"),
1369
+ agentName,
1370
+ }).map((entry) => ({
1371
+ friendId: entry.friendId,
1372
+ friendName: entry.friendName,
1373
+ channel: entry.channel,
1374
+ lastActivity: entry.lastActivityAt,
1375
+ }));
1376
+ },
543
1377
  };
544
1378
  }
1379
+ function formatMcpResponse(command, response) {
1380
+ if (command.kind === "mcp.list") {
1381
+ const allTools = response.data;
1382
+ if (!allTools || allTools.length === 0) {
1383
+ return response.message ?? "no tools available from connected MCP servers";
1384
+ }
1385
+ const lines = [];
1386
+ for (const entry of allTools) {
1387
+ lines.push(`[${entry.server}]`);
1388
+ for (const tool of entry.tools) {
1389
+ lines.push(` ${tool.name}: ${tool.description}`);
1390
+ }
1391
+ }
1392
+ return lines.join("\n");
1393
+ }
1394
+ // mcp.call
1395
+ const result = response.data;
1396
+ if (!result) {
1397
+ return response.message ?? "no result";
1398
+ }
1399
+ return result.content.map((c) => c.text).join("\n");
1400
+ }
545
1401
  function toDaemonCommand(command) {
546
1402
  return command;
547
1403
  }
@@ -549,28 +1405,17 @@ async function resolveHatchInput(command, deps) {
549
1405
  const prompt = deps.promptInput;
550
1406
  const agentName = command.agentName ?? (prompt ? await prompt("Hatchling name: ") : "");
551
1407
  const humanName = command.humanName ?? (prompt ? await prompt("Your name: ") : os.userInfo().username);
552
- const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex): ") : "");
1408
+ const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex|github-copilot): ") : "");
553
1409
  if (!agentName || !humanName || !isAgentProvider(providerRaw)) {
554
1410
  throw new Error(`Usage\n${usage()}`);
555
1411
  }
556
- const credentials = { ...(command.credentials ?? {}) };
557
- if (providerRaw === "anthropic" && !credentials.setupToken && prompt) {
558
- credentials.setupToken = await prompt("Anthropic setup-token: ");
559
- }
560
- if (providerRaw === "openai-codex" && !credentials.oauthAccessToken && prompt) {
561
- credentials.oauthAccessToken = await prompt("OpenAI Codex OAuth token: ");
562
- }
563
- if (providerRaw === "minimax" && !credentials.apiKey && prompt) {
564
- credentials.apiKey = await prompt("MiniMax API key: ");
565
- }
566
- if (providerRaw === "azure") {
567
- if (!credentials.apiKey && prompt)
568
- credentials.apiKey = await prompt("Azure API key: ");
569
- if (!credentials.endpoint && prompt)
570
- credentials.endpoint = await prompt("Azure endpoint: ");
571
- if (!credentials.deployment && prompt)
572
- credentials.deployment = await prompt("Azure deployment: ");
573
- }
1412
+ const credentials = await (0, auth_flow_1.resolveHatchCredentials)({
1413
+ agentName,
1414
+ provider: providerRaw,
1415
+ credentials: command.credentials,
1416
+ promptInput: prompt,
1417
+ runAuthFlow: deps.runAuthFlow,
1418
+ });
574
1419
  return {
575
1420
  agentName,
576
1421
  humanName,
@@ -596,12 +1441,257 @@ async function registerOuroBundleTypeNonBlocking(deps) {
596
1441
  });
597
1442
  }
598
1443
  }
1444
+ async function performSystemSetup(deps) {
1445
+ // Install ouro command to PATH (non-blocking)
1446
+ if (deps.installOuroCommand) {
1447
+ try {
1448
+ const installResult = deps.installOuroCommand();
1449
+ /* v8 ignore next -- migration hint: only fires once during old→new layout migration @preserve */
1450
+ if (installResult.migratedFromOldPath) {
1451
+ deps.writeStdout("migrated ouro to ~/.ouro-cli/ — open a new terminal or run: source ~/.zshrc");
1452
+ }
1453
+ }
1454
+ catch (error) {
1455
+ (0, runtime_1.emitNervesEvent)({
1456
+ level: "warn",
1457
+ component: "daemon",
1458
+ event: "daemon.system_setup_ouro_cmd_error",
1459
+ message: "failed to install ouro command to PATH",
1460
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1461
+ });
1462
+ }
1463
+ }
1464
+ if (deps.syncGlobalOuroBotWrapper) {
1465
+ try {
1466
+ await Promise.resolve(deps.syncGlobalOuroBotWrapper());
1467
+ }
1468
+ catch (error) {
1469
+ (0, runtime_1.emitNervesEvent)({
1470
+ level: "warn",
1471
+ component: "daemon",
1472
+ event: "daemon.system_setup_ouro_bot_wrapper_error",
1473
+ message: "failed to sync global ouro.bot wrapper",
1474
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1475
+ });
1476
+ }
1477
+ }
1478
+ // Ensure skill-management skill is available
1479
+ if (deps.ensureSkillManagement) {
1480
+ try {
1481
+ await deps.ensureSkillManagement();
1482
+ /* v8 ignore start -- defensive: ensureSkillManagement handles its own errors internally @preserve */
1483
+ }
1484
+ catch (error) {
1485
+ (0, runtime_1.emitNervesEvent)({
1486
+ level: "warn",
1487
+ component: "daemon",
1488
+ event: "daemon.system_setup_skill_management_error",
1489
+ message: "failed to ensure skill-management skill",
1490
+ meta: { error: error instanceof Error ? error.message : String(error) },
1491
+ });
1492
+ }
1493
+ /* v8 ignore stop */
1494
+ }
1495
+ // Register .ouro bundle type (UTI on macOS)
1496
+ await registerOuroBundleTypeNonBlocking(deps);
1497
+ }
1498
+ function executeTaskCommand(command, taskMod) {
1499
+ if (command.kind === "task.board") {
1500
+ if (command.status) {
1501
+ const lines = taskMod.boardStatus(command.status);
1502
+ return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
1503
+ }
1504
+ const board = taskMod.getBoard();
1505
+ return board.full || board.compact || "no tasks found";
1506
+ }
1507
+ if (command.kind === "task.create") {
1508
+ try {
1509
+ const created = taskMod.createTask({
1510
+ title: command.title,
1511
+ type: command.type ?? "one-shot",
1512
+ category: "general",
1513
+ body: "",
1514
+ });
1515
+ return `created: ${created}`;
1516
+ }
1517
+ catch (error) {
1518
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1519
+ }
1520
+ }
1521
+ if (command.kind === "task.update") {
1522
+ const result = taskMod.updateStatus(command.id, command.status);
1523
+ if (!result.ok) {
1524
+ return `error: ${result.reason ?? "status update failed"}`;
1525
+ }
1526
+ const archivedSuffix = result.archived && result.archived.length > 0
1527
+ ? ` | archived: ${result.archived.join(", ")}`
1528
+ : "";
1529
+ return `updated: ${command.id} -> ${result.to}${archivedSuffix}`;
1530
+ }
1531
+ if (command.kind === "task.show") {
1532
+ const task = taskMod.getTask(command.id);
1533
+ if (!task)
1534
+ return `task not found: ${command.id}`;
1535
+ return [
1536
+ `title: ${task.title}`,
1537
+ `type: ${task.type}`,
1538
+ `status: ${task.status}`,
1539
+ `category: ${task.category}`,
1540
+ `created: ${task.created}`,
1541
+ `updated: ${task.updated}`,
1542
+ `path: ${task.path}`,
1543
+ task.body ? `\n${task.body}` : "",
1544
+ ].filter(Boolean).join("\n");
1545
+ }
1546
+ if (command.kind === "task.actionable") {
1547
+ const lines = taskMod.boardAction();
1548
+ return lines.length > 0 ? lines.join("\n") : "no action required";
1549
+ }
1550
+ if (command.kind === "task.deps") {
1551
+ const lines = taskMod.boardDeps();
1552
+ return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
1553
+ }
1554
+ // command.kind === "task.sessions"
1555
+ const lines = taskMod.boardSessions();
1556
+ return lines.length > 0 ? lines.join("\n") : "no active sessions";
1557
+ }
1558
+ const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
1559
+ /* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
1560
+ function higherTrust(a, b) {
1561
+ const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
1562
+ const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
1563
+ return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
1564
+ }
1565
+ /* v8 ignore stop */
1566
+ async function executeFriendCommand(command, store) {
1567
+ if (command.kind === "friend.list") {
1568
+ const listAll = store.listAll;
1569
+ if (!listAll)
1570
+ return "friend store does not support listing";
1571
+ const friends = await listAll.call(store);
1572
+ if (friends.length === 0)
1573
+ return "no friends found";
1574
+ const lines = friends.map((f) => {
1575
+ const trust = f.trustLevel ?? "unknown";
1576
+ return `${f.id} ${f.name} ${trust}`;
1577
+ });
1578
+ return lines.join("\n");
1579
+ }
1580
+ if (command.kind === "friend.show") {
1581
+ const record = await store.get(command.friendId);
1582
+ if (!record)
1583
+ return `friend not found: ${command.friendId}`;
1584
+ return JSON.stringify(record, null, 2);
1585
+ }
1586
+ if (command.kind === "friend.create") {
1587
+ const now = new Date().toISOString();
1588
+ const id = (0, crypto_1.randomUUID)();
1589
+ const trustLevel = (command.trustLevel ?? "acquaintance");
1590
+ await store.put(id, {
1591
+ id,
1592
+ name: command.name,
1593
+ trustLevel,
1594
+ externalIds: [],
1595
+ tenantMemberships: [],
1596
+ toolPreferences: {},
1597
+ notes: {},
1598
+ totalTokens: 0,
1599
+ createdAt: now,
1600
+ updatedAt: now,
1601
+ schemaVersion: 1,
1602
+ });
1603
+ return `created: ${id} (${command.name}, ${trustLevel})`;
1604
+ }
1605
+ if (command.kind === "friend.update") {
1606
+ const current = await store.get(command.friendId);
1607
+ if (!current)
1608
+ return `friend not found: ${command.friendId}`;
1609
+ const now = new Date().toISOString();
1610
+ await store.put(command.friendId, {
1611
+ ...current,
1612
+ trustLevel: command.trustLevel,
1613
+ role: command.trustLevel,
1614
+ updatedAt: now,
1615
+ });
1616
+ return `updated: ${command.friendId} → trust=${command.trustLevel}`;
1617
+ }
1618
+ if (command.kind === "friend.link") {
1619
+ const current = await store.get(command.friendId);
1620
+ if (!current)
1621
+ return `friend not found: ${command.friendId}`;
1622
+ const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
1623
+ if (alreadyLinked)
1624
+ return `identity already linked: ${command.provider}:${command.externalId}`;
1625
+ const now = new Date().toISOString();
1626
+ const newExternalIds = [
1627
+ ...current.externalIds,
1628
+ { provider: command.provider, externalId: command.externalId, linkedAt: now },
1629
+ ];
1630
+ // Orphan cleanup: check if another friend has this externalId
1631
+ const orphan = await store.findByExternalId(command.provider, command.externalId);
1632
+ let mergeMessage = "";
1633
+ let mergedNotes = { ...current.notes };
1634
+ let mergedTrust = current.trustLevel;
1635
+ let orphanExternalIds = [];
1636
+ if (orphan && orphan.id !== command.friendId) {
1637
+ // Merge orphan's notes (target's notes take priority)
1638
+ mergedNotes = { ...orphan.notes, ...current.notes };
1639
+ // Keep higher trust level
1640
+ mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
1641
+ // Collect orphan's other externalIds (excluding the one being linked)
1642
+ orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
1643
+ await store.delete(orphan.id);
1644
+ mergeMessage = ` (merged orphan ${orphan.id})`;
1645
+ }
1646
+ await store.put(command.friendId, {
1647
+ ...current,
1648
+ externalIds: [...newExternalIds, ...orphanExternalIds],
1649
+ notes: mergedNotes,
1650
+ trustLevel: mergedTrust,
1651
+ updatedAt: now,
1652
+ });
1653
+ return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
1654
+ }
1655
+ // command.kind === "friend.unlink"
1656
+ const current = await store.get(command.friendId);
1657
+ if (!current)
1658
+ return `friend not found: ${command.friendId}`;
1659
+ const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
1660
+ if (idx === -1)
1661
+ return `identity not linked: ${command.provider}:${command.externalId}`;
1662
+ const now = new Date().toISOString();
1663
+ const filtered = current.externalIds.filter((_, i) => i !== idx);
1664
+ await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
1665
+ return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
1666
+ }
1667
+ function executeReminderCommand(command, taskMod) {
1668
+ try {
1669
+ const created = taskMod.createTask({
1670
+ title: command.title,
1671
+ type: command.cadence ? "habit" : "one-shot",
1672
+ category: command.category ?? "reminder",
1673
+ body: command.body,
1674
+ scheduledAt: command.scheduledAt,
1675
+ cadence: command.cadence,
1676
+ requester: command.requester,
1677
+ });
1678
+ return `created: ${created}`;
1679
+ }
1680
+ catch (error) {
1681
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1682
+ }
1683
+ }
599
1684
  async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
600
1685
  if (args.includes("--help") || args.includes("-h")) {
601
1686
  const text = usage();
602
1687
  deps.writeStdout(text);
603
1688
  return text;
604
1689
  }
1690
+ if (args.length === 1 && (args[0] === "-v" || args[0] === "--version")) {
1691
+ const text = formatVersionOutput();
1692
+ deps.writeStdout(text);
1693
+ return text;
1694
+ }
605
1695
  let command;
606
1696
  try {
607
1697
  command = parseOuroCommand(args);
@@ -620,23 +1710,12 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
620
1710
  if (args.length === 0) {
621
1711
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : defaultListDiscoveredAgents());
622
1712
  if (discovered.length === 0 && deps.runAdoptionSpecialist) {
1713
+ // System setup first — ouro command, subagents, UTI — before the interactive specialist
1714
+ await performSystemSetup(deps);
623
1715
  const hatchlingName = await deps.runAdoptionSpecialist();
624
1716
  if (!hatchlingName) {
625
1717
  return "";
626
1718
  }
627
- try {
628
- await deps.installSubagents();
629
- }
630
- catch (error) {
631
- (0, runtime_1.emitNervesEvent)({
632
- level: "warn",
633
- component: "daemon",
634
- event: "daemon.subagent_install_error",
635
- message: "subagent auto-install failed",
636
- meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
637
- });
638
- }
639
- await registerOuroBundleTypeNonBlocking(deps);
640
1719
  await ensureDaemonRunning(deps);
641
1720
  if (deps.startChat) {
642
1721
  await deps.startChat(hatchlingName);
@@ -683,54 +1762,473 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
683
1762
  meta: { kind: command.kind },
684
1763
  });
685
1764
  if (command.kind === "daemon.up") {
686
- try {
687
- await deps.installSubagents();
1765
+ // ── versioned CLI update check ──
1766
+ if (deps.checkForCliUpdate) {
1767
+ let pendingReExec = false;
1768
+ try {
1769
+ const updateResult = await deps.checkForCliUpdate();
1770
+ if (updateResult.available && updateResult.latestVersion) {
1771
+ /* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
1772
+ const currentVersion = deps.getCurrentCliVersion?.() ?? "unknown";
1773
+ await deps.installCliVersion(updateResult.latestVersion);
1774
+ deps.activateCliVersion(updateResult.latestVersion);
1775
+ deps.writeStdout(`ouro updated to ${updateResult.latestVersion} (was ${currentVersion})`);
1776
+ pendingReExec = true;
1777
+ }
1778
+ /* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
1779
+ }
1780
+ catch (error) {
1781
+ (0, runtime_1.emitNervesEvent)({
1782
+ level: "warn",
1783
+ component: "daemon",
1784
+ event: "daemon.cli_update_check_error",
1785
+ message: "CLI update check failed",
1786
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1787
+ });
1788
+ }
1789
+ /* v8 ignore stop */
1790
+ if (pendingReExec) {
1791
+ deps.reExecFromNewVersion(args);
1792
+ }
688
1793
  }
689
- catch (error) {
690
- (0, runtime_1.emitNervesEvent)({
691
- level: "warn",
692
- component: "daemon",
693
- event: "daemon.subagent_install_error",
694
- message: "subagent auto-install failed",
695
- meta: { error: error instanceof Error ? error.message : String(error) },
696
- });
1794
+ await performSystemSetup(deps);
1795
+ if (deps.ensureDaemonBootPersistence) {
1796
+ try {
1797
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
1798
+ }
1799
+ catch (error) {
1800
+ (0, runtime_1.emitNervesEvent)({
1801
+ level: "warn",
1802
+ component: "daemon",
1803
+ event: "daemon.system_setup_launchd_error",
1804
+ message: "failed to persist daemon boot startup",
1805
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
1806
+ });
1807
+ }
1808
+ }
1809
+ // Run update hooks before starting daemon so user sees the output
1810
+ (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
1811
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
1812
+ const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
1813
+ // Snapshot the previous CLI version from the first bundle-meta before
1814
+ // hooks overwrite it. This detects when npx downloaded a newer CLI.
1815
+ const previousCliVersion = readFirstBundleMetaVersion(bundlesRoot);
1816
+ const updateSummary = await (0, update_hooks_1.applyPendingUpdates)(bundlesRoot, currentVersion);
1817
+ // Notify about CLI binary update (npx downloaded a new version)
1818
+ /* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
1819
+ if (previousCliVersion && previousCliVersion !== currentVersion) {
1820
+ deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
1821
+ }
1822
+ /* v8 ignore stop */
1823
+ if (updateSummary.updated.length > 0) {
1824
+ const agents = updateSummary.updated.map((e) => e.agent);
1825
+ const from = updateSummary.updated[0].from;
1826
+ const to = updateSummary.updated[0].to;
1827
+ const fromStr = from ? ` (was ${from})` : "";
1828
+ const count = agents.length;
1829
+ deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
697
1830
  }
698
- await registerOuroBundleTypeNonBlocking(deps);
699
1831
  const daemonResult = await ensureDaemonRunning(deps);
700
1832
  deps.writeStdout(daemonResult.message);
701
1833
  return daemonResult.message;
702
1834
  }
1835
+ // ── rollback command (local, no daemon socket needed for symlinks) ──
1836
+ /* v8 ignore start -- rollback/versions: tested via daemon-cli-rollback/versions tests @preserve */
1837
+ if (command.kind === "rollback") {
1838
+ const currentVersion = deps.getCurrentCliVersion?.() ?? "unknown";
1839
+ if (command.version) {
1840
+ // Rollback to a specific version
1841
+ const installed = deps.listCliVersions?.() ?? [];
1842
+ if (!installed.includes(command.version)) {
1843
+ try {
1844
+ await deps.installCliVersion(command.version);
1845
+ }
1846
+ catch (error) {
1847
+ const message = `failed to install version ${command.version}: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1848
+ deps.writeStdout(message);
1849
+ return message;
1850
+ }
1851
+ }
1852
+ deps.activateCliVersion(command.version);
1853
+ }
1854
+ else {
1855
+ // Rollback to previous version
1856
+ const previousVersion = deps.getPreviousCliVersion?.();
1857
+ if (!previousVersion) {
1858
+ const message = "no previous version to roll back to";
1859
+ deps.writeStdout(message);
1860
+ return message;
1861
+ }
1862
+ deps.activateCliVersion(previousVersion);
1863
+ command = { ...command, version: previousVersion };
1864
+ }
1865
+ // Stop daemon (non-fatal if not running)
1866
+ try {
1867
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
1868
+ }
1869
+ catch {
1870
+ // Daemon may not be running — that's fine
1871
+ }
1872
+ const message = `rolled back to ${command.version} (was ${currentVersion})`;
1873
+ deps.writeStdout(message);
1874
+ return message;
1875
+ }
1876
+ // ── versions command (local, no daemon socket needed) ──
1877
+ if (command.kind === "versions") {
1878
+ const versions = deps.listCliVersions?.() ?? [];
1879
+ if (versions.length === 0) {
1880
+ const message = "no versions installed";
1881
+ deps.writeStdout(message);
1882
+ return message;
1883
+ }
1884
+ const current = deps.getCurrentCliVersion?.();
1885
+ const previous = deps.getPreviousCliVersion?.();
1886
+ const lines = versions.map((v) => {
1887
+ let line = v;
1888
+ if (v === current)
1889
+ line += " * current";
1890
+ if (v === previous)
1891
+ line += " (previous)";
1892
+ return line;
1893
+ });
1894
+ const message = lines.join("\n");
1895
+ deps.writeStdout(message);
1896
+ return message;
1897
+ }
1898
+ /* v8 ignore stop */
703
1899
  if (command.kind === "daemon.logs" && deps.tailLogs) {
704
1900
  deps.tailLogs();
705
1901
  return "";
706
1902
  }
707
- if (command.kind === "friend.link") {
708
- const linker = deps.linkFriendIdentity ?? defaultLinkFriendIdentity;
709
- const message = await linker(command);
1903
+ // ── mcp subcommands (routed through daemon socket) ──
1904
+ if (command.kind === "mcp.list" || command.kind === "mcp.call") {
1905
+ const daemonCommand = toDaemonCommand(command);
1906
+ let response;
1907
+ try {
1908
+ response = await deps.sendCommand(deps.socketPath, daemonCommand);
1909
+ }
1910
+ catch {
1911
+ const message = "daemon unavailable — start with `ouro up` first";
1912
+ deps.writeStdout(message);
1913
+ return message;
1914
+ }
1915
+ if (!response.ok) {
1916
+ const message = response.error ?? "unknown error";
1917
+ deps.writeStdout(message);
1918
+ return message;
1919
+ }
1920
+ const message = formatMcpResponse(command, response);
1921
+ deps.writeStdout(message);
1922
+ return message;
1923
+ }
1924
+ // ── task subcommands (local, no daemon socket needed) ──
1925
+ if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
1926
+ command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
1927
+ command.kind === "task.sessions") {
1928
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1929
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
1930
+ /* v8 ignore stop */
1931
+ const message = executeTaskCommand(command, taskMod);
1932
+ deps.writeStdout(message);
1933
+ return message;
1934
+ }
1935
+ // ── reminder subcommands (local, no daemon socket needed) ──
1936
+ if (command.kind === "reminder.create") {
1937
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1938
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
1939
+ /* v8 ignore stop */
1940
+ const message = executeReminderCommand(command, taskMod);
1941
+ deps.writeStdout(message);
1942
+ return message;
1943
+ }
1944
+ // ── friend subcommands (local, no daemon socket needed) ──
1945
+ if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
1946
+ command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
1947
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1948
+ let store = deps.friendStore;
1949
+ if (!store) {
1950
+ // Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
1951
+ const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
1952
+ const friendsDir = agentName
1953
+ ? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
1954
+ : path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
1955
+ store = new store_file_1.FileFriendStore(friendsDir);
1956
+ }
1957
+ /* v8 ignore stop */
1958
+ const message = await executeFriendCommand(command, store);
1959
+ deps.writeStdout(message);
1960
+ return message;
1961
+ }
1962
+ // ── auth (local, no daemon socket needed) ──
1963
+ if (command.kind === "auth.run") {
1964
+ const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent).config.provider;
1965
+ /* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
1966
+ const authRunner = deps.runAuthFlow ?? auth_flow_1.runRuntimeAuthFlow;
1967
+ const result = await authRunner({
1968
+ agentName: command.agent,
1969
+ provider,
1970
+ promptInput: deps.promptInput,
1971
+ });
1972
+ // Behavior: ouro auth stores credentials only — does NOT switch provider.
1973
+ // Use `ouro auth switch` to change the active provider.
1974
+ deps.writeStdout(result.message);
1975
+ return result.message;
1976
+ }
1977
+ // ── auth verify (local, no daemon socket needed) ──
1978
+ /* v8 ignore start -- auth verify/switch: tested in daemon-cli.test.ts but v8 traces differ in CI @preserve */
1979
+ if (command.kind === "auth.verify") {
1980
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
1981
+ const providers = secrets.providers;
1982
+ const fetchFn = deps.fetchImpl ?? fetch;
1983
+ if (command.provider) {
1984
+ const status = await verifyProviderCredentials(command.provider, providers, fetchFn);
1985
+ const message = `${command.provider}: ${status}`;
1986
+ deps.writeStdout(message);
1987
+ return message;
1988
+ }
1989
+ const lines = [];
1990
+ for (const p of Object.keys(providers)) {
1991
+ const status = await verifyProviderCredentials(p, providers, fetchFn);
1992
+ lines.push(`${p}: ${status}`);
1993
+ }
1994
+ const message = lines.join("\n");
1995
+ deps.writeStdout(message);
1996
+ return message;
1997
+ }
1998
+ // ── auth switch (local, no daemon socket needed) ──
1999
+ if (command.kind === "auth.switch") {
2000
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2001
+ const providerSecrets = secrets.providers[command.provider];
2002
+ if (!providerSecrets || !hasStoredCredentials(command.provider, providerSecrets)) {
2003
+ const message = `no credentials stored for ${command.provider}. Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`;
2004
+ deps.writeStdout(message);
2005
+ return message;
2006
+ }
2007
+ (0, auth_flow_1.writeAgentProviderSelection)(command.agent, command.provider);
2008
+ const message = `switched ${command.agent} to ${command.provider}`;
2009
+ deps.writeStdout(message);
2010
+ return message;
2011
+ }
2012
+ /* v8 ignore stop */
2013
+ // ── config models (local, no daemon socket needed) ──
2014
+ /* v8 ignore start -- config models: tested via daemon-cli.test.ts @preserve */
2015
+ if (command.kind === "config.models") {
2016
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
2017
+ const provider = config.provider;
2018
+ if (provider !== "github-copilot") {
2019
+ const message = `model listing not available for ${provider} — check provider documentation.`;
2020
+ deps.writeStdout(message);
2021
+ return message;
2022
+ }
2023
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2024
+ const ghConfig = secrets.providers["github-copilot"];
2025
+ if (!ghConfig.githubToken || !ghConfig.baseUrl) {
2026
+ throw new Error(`github-copilot credentials not configured. Run \`ouro auth --agent ${command.agent} --provider github-copilot\` first.`);
2027
+ }
2028
+ const fetchFn = deps.fetchImpl ?? fetch;
2029
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
2030
+ if (models.length === 0) {
2031
+ const message = "no models found";
2032
+ deps.writeStdout(message);
2033
+ return message;
2034
+ }
2035
+ const lines = ["available models:"];
2036
+ for (const m of models) {
2037
+ const caps = m.capabilities?.length ? ` (${m.capabilities.join(", ")})` : "";
2038
+ lines.push(` ${m.id}${caps}`);
2039
+ }
2040
+ const message = lines.join("\n");
2041
+ deps.writeStdout(message);
2042
+ return message;
2043
+ }
2044
+ /* v8 ignore stop */
2045
+ // ── config model (local, no daemon socket needed) ──
2046
+ /* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
2047
+ if (command.kind === "config.model") {
2048
+ // Validate model availability for github-copilot before writing
2049
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
2050
+ if (config.provider === "github-copilot") {
2051
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2052
+ const ghConfig = secrets.providers["github-copilot"];
2053
+ if (ghConfig.githubToken && ghConfig.baseUrl) {
2054
+ const fetchFn = deps.fetchImpl ?? fetch;
2055
+ try {
2056
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
2057
+ const available = models.map((m) => m.id);
2058
+ if (available.length > 0 && !available.includes(command.modelName)) {
2059
+ const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
2060
+ deps.writeStdout(message);
2061
+ return message;
2062
+ }
2063
+ }
2064
+ catch {
2065
+ // Catalog validation failed — fall through to ping test
2066
+ }
2067
+ // Ping test: verify the model actually works before switching
2068
+ const pingResult = await pingGithubCopilotModel(ghConfig.baseUrl, ghConfig.githubToken, command.modelName, fetchFn);
2069
+ if (!pingResult.ok) {
2070
+ const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
2071
+ deps.writeStdout(message);
2072
+ return message;
2073
+ }
2074
+ }
2075
+ }
2076
+ const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, command.modelName);
2077
+ const message = previousModel
2078
+ ? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
2079
+ : `set ${command.agent} model on ${provider}: ${command.modelName}`;
2080
+ deps.writeStdout(message);
2081
+ return message;
2082
+ }
2083
+ /* v8 ignore stop */
2084
+ // ── whoami (local, no daemon socket needed) ──
2085
+ if (command.kind === "whoami") {
2086
+ if (command.agent) {
2087
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
2088
+ const message = [
2089
+ `agent: ${command.agent}`,
2090
+ `home: ${agentRoot}`,
2091
+ `bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
2092
+ ].join("\n");
2093
+ deps.writeStdout(message);
2094
+ return message;
2095
+ }
2096
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2097
+ try {
2098
+ const info = deps.whoamiInfo
2099
+ ? deps.whoamiInfo()
2100
+ : {
2101
+ agentName: (0, identity_1.getAgentName)(),
2102
+ homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
2103
+ bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
2104
+ };
2105
+ const message = [
2106
+ `agent: ${info.agentName}`,
2107
+ `home: ${info.homePath}`,
2108
+ `bones: ${info.bonesVersion}`,
2109
+ ].join("\n");
2110
+ deps.writeStdout(message);
2111
+ return message;
2112
+ }
2113
+ catch {
2114
+ const message = "error: no agent context — use --agent <name> to specify";
2115
+ deps.writeStdout(message);
2116
+ return message;
2117
+ }
2118
+ /* v8 ignore stop */
2119
+ }
2120
+ // ── changelog (local, no daemon socket needed) ──
2121
+ if (command.kind === "changelog") {
2122
+ try {
2123
+ const changelogPath = deps.getChangelogPath
2124
+ ? deps.getChangelogPath()
2125
+ : (0, bundle_manifest_1.getChangelogPath)();
2126
+ const raw = fs.readFileSync(changelogPath, "utf-8");
2127
+ const entries = JSON.parse(raw);
2128
+ let filtered = entries;
2129
+ if (command.from) {
2130
+ const fromVersion = command.from;
2131
+ filtered = entries.filter((e) => e.version > fromVersion);
2132
+ }
2133
+ if (filtered.length === 0) {
2134
+ const message = "no changelog entries found.";
2135
+ deps.writeStdout(message);
2136
+ return message;
2137
+ }
2138
+ const lines = [];
2139
+ for (const entry of filtered) {
2140
+ lines.push(`## ${entry.version}${entry.date ? ` (${entry.date})` : ""}`);
2141
+ if (entry.changes) {
2142
+ for (const change of entry.changes) {
2143
+ lines.push(`- ${change}`);
2144
+ }
2145
+ }
2146
+ lines.push("");
2147
+ }
2148
+ const message = lines.join("\n").trim();
2149
+ deps.writeStdout(message);
2150
+ return message;
2151
+ }
2152
+ catch {
2153
+ const message = "no changelog entries found.";
2154
+ deps.writeStdout(message);
2155
+ return message;
2156
+ }
2157
+ }
2158
+ // ── thoughts (local, no daemon socket needed) ──
2159
+ if (command.kind === "thoughts") {
2160
+ try {
2161
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2162
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
2163
+ const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
2164
+ if (command.json) {
2165
+ try {
2166
+ const raw = fs.readFileSync(sessionFilePath, "utf-8");
2167
+ deps.writeStdout(raw);
2168
+ return raw;
2169
+ }
2170
+ catch {
2171
+ const message = "no inner dialog session found";
2172
+ deps.writeStdout(message);
2173
+ return message;
2174
+ }
2175
+ }
2176
+ const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
2177
+ const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
2178
+ deps.writeStdout(message);
2179
+ if (command.follow) {
2180
+ deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
2181
+ /* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
2182
+ const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
2183
+ deps.writeStdout("\n" + formatted);
2184
+ });
2185
+ /* v8 ignore stop */
2186
+ // Block until process exit; cleanup watcher on SIGINT/SIGTERM
2187
+ return new Promise((resolve) => {
2188
+ const cleanup = () => { stop(); resolve(message); };
2189
+ process.once("SIGINT", cleanup);
2190
+ process.once("SIGTERM", cleanup);
2191
+ });
2192
+ }
2193
+ return message;
2194
+ }
2195
+ catch {
2196
+ const message = "error: no agent context — use --agent <name> to specify";
2197
+ deps.writeStdout(message);
2198
+ return message;
2199
+ }
2200
+ }
2201
+ // ── session list (local, no daemon socket needed) ──
2202
+ if (command.kind === "session.list") {
2203
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2204
+ const scanner = deps.scanSessions ?? (async () => []);
2205
+ /* v8 ignore stop */
2206
+ const sessions = await scanner();
2207
+ if (sessions.length === 0) {
2208
+ const message = "no active sessions";
2209
+ deps.writeStdout(message);
2210
+ return message;
2211
+ }
2212
+ const lines = sessions.map((s) => `${s.friendId} ${s.friendName} ${s.channel} ${s.lastActivity}`);
2213
+ const message = lines.join("\n");
710
2214
  deps.writeStdout(message);
711
2215
  return message;
712
2216
  }
2217
+ if (command.kind === "chat.connect" && deps.startChat) {
2218
+ await ensureDaemonRunning(deps);
2219
+ await deps.startChat(command.agent);
2220
+ return "";
2221
+ }
713
2222
  if (command.kind === "hatch.start") {
714
2223
  // Route through adoption specialist when no explicit hatch args were provided
715
2224
  const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
716
2225
  if (deps.runAdoptionSpecialist && !hasExplicitHatchArgs) {
2226
+ // System setup first — ouro command, subagents, UTI — before the interactive specialist
2227
+ await performSystemSetup(deps);
717
2228
  const hatchlingName = await deps.runAdoptionSpecialist();
718
2229
  if (!hatchlingName) {
719
2230
  return "";
720
2231
  }
721
- try {
722
- await deps.installSubagents();
723
- }
724
- catch (error) {
725
- (0, runtime_1.emitNervesEvent)({
726
- level: "warn",
727
- component: "daemon",
728
- event: "daemon.subagent_install_error",
729
- message: "subagent auto-install failed",
730
- meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
731
- });
732
- }
733
- await registerOuroBundleTypeNonBlocking(deps);
734
2232
  await ensureDaemonRunning(deps);
735
2233
  if (deps.startChat) {
736
2234
  await deps.startChat(hatchlingName);
@@ -746,19 +2244,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
746
2244
  }
747
2245
  const hatchInput = await resolveHatchInput(command, deps);
748
2246
  const result = await hatchRunner(hatchInput);
749
- try {
750
- await deps.installSubagents();
751
- }
752
- catch (error) {
753
- (0, runtime_1.emitNervesEvent)({
754
- level: "warn",
755
- component: "daemon",
756
- event: "daemon.subagent_install_error",
757
- message: "subagent auto-install failed",
758
- meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
759
- });
760
- }
761
- await registerOuroBundleTypeNonBlocking(deps);
2247
+ await performSystemSetup(deps);
762
2248
  const daemonResult = await ensureDaemonRunning(deps);
763
2249
  if (deps.startChat) {
764
2250
  await deps.startChat(hatchInput.agentName);
@@ -780,9 +2266,22 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
780
2266
  deps.writeStdout(message);
781
2267
  return message;
782
2268
  }
2269
+ if (command.kind === "daemon.status" && isDaemonUnavailableError(error)) {
2270
+ const message = daemonUnavailableStatusOutput(deps.socketPath);
2271
+ deps.writeStdout(message);
2272
+ return message;
2273
+ }
2274
+ if (command.kind === "daemon.stop" && isDaemonUnavailableError(error)) {
2275
+ const message = "daemon not running";
2276
+ deps.writeStdout(message);
2277
+ return message;
2278
+ }
783
2279
  throw error;
784
2280
  }
785
- const message = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
2281
+ const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
2282
+ const message = command.kind === "daemon.status"
2283
+ ? formatDaemonStatusOutput(response, fallbackMessage)
2284
+ : fallbackMessage;
786
2285
  deps.writeStdout(message);
787
2286
  return message;
788
2287
  }