@ouro.bot/cli 0.1.0-alpha.13 → 0.1.0-alpha.131

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