@ouro.bot/cli 0.1.0-alpha.11 → 0.1.0-alpha.111

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