@ouro.bot/cli 0.1.0-alpha.12 → 0.1.0-alpha.121

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