@ouro.bot/cli 0.1.0-alpha.14 → 0.1.0-alpha.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +151 -205
- package/changelog.json +913 -0
- package/dist/heart/active-work.js +633 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +105 -0
- package/dist/heart/config.js +66 -21
- package/dist/heart/core.js +645 -112
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/agent-service.js +467 -0
- package/dist/heart/daemon/auth-flow.js +457 -0
- package/dist/heart/daemon/daemon-cli.js +1840 -196
- package/dist/heart/daemon/daemon-entry.js +80 -3
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +301 -1
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +15 -80
- package/dist/heart/daemon/heartbeat-timer.js +131 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/mcp-server.js +464 -0
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-path-installer.js +67 -34
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +13 -0
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +50 -2
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +2 -2
- package/dist/heart/daemon/specialist-prompt.js +10 -7
- package/dist/heart/daemon/specialist-tools.js +52 -3
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +64 -21
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +197 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +159 -0
- package/dist/heart/providers/anthropic-token.js +163 -0
- package/dist/heart/providers/anthropic.js +195 -34
- package/dist/heart/providers/azure.js +115 -9
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +49 -14
- package/dist/heart/session-activity.js +173 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/streaming.js +117 -33
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +122 -32
- package/dist/mind/bundle-manifest.js +14 -0
- package/dist/mind/context.js +125 -14
- package/dist/mind/{memory.js → diary.js} +70 -56
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/obligations.js +134 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +554 -88
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +12 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/ado-semantic.js +6 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +254 -0
- package/dist/repertoire/coding/feedback.js +301 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +210 -4
- package/dist/repertoire/coding/spawner.js +59 -12
- package/dist/repertoire/coding/tools.js +176 -4
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +198 -0
- package/dist/repertoire/skills.js +14 -23
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +980 -271
- package/dist/repertoire/tools-bluebubbles.js +94 -0
- package/dist/repertoire/tools-github.js +1 -6
- package/dist/repertoire/tools-teams.js +65 -59
- package/dist/repertoire/tools.js +266 -91
- package/dist/senses/attention-queue.js +97 -0
- package/dist/senses/bluebubbles-client.js +210 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +944 -45
- package/dist/senses/cli-entry.js +1 -1
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +374 -131
- package/dist/senses/contextual-heartbeat.js +102 -0
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +500 -84
- package/dist/senses/pipeline.js +454 -0
- package/dist/senses/surface-tool.js +82 -0
- package/dist/senses/teams.js +607 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +12 -4
- package/skills/configure-dev-tools.md +109 -0
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
|
@@ -34,26 +34,45 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
37
|
+
exports.listGithubCopilotModels = listGithubCopilotModels;
|
|
38
|
+
exports.pingGithubCopilotModel = pingGithubCopilotModel;
|
|
37
39
|
exports.parseOuroCommand = parseOuroCommand;
|
|
40
|
+
exports.readFirstBundleMetaVersion = readFirstBundleMetaVersion;
|
|
38
41
|
exports.discoverExistingCredentials = discoverExistingCredentials;
|
|
39
42
|
exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
|
|
40
43
|
exports.runOuroCli = runOuroCli;
|
|
41
44
|
const child_process_1 = require("child_process");
|
|
45
|
+
const crypto_1 = require("crypto");
|
|
42
46
|
const fs = __importStar(require("fs"));
|
|
43
|
-
const net = __importStar(require("net"));
|
|
44
47
|
const os = __importStar(require("os"));
|
|
45
48
|
const path = __importStar(require("path"));
|
|
49
|
+
const semver = __importStar(require("semver"));
|
|
46
50
|
const identity_1 = require("../identity");
|
|
47
51
|
const runtime_1 = require("../../nerves/runtime");
|
|
48
52
|
const store_file_1 = require("../../mind/friends/store-file");
|
|
49
53
|
const types_1 = require("../../mind/friends/types");
|
|
50
54
|
const ouro_uti_1 = require("./ouro-uti");
|
|
51
55
|
const ouro_path_installer_1 = require("./ouro-path-installer");
|
|
52
|
-
const
|
|
56
|
+
const ouro_version_manager_1 = require("./ouro-version-manager");
|
|
57
|
+
const skill_management_installer_1 = require("./skill-management-installer");
|
|
53
58
|
const hatch_flow_1 = require("./hatch-flow");
|
|
54
59
|
const specialist_orchestrator_1 = require("./specialist-orchestrator");
|
|
55
60
|
const specialist_prompt_1 = require("./specialist-prompt");
|
|
56
61
|
const specialist_tools_1 = require("./specialist-tools");
|
|
62
|
+
const runtime_metadata_1 = require("./runtime-metadata");
|
|
63
|
+
const runtime_mode_1 = require("./runtime-mode");
|
|
64
|
+
const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
|
|
65
|
+
const agent_discovery_1 = require("./agent-discovery");
|
|
66
|
+
const update_hooks_1 = require("./update-hooks");
|
|
67
|
+
const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
68
|
+
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
69
|
+
const tasks_1 = require("../../repertoire/tasks");
|
|
70
|
+
const thoughts_1 = require("./thoughts");
|
|
71
|
+
const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
|
|
72
|
+
const launchd_1 = require("./launchd");
|
|
73
|
+
const socket_client_1 = require("./socket-client");
|
|
74
|
+
const session_activity_1 = require("../session-activity");
|
|
75
|
+
const auth_flow_1 = require("./auth-flow");
|
|
57
76
|
function stringField(value) {
|
|
58
77
|
return typeof value === "string" ? value : null;
|
|
59
78
|
}
|
|
@@ -78,8 +97,14 @@ function parseStatusPayload(data) {
|
|
|
78
97
|
daemon: stringField(overview.daemon) ?? "unknown",
|
|
79
98
|
health: stringField(overview.health) ?? "unknown",
|
|
80
99
|
socketPath: stringField(overview.socketPath) ?? "unknown",
|
|
100
|
+
version: stringField(overview.version) ?? "unknown",
|
|
101
|
+
lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
|
|
102
|
+
repoRoot: stringField(overview.repoRoot) ?? "unknown",
|
|
103
|
+
configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
|
|
81
104
|
workerCount: numberField(overview.workerCount) ?? 0,
|
|
82
105
|
senseCount: numberField(overview.senseCount) ?? 0,
|
|
106
|
+
entryPath: stringField(overview.entryPath) ?? "unknown",
|
|
107
|
+
mode: stringField(overview.mode) ?? "unknown",
|
|
83
108
|
};
|
|
84
109
|
const parsedSenses = senses.map((entry) => {
|
|
85
110
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
@@ -158,6 +183,10 @@ function formatDaemonStatusOutput(response, fallback) {
|
|
|
158
183
|
const overviewRows = [
|
|
159
184
|
["Daemon", payload.overview.daemon],
|
|
160
185
|
["Socket", payload.overview.socketPath],
|
|
186
|
+
["Version", payload.overview.version],
|
|
187
|
+
["Last Updated", payload.overview.lastUpdated],
|
|
188
|
+
["Entry Path", payload.overview.entryPath],
|
|
189
|
+
["Mode", payload.overview.mode],
|
|
161
190
|
["Workers", String(payload.overview.workerCount)],
|
|
162
191
|
["Senses", String(payload.overview.senseCount)],
|
|
163
192
|
["Health", payload.overview.health],
|
|
@@ -190,10 +219,35 @@ function formatDaemonStatusOutput(response, fallback) {
|
|
|
190
219
|
async function ensureDaemonRunning(deps) {
|
|
191
220
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
192
221
|
if (alive) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
222
|
+
const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
223
|
+
let runningRuntimePromise = null;
|
|
224
|
+
const fetchRunningRuntimeMetadata = async () => {
|
|
225
|
+
runningRuntimePromise ??= (async () => {
|
|
226
|
+
const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
|
|
227
|
+
const payload = parseStatusPayload(status.data);
|
|
228
|
+
return {
|
|
229
|
+
version: payload?.overview.version ?? "unknown",
|
|
230
|
+
lastUpdated: payload?.overview.lastUpdated ?? "unknown",
|
|
231
|
+
repoRoot: payload?.overview.repoRoot ?? "unknown",
|
|
232
|
+
configFingerprint: payload?.overview.configFingerprint ?? "unknown",
|
|
233
|
+
};
|
|
234
|
+
})();
|
|
235
|
+
return runningRuntimePromise;
|
|
196
236
|
};
|
|
237
|
+
return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
|
|
238
|
+
socketPath: deps.socketPath,
|
|
239
|
+
localVersion: localRuntime.version,
|
|
240
|
+
localLastUpdated: localRuntime.lastUpdated,
|
|
241
|
+
localRepoRoot: localRuntime.repoRoot,
|
|
242
|
+
localConfigFingerprint: localRuntime.configFingerprint,
|
|
243
|
+
fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
|
|
244
|
+
fetchRunningRuntimeMetadata,
|
|
245
|
+
stopDaemon: async () => {
|
|
246
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
247
|
+
},
|
|
248
|
+
cleanupStaleSocket: deps.cleanupStaleSocket,
|
|
249
|
+
startDaemonProcess: deps.startDaemonProcess,
|
|
250
|
+
});
|
|
197
251
|
}
|
|
198
252
|
deps.cleanupStaleSocket(deps.socketPath);
|
|
199
253
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
@@ -202,17 +256,96 @@ async function ensureDaemonRunning(deps) {
|
|
|
202
256
|
message: `daemon started (pid ${started.pid ?? "unknown"})`,
|
|
203
257
|
};
|
|
204
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Extract `--agent <name>` from an args array, returning the agent name and
|
|
261
|
+
* the remaining args with the flag pair removed.
|
|
262
|
+
*/
|
|
263
|
+
function extractAgentFlag(args) {
|
|
264
|
+
const idx = args.indexOf("--agent");
|
|
265
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
266
|
+
return { rest: args };
|
|
267
|
+
const agent = args[idx + 1];
|
|
268
|
+
const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
|
|
269
|
+
return { agent, rest };
|
|
270
|
+
}
|
|
205
271
|
function usage() {
|
|
206
272
|
return [
|
|
207
273
|
"Usage:",
|
|
208
274
|
" ouro [up]",
|
|
209
|
-
" ouro
|
|
275
|
+
" ouro dev [--repo-path <path>] [--clone [--clone-path <path>]]",
|
|
276
|
+
" ouro stop|down|status|logs|hatch",
|
|
277
|
+
" ouro -v|--version",
|
|
278
|
+
" ouro config model --agent <name> <model-name>",
|
|
279
|
+
" ouro config models --agent <name>",
|
|
280
|
+
" ouro auth --agent <name> [--provider <provider>]",
|
|
281
|
+
" ouro auth verify --agent <name> [--provider <provider>]",
|
|
282
|
+
" ouro auth switch --agent <name> --provider <provider>",
|
|
210
283
|
" ouro chat <agent>",
|
|
211
284
|
" ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
|
|
212
285
|
" ouro poke <agent> --task <task-id>",
|
|
213
286
|
" ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
|
|
287
|
+
" ouro task board [<status>] [--agent <name>]",
|
|
288
|
+
" ouro task create <title> [--type <type>] [--agent <name>]",
|
|
289
|
+
" ouro task update <id> <status> [--agent <name>]",
|
|
290
|
+
" ouro task show <id> [--agent <name>]",
|
|
291
|
+
" ouro task actionable|deps|sessions [--agent <name>]",
|
|
292
|
+
" ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>] [--category <category>] [--agent <name>]",
|
|
293
|
+
" ouro friend list [--agent <name>]",
|
|
294
|
+
" ouro friend show <id> [--agent <name>]",
|
|
295
|
+
" ouro friend create --name <name> [--trust <level>] [--agent <name>]",
|
|
296
|
+
" ouro friend update <id> --trust <level> [--agent <name>]",
|
|
297
|
+
" ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
|
|
298
|
+
" ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
299
|
+
" ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
300
|
+
" ouro whoami [--agent <name>]",
|
|
301
|
+
" ouro session list [--agent <name>]",
|
|
302
|
+
" ouro mcp list",
|
|
303
|
+
" ouro mcp call <server> <tool> [--args '{...}']",
|
|
304
|
+
" ouro rollback [<version>]",
|
|
305
|
+
" ouro versions",
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
function formatVersionOutput() {
|
|
309
|
+
return (0, runtime_metadata_1.getRuntimeMetadata)().version;
|
|
310
|
+
}
|
|
311
|
+
function buildStoppedStatusPayload(socketPath) {
|
|
312
|
+
const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
313
|
+
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
314
|
+
return {
|
|
315
|
+
overview: {
|
|
316
|
+
daemon: "stopped",
|
|
317
|
+
health: "warn",
|
|
318
|
+
socketPath,
|
|
319
|
+
version: metadata.version,
|
|
320
|
+
lastUpdated: metadata.lastUpdated,
|
|
321
|
+
repoRoot: metadata.repoRoot,
|
|
322
|
+
configFingerprint: metadata.configFingerprint,
|
|
323
|
+
workerCount: 0,
|
|
324
|
+
senseCount: 0,
|
|
325
|
+
entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
|
|
326
|
+
mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
|
|
327
|
+
},
|
|
328
|
+
senses: [],
|
|
329
|
+
workers: [],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function daemonUnavailableStatusOutput(socketPath) {
|
|
333
|
+
return [
|
|
334
|
+
formatDaemonStatusOutput({
|
|
335
|
+
ok: true,
|
|
336
|
+
summary: "daemon not running",
|
|
337
|
+
data: buildStoppedStatusPayload(socketPath),
|
|
338
|
+
}, "daemon not running"),
|
|
339
|
+
"",
|
|
340
|
+
"daemon not running; run `ouro up`",
|
|
214
341
|
].join("\n");
|
|
215
342
|
}
|
|
343
|
+
function isDaemonUnavailableError(error) {
|
|
344
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
345
|
+
? String(error.code ?? "")
|
|
346
|
+
: "";
|
|
347
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
348
|
+
}
|
|
216
349
|
function parseMessageCommand(args) {
|
|
217
350
|
let to;
|
|
218
351
|
let sessionId;
|
|
@@ -264,7 +397,7 @@ function parsePokeCommand(args) {
|
|
|
264
397
|
throw new Error(`Usage\n${usage()}`);
|
|
265
398
|
return { kind: "task.poke", agent, taskId };
|
|
266
399
|
}
|
|
267
|
-
function parseLinkCommand(args) {
|
|
400
|
+
function parseLinkCommand(args, kind = "friend.link") {
|
|
268
401
|
const agent = args[0];
|
|
269
402
|
if (!agent)
|
|
270
403
|
throw new Error(`Usage\n${usage()}`);
|
|
@@ -296,7 +429,7 @@ function parseLinkCommand(args) {
|
|
|
296
429
|
throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation.`);
|
|
297
430
|
}
|
|
298
431
|
return {
|
|
299
|
-
kind
|
|
432
|
+
kind,
|
|
300
433
|
agent,
|
|
301
434
|
friendId,
|
|
302
435
|
provider: providerRaw,
|
|
@@ -304,7 +437,102 @@ function parseLinkCommand(args) {
|
|
|
304
437
|
};
|
|
305
438
|
}
|
|
306
439
|
function isAgentProvider(value) {
|
|
307
|
-
return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex";
|
|
440
|
+
return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
|
|
441
|
+
}
|
|
442
|
+
/* v8 ignore start -- hasStoredCredentials: per-provider branches tested via auth switch tests @preserve */
|
|
443
|
+
function hasStoredCredentials(provider, providerSecrets) {
|
|
444
|
+
if (provider === "anthropic")
|
|
445
|
+
return !!providerSecrets.setupToken;
|
|
446
|
+
if (provider === "openai-codex")
|
|
447
|
+
return !!providerSecrets.oauthAccessToken;
|
|
448
|
+
if (provider === "github-copilot")
|
|
449
|
+
return !!providerSecrets.githubToken;
|
|
450
|
+
if (provider === "minimax")
|
|
451
|
+
return !!providerSecrets.apiKey;
|
|
452
|
+
// azure
|
|
453
|
+
return !!providerSecrets.endpoint && !!providerSecrets.apiKey;
|
|
454
|
+
}
|
|
455
|
+
/* v8 ignore stop */
|
|
456
|
+
/* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
|
|
457
|
+
async function verifyProviderCredentials(provider, providers) {
|
|
458
|
+
const config = providers[provider];
|
|
459
|
+
if (!config)
|
|
460
|
+
return "not configured";
|
|
461
|
+
try {
|
|
462
|
+
const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../../heart/provider-ping")));
|
|
463
|
+
const result = await pingProvider(provider, config);
|
|
464
|
+
return result.ok ? "ok" : `failed (${result.message})`;
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
return `failed (${error instanceof Error ? error.message : String(error)})`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
|
|
471
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
472
|
+
const response = await fetchImpl(url, {
|
|
473
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
474
|
+
});
|
|
475
|
+
if (!response.ok) {
|
|
476
|
+
throw new Error(`model listing failed (HTTP ${response.status})`);
|
|
477
|
+
}
|
|
478
|
+
const body = await response.json();
|
|
479
|
+
/* v8 ignore start -- response shape handling: tested via config-models.test.ts @preserve */
|
|
480
|
+
const items = Array.isArray(body) ? body : (body?.data ?? []);
|
|
481
|
+
return items.map((item) => {
|
|
482
|
+
const rec = item;
|
|
483
|
+
const capabilities = Array.isArray(rec.capabilities)
|
|
484
|
+
? rec.capabilities.filter((c) => typeof c === "string")
|
|
485
|
+
: undefined;
|
|
486
|
+
return {
|
|
487
|
+
id: String(rec.id ?? rec.name ?? ""),
|
|
488
|
+
name: String(rec.name ?? rec.id ?? ""),
|
|
489
|
+
...(capabilities ? { capabilities } : {}),
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
/* v8 ignore stop */
|
|
493
|
+
}
|
|
494
|
+
async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
|
|
495
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
496
|
+
const isClaude = model.startsWith("claude");
|
|
497
|
+
const url = isClaude ? `${base}/chat/completions` : `${base}/responses`;
|
|
498
|
+
const body = isClaude
|
|
499
|
+
? JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 1 })
|
|
500
|
+
: JSON.stringify({ model, input: "ping", max_output_tokens: 16 });
|
|
501
|
+
try {
|
|
502
|
+
const response = await fetchImpl(url, {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: {
|
|
505
|
+
Authorization: `Bearer ${token}`,
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
},
|
|
508
|
+
body,
|
|
509
|
+
});
|
|
510
|
+
if (response.ok)
|
|
511
|
+
return { ok: true };
|
|
512
|
+
let detail = `HTTP ${response.status}`;
|
|
513
|
+
try {
|
|
514
|
+
const json = await response.json();
|
|
515
|
+
/* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
|
|
516
|
+
if (typeof json.error === "string")
|
|
517
|
+
detail = json.error;
|
|
518
|
+
else if (typeof json.error === "object" && json.error !== null) {
|
|
519
|
+
const errObj = json.error;
|
|
520
|
+
if (typeof errObj.message === "string")
|
|
521
|
+
detail = errObj.message;
|
|
522
|
+
}
|
|
523
|
+
else if (typeof json.message === "string")
|
|
524
|
+
detail = json.message;
|
|
525
|
+
/* v8 ignore stop */
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// response body not JSON — keep HTTP status
|
|
529
|
+
}
|
|
530
|
+
return { ok: false, error: detail };
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
/* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
|
|
534
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
535
|
+
}
|
|
308
536
|
}
|
|
309
537
|
function parseHatchCommand(args) {
|
|
310
538
|
let agentName;
|
|
@@ -361,7 +589,7 @@ function parseHatchCommand(args) {
|
|
|
361
589
|
}
|
|
362
590
|
}
|
|
363
591
|
if (providerRaw && !isAgentProvider(providerRaw)) {
|
|
364
|
-
throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex.");
|
|
592
|
+
throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex|github-copilot.");
|
|
365
593
|
}
|
|
366
594
|
const provider = providerRaw && isAgentProvider(providerRaw) ? providerRaw : undefined;
|
|
367
595
|
return {
|
|
@@ -373,13 +601,353 @@ function parseHatchCommand(args) {
|
|
|
373
601
|
migrationPath,
|
|
374
602
|
};
|
|
375
603
|
}
|
|
604
|
+
function parseTaskCommand(args) {
|
|
605
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
606
|
+
const [sub, ...rest] = cleaned;
|
|
607
|
+
if (!sub)
|
|
608
|
+
throw new Error(`Usage\n${usage()}`);
|
|
609
|
+
if (sub === "board") {
|
|
610
|
+
const status = rest[0];
|
|
611
|
+
return status
|
|
612
|
+
? { kind: "task.board", status, ...(agent ? { agent } : {}) }
|
|
613
|
+
: { kind: "task.board", ...(agent ? { agent } : {}) };
|
|
614
|
+
}
|
|
615
|
+
if (sub === "create") {
|
|
616
|
+
const title = rest[0];
|
|
617
|
+
if (!title)
|
|
618
|
+
throw new Error(`Usage\n${usage()}`);
|
|
619
|
+
let type;
|
|
620
|
+
for (let i = 1; i < rest.length; i++) {
|
|
621
|
+
if (rest[i] === "--type" && rest[i + 1]) {
|
|
622
|
+
type = rest[i + 1];
|
|
623
|
+
i += 1;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return type
|
|
627
|
+
? { kind: "task.create", title, type, ...(agent ? { agent } : {}) }
|
|
628
|
+
: { kind: "task.create", title, ...(agent ? { agent } : {}) };
|
|
629
|
+
}
|
|
630
|
+
if (sub === "update") {
|
|
631
|
+
const id = rest[0];
|
|
632
|
+
const status = rest[1];
|
|
633
|
+
if (!id || !status)
|
|
634
|
+
throw new Error(`Usage\n${usage()}`);
|
|
635
|
+
return { kind: "task.update", id, status, ...(agent ? { agent } : {}) };
|
|
636
|
+
}
|
|
637
|
+
if (sub === "show") {
|
|
638
|
+
const id = rest[0];
|
|
639
|
+
if (!id)
|
|
640
|
+
throw new Error(`Usage\n${usage()}`);
|
|
641
|
+
return { kind: "task.show", id, ...(agent ? { agent } : {}) };
|
|
642
|
+
}
|
|
643
|
+
if (sub === "actionable")
|
|
644
|
+
return { kind: "task.actionable", ...(agent ? { agent } : {}) };
|
|
645
|
+
if (sub === "deps")
|
|
646
|
+
return { kind: "task.deps", ...(agent ? { agent } : {}) };
|
|
647
|
+
if (sub === "sessions")
|
|
648
|
+
return { kind: "task.sessions", ...(agent ? { agent } : {}) };
|
|
649
|
+
throw new Error(`Usage\n${usage()}`);
|
|
650
|
+
}
|
|
651
|
+
function parseAuthCommand(args) {
|
|
652
|
+
const first = args[0];
|
|
653
|
+
// Support both positional (`auth switch`) and flag (`auth --switch`) forms
|
|
654
|
+
if (first === "verify" || first === "switch" || first === "--verify" || first === "--switch") {
|
|
655
|
+
const subcommand = first.replace(/^--/, "");
|
|
656
|
+
const { agent, rest } = extractAgentFlag(args.slice(1));
|
|
657
|
+
let provider;
|
|
658
|
+
/* v8 ignore start -- provider flag parsing: branches tested via CLI parsing tests @preserve */
|
|
659
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
660
|
+
if (rest[i] === "--provider") {
|
|
661
|
+
const value = rest[i + 1];
|
|
662
|
+
if (!isAgentProvider(value))
|
|
663
|
+
throw new Error(`Usage\n${usage()}`);
|
|
664
|
+
provider = value;
|
|
665
|
+
i += 1;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/* v8 ignore stop */
|
|
670
|
+
/* v8 ignore next -- defensive: agent always provided in tests @preserve */
|
|
671
|
+
if (!agent)
|
|
672
|
+
throw new Error(`Usage\n${usage()}`);
|
|
673
|
+
if (subcommand === "switch") {
|
|
674
|
+
if (!provider)
|
|
675
|
+
throw new Error(`auth switch requires --provider.\n${usage()}`);
|
|
676
|
+
return { kind: "auth.switch", agent, provider };
|
|
677
|
+
}
|
|
678
|
+
return provider ? { kind: "auth.verify", agent, provider } : { kind: "auth.verify", agent };
|
|
679
|
+
}
|
|
680
|
+
const { agent, rest } = extractAgentFlag(args);
|
|
681
|
+
let provider;
|
|
682
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
683
|
+
if (rest[i] === "--provider") {
|
|
684
|
+
const value = rest[i + 1];
|
|
685
|
+
if (!isAgentProvider(value))
|
|
686
|
+
throw new Error(`Usage\n${usage()}`);
|
|
687
|
+
provider = value;
|
|
688
|
+
i += 1;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (!agent) {
|
|
693
|
+
throw new Error([
|
|
694
|
+
"Usage:",
|
|
695
|
+
" ouro auth --agent <name> [--provider <provider>] Set up credentials",
|
|
696
|
+
" ouro auth verify --agent <name> [--provider <p>] Verify credentials work",
|
|
697
|
+
" ouro auth switch --agent <name> --provider <p> Switch active provider",
|
|
698
|
+
].join("\n"));
|
|
699
|
+
}
|
|
700
|
+
return provider ? { kind: "auth.run", agent, provider } : { kind: "auth.run", agent };
|
|
701
|
+
}
|
|
702
|
+
function parseReminderCommand(args) {
|
|
703
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
704
|
+
const [sub, ...rest] = cleaned;
|
|
705
|
+
if (!sub)
|
|
706
|
+
throw new Error(`Usage\n${usage()}`);
|
|
707
|
+
if (sub === "create") {
|
|
708
|
+
const title = rest[0];
|
|
709
|
+
if (!title)
|
|
710
|
+
throw new Error(`Usage\n${usage()}`);
|
|
711
|
+
let body;
|
|
712
|
+
let scheduledAt;
|
|
713
|
+
let cadence;
|
|
714
|
+
let category;
|
|
715
|
+
let requester;
|
|
716
|
+
for (let i = 1; i < rest.length; i++) {
|
|
717
|
+
if (rest[i] === "--body" && rest[i + 1]) {
|
|
718
|
+
body = rest[i + 1];
|
|
719
|
+
i += 1;
|
|
720
|
+
}
|
|
721
|
+
else if (rest[i] === "--at" && rest[i + 1]) {
|
|
722
|
+
scheduledAt = rest[i + 1];
|
|
723
|
+
i += 1;
|
|
724
|
+
}
|
|
725
|
+
else if (rest[i] === "--cadence" && rest[i + 1]) {
|
|
726
|
+
cadence = rest[i + 1];
|
|
727
|
+
i += 1;
|
|
728
|
+
}
|
|
729
|
+
else if (rest[i] === "--category" && rest[i + 1]) {
|
|
730
|
+
category = rest[i + 1];
|
|
731
|
+
i += 1;
|
|
732
|
+
}
|
|
733
|
+
else if (rest[i] === "--requester" && rest[i + 1]) {
|
|
734
|
+
requester = rest[i + 1];
|
|
735
|
+
i += 1;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (!body)
|
|
739
|
+
throw new Error(`Usage\n${usage()}`);
|
|
740
|
+
if (!scheduledAt && !cadence)
|
|
741
|
+
throw new Error(`Usage\n${usage()}`);
|
|
742
|
+
return {
|
|
743
|
+
kind: "reminder.create",
|
|
744
|
+
title,
|
|
745
|
+
body,
|
|
746
|
+
...(scheduledAt ? { scheduledAt } : {}),
|
|
747
|
+
...(cadence ? { cadence } : {}),
|
|
748
|
+
...(category ? { category } : {}),
|
|
749
|
+
...(requester ? { requester } : {}),
|
|
750
|
+
...(agent ? { agent } : {}),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
throw new Error(`Usage\n${usage()}`);
|
|
754
|
+
}
|
|
755
|
+
function parseSessionCommand(args) {
|
|
756
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
757
|
+
const [sub] = cleaned;
|
|
758
|
+
if (!sub)
|
|
759
|
+
throw new Error(`Usage\n${usage()}`);
|
|
760
|
+
if (sub === "list")
|
|
761
|
+
return { kind: "session.list", ...(agent ? { agent } : {}) };
|
|
762
|
+
throw new Error(`Usage\n${usage()}`);
|
|
763
|
+
}
|
|
764
|
+
function parseAttentionCommand(args) {
|
|
765
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
766
|
+
const sub = cleaned[0];
|
|
767
|
+
if (sub === "show" && cleaned[1]) {
|
|
768
|
+
return { kind: "attention.show", id: cleaned[1], ...(agent ? { agent } : {}) };
|
|
769
|
+
}
|
|
770
|
+
if (sub === "history") {
|
|
771
|
+
return { kind: "attention.history", ...(agent ? { agent } : {}) };
|
|
772
|
+
}
|
|
773
|
+
return { kind: "attention.list", ...(agent ? { agent } : {}) };
|
|
774
|
+
}
|
|
775
|
+
function parseThoughtsCommand(args) {
|
|
776
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
777
|
+
let last;
|
|
778
|
+
let json = false;
|
|
779
|
+
let follow = false;
|
|
780
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
781
|
+
if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
|
|
782
|
+
last = Number.parseInt(cleaned[i + 1], 10);
|
|
783
|
+
i++;
|
|
784
|
+
}
|
|
785
|
+
if (cleaned[i] === "--json")
|
|
786
|
+
json = true;
|
|
787
|
+
if (cleaned[i] === "--follow" || cleaned[i] === "-f")
|
|
788
|
+
follow = true;
|
|
789
|
+
}
|
|
790
|
+
return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
|
|
791
|
+
}
|
|
792
|
+
function parseFriendCommand(args) {
|
|
793
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
794
|
+
const [sub, ...rest] = cleaned;
|
|
795
|
+
if (!sub)
|
|
796
|
+
throw new Error(`Usage\n${usage()}`);
|
|
797
|
+
if (sub === "list")
|
|
798
|
+
return { kind: "friend.list", ...(agent ? { agent } : {}) };
|
|
799
|
+
if (sub === "show") {
|
|
800
|
+
const friendId = rest[0];
|
|
801
|
+
if (!friendId)
|
|
802
|
+
throw new Error(`Usage\n${usage()}`);
|
|
803
|
+
return { kind: "friend.show", friendId, ...(agent ? { agent } : {}) };
|
|
804
|
+
}
|
|
805
|
+
if (sub === "create") {
|
|
806
|
+
let name;
|
|
807
|
+
let trustLevel;
|
|
808
|
+
for (let i = 0; i < rest.length; i++) {
|
|
809
|
+
if (rest[i] === "--name" && rest[i + 1]) {
|
|
810
|
+
name = rest[i + 1];
|
|
811
|
+
i += 1;
|
|
812
|
+
}
|
|
813
|
+
else if (rest[i] === "--trust" && rest[i + 1]) {
|
|
814
|
+
trustLevel = rest[i + 1];
|
|
815
|
+
i += 1;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (!name)
|
|
819
|
+
throw new Error(`Usage\n${usage()}`);
|
|
820
|
+
return {
|
|
821
|
+
kind: "friend.create",
|
|
822
|
+
name,
|
|
823
|
+
...(trustLevel ? { trustLevel } : {}),
|
|
824
|
+
...(agent ? { agent } : {}),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
if (sub === "update") {
|
|
828
|
+
const friendId = rest[0];
|
|
829
|
+
if (!friendId)
|
|
830
|
+
throw new Error(`Usage: ouro friend update <id> --trust <level>`);
|
|
831
|
+
let trustLevel;
|
|
832
|
+
/* v8 ignore start -- flag parsing loop: tested via CLI parsing tests @preserve */
|
|
833
|
+
for (let i = 1; i < rest.length; i++) {
|
|
834
|
+
if (rest[i] === "--trust" && rest[i + 1]) {
|
|
835
|
+
trustLevel = rest[i + 1];
|
|
836
|
+
i += 1;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/* v8 ignore stop */
|
|
840
|
+
const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
|
|
841
|
+
if (!trustLevel || !VALID_TRUST_LEVELS.has(trustLevel)) {
|
|
842
|
+
throw new Error(`Usage: ouro friend update <id> --trust <stranger|acquaintance|friend|family>`);
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
kind: "friend.update",
|
|
846
|
+
friendId,
|
|
847
|
+
trustLevel: trustLevel,
|
|
848
|
+
...(agent ? { agent } : {}),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
if (sub === "link")
|
|
852
|
+
return parseLinkCommand(rest, "friend.link");
|
|
853
|
+
if (sub === "unlink")
|
|
854
|
+
return parseLinkCommand(rest, "friend.unlink");
|
|
855
|
+
throw new Error(`Usage\n${usage()}`);
|
|
856
|
+
}
|
|
857
|
+
function parseConfigCommand(args) {
|
|
858
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
859
|
+
const [sub, ...rest] = cleaned;
|
|
860
|
+
if (!sub)
|
|
861
|
+
throw new Error(`Usage\n${usage()}`);
|
|
862
|
+
if (sub === "model") {
|
|
863
|
+
if (!agent)
|
|
864
|
+
throw new Error("--agent is required for config model");
|
|
865
|
+
const modelName = rest[0];
|
|
866
|
+
if (!modelName)
|
|
867
|
+
throw new Error(`Usage: ouro config model --agent <name> <model-name>`);
|
|
868
|
+
return { kind: "config.model", agent, modelName };
|
|
869
|
+
}
|
|
870
|
+
if (sub === "models") {
|
|
871
|
+
if (!agent)
|
|
872
|
+
throw new Error("--agent is required for config models");
|
|
873
|
+
return { kind: "config.models", agent };
|
|
874
|
+
}
|
|
875
|
+
throw new Error(`Usage\n${usage()}`);
|
|
876
|
+
}
|
|
877
|
+
function parseMcpCommand(args) {
|
|
878
|
+
const [sub, ...rest] = args;
|
|
879
|
+
if (!sub)
|
|
880
|
+
throw new Error(`Usage\n${usage()}`);
|
|
881
|
+
if (sub === "list")
|
|
882
|
+
return { kind: "mcp.list" };
|
|
883
|
+
if (sub === "call") {
|
|
884
|
+
const server = rest[0];
|
|
885
|
+
const tool = rest[1];
|
|
886
|
+
if (!server || !tool)
|
|
887
|
+
throw new Error(`Usage\n${usage()}`);
|
|
888
|
+
const argsIdx = rest.indexOf("--args");
|
|
889
|
+
const mcpArgs = argsIdx !== -1 && rest[argsIdx + 1] ? rest[argsIdx + 1] : undefined;
|
|
890
|
+
return { kind: "mcp.call", server, tool, ...(mcpArgs ? { args: mcpArgs } : {}) };
|
|
891
|
+
}
|
|
892
|
+
throw new Error(`Usage\n${usage()}`);
|
|
893
|
+
}
|
|
894
|
+
function parseMcpServeCommand(args) {
|
|
895
|
+
let agent;
|
|
896
|
+
let friendId;
|
|
897
|
+
let socketOverride;
|
|
898
|
+
for (let i = 0; i < args.length; i++) {
|
|
899
|
+
if (args[i] === "--agent" && args[i + 1]) {
|
|
900
|
+
agent = args[++i];
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (args[i] === "--friend" && args[i + 1]) {
|
|
904
|
+
friendId = args[++i];
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (args[i] === "--socket" && args[i + 1]) {
|
|
908
|
+
socketOverride = args[++i];
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (!agent)
|
|
913
|
+
throw new Error("mcp-serve requires --agent <name>");
|
|
914
|
+
return { kind: "mcp-serve", agent, ...(friendId ? { friendId } : {}), ...(socketOverride ? { socketOverride } : {}) };
|
|
915
|
+
}
|
|
376
916
|
function parseOuroCommand(args) {
|
|
377
917
|
const [head, second] = args;
|
|
378
918
|
if (!head)
|
|
379
919
|
return { kind: "daemon.up" };
|
|
920
|
+
if (head === "--agent" && second) {
|
|
921
|
+
return parseOuroCommand(args.slice(2));
|
|
922
|
+
}
|
|
380
923
|
if (head === "up")
|
|
381
924
|
return { kind: "daemon.up" };
|
|
382
|
-
if (head === "
|
|
925
|
+
if (head === "dev") {
|
|
926
|
+
const devArgs = args.slice(1);
|
|
927
|
+
let repoPath;
|
|
928
|
+
let clone = false;
|
|
929
|
+
let clonePath;
|
|
930
|
+
for (let i = 0; i < devArgs.length; i++) {
|
|
931
|
+
if (devArgs[i] === "--repo-path" && devArgs[i + 1]) {
|
|
932
|
+
repoPath = devArgs[++i];
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
if (devArgs[i] === "--clone") {
|
|
936
|
+
clone = true;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (devArgs[i] === "--clone-path" && devArgs[i + 1]) {
|
|
940
|
+
clonePath = devArgs[++i];
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return { kind: "daemon.dev", repoPath, clone, clonePath };
|
|
945
|
+
}
|
|
946
|
+
if (head === "rollback")
|
|
947
|
+
return { kind: "rollback", ...(second ? { version: second } : {}) };
|
|
948
|
+
if (head === "versions")
|
|
949
|
+
return { kind: "versions" };
|
|
950
|
+
if (head === "stop" || head === "down")
|
|
383
951
|
return { kind: "daemon.stop" };
|
|
384
952
|
if (head === "status")
|
|
385
953
|
return { kind: "daemon.status" };
|
|
@@ -387,6 +955,38 @@ function parseOuroCommand(args) {
|
|
|
387
955
|
return { kind: "daemon.logs" };
|
|
388
956
|
if (head === "hatch")
|
|
389
957
|
return parseHatchCommand(args.slice(1));
|
|
958
|
+
if (head === "auth")
|
|
959
|
+
return parseAuthCommand(args.slice(1));
|
|
960
|
+
if (head === "task")
|
|
961
|
+
return parseTaskCommand(args.slice(1));
|
|
962
|
+
if (head === "reminder")
|
|
963
|
+
return parseReminderCommand(args.slice(1));
|
|
964
|
+
if (head === "friend")
|
|
965
|
+
return parseFriendCommand(args.slice(1));
|
|
966
|
+
if (head === "config")
|
|
967
|
+
return parseConfigCommand(args.slice(1));
|
|
968
|
+
if (head === "mcp")
|
|
969
|
+
return parseMcpCommand(args.slice(1));
|
|
970
|
+
if (head === "whoami") {
|
|
971
|
+
const { agent } = extractAgentFlag(args.slice(1));
|
|
972
|
+
return { kind: "whoami", ...(agent ? { agent } : {}) };
|
|
973
|
+
}
|
|
974
|
+
if (head === "session")
|
|
975
|
+
return parseSessionCommand(args.slice(1));
|
|
976
|
+
if (head === "changelog") {
|
|
977
|
+
const sliced = args.slice(1);
|
|
978
|
+
const { agent, rest: remaining } = extractAgentFlag(sliced);
|
|
979
|
+
let from;
|
|
980
|
+
const fromIdx = remaining.indexOf("--from");
|
|
981
|
+
if (fromIdx !== -1 && remaining[fromIdx + 1]) {
|
|
982
|
+
from = remaining[fromIdx + 1];
|
|
983
|
+
}
|
|
984
|
+
return { kind: "changelog", ...(from ? { from } : {}), ...(agent ? { agent } : {}) };
|
|
985
|
+
}
|
|
986
|
+
if (head === "thoughts")
|
|
987
|
+
return parseThoughtsCommand(args.slice(1));
|
|
988
|
+
if (head === "attention")
|
|
989
|
+
return parseAttentionCommand(args.slice(1));
|
|
390
990
|
if (head === "chat") {
|
|
391
991
|
if (!second)
|
|
392
992
|
throw new Error(`Usage\n${usage()}`);
|
|
@@ -398,40 +998,10 @@ function parseOuroCommand(args) {
|
|
|
398
998
|
return parsePokeCommand(args.slice(1));
|
|
399
999
|
if (head === "link")
|
|
400
1000
|
return parseLinkCommand(args.slice(1));
|
|
1001
|
+
if (head === "mcp-serve")
|
|
1002
|
+
return parseMcpServeCommand(args.slice(1));
|
|
401
1003
|
throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
|
|
402
1004
|
}
|
|
403
|
-
function defaultSendCommand(socketPath, command) {
|
|
404
|
-
return new Promise((resolve, reject) => {
|
|
405
|
-
const client = net.createConnection(socketPath);
|
|
406
|
-
let raw = "";
|
|
407
|
-
client.on("connect", () => {
|
|
408
|
-
client.write(JSON.stringify(command));
|
|
409
|
-
client.end();
|
|
410
|
-
});
|
|
411
|
-
client.on("data", (chunk) => {
|
|
412
|
-
raw += chunk.toString("utf-8");
|
|
413
|
-
});
|
|
414
|
-
client.on("error", reject);
|
|
415
|
-
client.on("end", () => {
|
|
416
|
-
const trimmed = raw.trim();
|
|
417
|
-
if (trimmed.length === 0 && command.kind === "daemon.stop") {
|
|
418
|
-
resolve({ ok: true, message: "daemon stopped" });
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (trimmed.length === 0) {
|
|
422
|
-
reject(new Error("Daemon returned empty response."));
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
try {
|
|
426
|
-
const parsed = JSON.parse(trimmed);
|
|
427
|
-
resolve(parsed);
|
|
428
|
-
}
|
|
429
|
-
catch (error) {
|
|
430
|
-
reject(error);
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
1005
|
function defaultStartDaemonProcess(socketPath) {
|
|
436
1006
|
const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
437
1007
|
const child = (0, child_process_1.spawn)("node", [entry, "--socket", socketPath], {
|
|
@@ -445,45 +1015,32 @@ function defaultWriteStdout(text) {
|
|
|
445
1015
|
// eslint-disable-next-line no-console -- terminal UX: CLI command output
|
|
446
1016
|
console.log(text);
|
|
447
1017
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1018
|
+
/**
|
|
1019
|
+
* Read the runtimeVersion from the first .ouro bundle's bundle-meta.json.
|
|
1020
|
+
* Returns undefined if none found or unreadable.
|
|
1021
|
+
*/
|
|
1022
|
+
function readFirstBundleMetaVersion(bundlesRoot) {
|
|
1023
|
+
try {
|
|
1024
|
+
if (!fs.existsSync(bundlesRoot))
|
|
1025
|
+
return undefined;
|
|
1026
|
+
const entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
|
|
1027
|
+
for (const entry of entries) {
|
|
1028
|
+
/* v8 ignore next -- skip non-.ouro dirs: tested via version-detect tests @preserve */
|
|
1029
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
|
|
1030
|
+
continue;
|
|
1031
|
+
const metaPath = path.join(bundlesRoot, entry.name, "bundle-meta.json");
|
|
1032
|
+
if (!fs.existsSync(metaPath))
|
|
1033
|
+
continue;
|
|
1034
|
+
const raw = fs.readFileSync(metaPath, "utf-8");
|
|
1035
|
+
const meta = JSON.parse(raw);
|
|
1036
|
+
if (meta.runtimeVersion)
|
|
1037
|
+
return meta.runtimeVersion;
|
|
464
1038
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
raw += chunk.toString("utf-8");
|
|
471
|
-
});
|
|
472
|
-
client.on("error", () => finalize(false));
|
|
473
|
-
client.on("end", () => {
|
|
474
|
-
if (raw.trim().length === 0) {
|
|
475
|
-
finalize(false);
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
try {
|
|
479
|
-
JSON.parse(raw);
|
|
480
|
-
finalize(true);
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
finalize(false);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
});
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
// Best effort — return undefined on any error
|
|
1042
|
+
}
|
|
1043
|
+
return undefined;
|
|
487
1044
|
}
|
|
488
1045
|
function defaultCleanupStaleSocket(socketPath) {
|
|
489
1046
|
if (fs.existsSync(socketPath)) {
|
|
@@ -519,9 +1076,38 @@ function defaultFallbackPendingMessage(command) {
|
|
|
519
1076
|
});
|
|
520
1077
|
return pendingPath;
|
|
521
1078
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1079
|
+
function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
1080
|
+
if (process.platform !== "darwin") {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const homeDir = os.homedir();
|
|
1084
|
+
const launchdDeps = {
|
|
1085
|
+
exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
|
|
1086
|
+
writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
|
|
1087
|
+
removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
|
|
1088
|
+
existsFile: (filePath) => fs.existsSync(filePath),
|
|
1089
|
+
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
1090
|
+
homeDir,
|
|
1091
|
+
userUid: process.getuid?.() ?? 0,
|
|
1092
|
+
};
|
|
1093
|
+
const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
1094
|
+
/* 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 */
|
|
1095
|
+
if (!fs.existsSync(entryPath)) {
|
|
1096
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1097
|
+
level: "warn",
|
|
1098
|
+
component: "daemon",
|
|
1099
|
+
event: "daemon.entry_path_missing",
|
|
1100
|
+
message: "entryPath does not exist on disk — plist may point to a stale location. Run 'ouro daemon install' from the correct location.",
|
|
1101
|
+
meta: { entryPath },
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
const logDir = (0, identity_1.getAgentDaemonLogsDir)();
|
|
1105
|
+
(0, launchd_1.installLaunchAgent)(launchdDeps, {
|
|
1106
|
+
nodePath: process.execPath,
|
|
1107
|
+
entryPath,
|
|
1108
|
+
socketPath,
|
|
1109
|
+
logDir,
|
|
1110
|
+
envPath: process.env.PATH,
|
|
525
1111
|
});
|
|
526
1112
|
}
|
|
527
1113
|
async function defaultPromptInput(question) {
|
|
@@ -539,61 +1125,11 @@ async function defaultPromptInput(question) {
|
|
|
539
1125
|
}
|
|
540
1126
|
}
|
|
541
1127
|
function defaultListDiscoveredAgents() {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
catch {
|
|
548
|
-
return [];
|
|
549
|
-
}
|
|
550
|
-
const discovered = [];
|
|
551
|
-
for (const entry of entries) {
|
|
552
|
-
if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
|
|
553
|
-
continue;
|
|
554
|
-
const agentName = entry.name.slice(0, -5);
|
|
555
|
-
const configPath = path.join(bundlesRoot, entry.name, "agent.json");
|
|
556
|
-
let enabled = true;
|
|
557
|
-
try {
|
|
558
|
-
const raw = fs.readFileSync(configPath, "utf-8");
|
|
559
|
-
const parsed = JSON.parse(raw);
|
|
560
|
-
if (typeof parsed.enabled === "boolean") {
|
|
561
|
-
enabled = parsed.enabled;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
catch {
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
if (enabled) {
|
|
568
|
-
discovered.push(agentName);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return discovered.sort((left, right) => left.localeCompare(right));
|
|
572
|
-
}
|
|
573
|
-
async function defaultLinkFriendIdentity(command) {
|
|
574
|
-
const friendStore = new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends"));
|
|
575
|
-
const current = await friendStore.get(command.friendId);
|
|
576
|
-
if (!current) {
|
|
577
|
-
return `friend not found: ${command.friendId}`;
|
|
578
|
-
}
|
|
579
|
-
const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
580
|
-
if (alreadyLinked) {
|
|
581
|
-
return `identity already linked: ${command.provider}:${command.externalId}`;
|
|
582
|
-
}
|
|
583
|
-
const now = new Date().toISOString();
|
|
584
|
-
await friendStore.put(command.friendId, {
|
|
585
|
-
...current,
|
|
586
|
-
externalIds: [
|
|
587
|
-
...current.externalIds,
|
|
588
|
-
{
|
|
589
|
-
provider: command.provider,
|
|
590
|
-
externalId: command.externalId,
|
|
591
|
-
linkedAt: now,
|
|
592
|
-
},
|
|
593
|
-
],
|
|
594
|
-
updatedAt: now,
|
|
1128
|
+
return (0, agent_discovery_1.listEnabledBundleAgents)({
|
|
1129
|
+
bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
|
|
1130
|
+
readdirSync: fs.readdirSync,
|
|
1131
|
+
readFileSync: fs.readFileSync,
|
|
595
1132
|
});
|
|
596
|
-
return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
|
|
597
1133
|
}
|
|
598
1134
|
function discoverExistingCredentials(secretsRoot) {
|
|
599
1135
|
const found = [];
|
|
@@ -626,16 +1162,16 @@ function discoverExistingCredentials(secretsRoot) {
|
|
|
626
1162
|
continue;
|
|
627
1163
|
for (const [provName, provConfig] of Object.entries(parsed.providers)) {
|
|
628
1164
|
if (provName === "anthropic" && provConfig.setupToken) {
|
|
629
|
-
found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken } });
|
|
1165
|
+
found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken }, providerConfig: { ...provConfig } });
|
|
630
1166
|
}
|
|
631
1167
|
else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
|
|
632
|
-
found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken } });
|
|
1168
|
+
found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken }, providerConfig: { ...provConfig } });
|
|
633
1169
|
}
|
|
634
1170
|
else if (provName === "minimax" && provConfig.apiKey) {
|
|
635
|
-
found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey } });
|
|
1171
|
+
found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey }, providerConfig: { ...provConfig } });
|
|
636
1172
|
}
|
|
637
1173
|
else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
|
|
638
|
-
found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment } });
|
|
1174
|
+
found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
|
|
639
1175
|
}
|
|
640
1176
|
}
|
|
641
1177
|
}
|
|
@@ -653,7 +1189,7 @@ function discoverExistingCredentials(secretsRoot) {
|
|
|
653
1189
|
async function defaultRunAdoptionSpecialist() {
|
|
654
1190
|
const { runCliSession } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
655
1191
|
const { patchRuntimeConfig } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
656
|
-
const { setAgentName } = await Promise.resolve().then(() => __importStar(require("../identity")));
|
|
1192
|
+
const { setAgentName, setAgentConfigOverride } = await Promise.resolve().then(() => __importStar(require("../identity")));
|
|
657
1193
|
const readlinePromises = await Promise.resolve().then(() => __importStar(require("readline/promises")));
|
|
658
1194
|
const crypto = await Promise.resolve().then(() => __importStar(require("crypto")));
|
|
659
1195
|
// Phase 1: cold CLI — collect provider/credentials with a simple readline
|
|
@@ -664,16 +1200,29 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
664
1200
|
};
|
|
665
1201
|
let providerRaw;
|
|
666
1202
|
let credentials = {};
|
|
1203
|
+
let providerConfig = {};
|
|
667
1204
|
const tempDir = path.join(os.tmpdir(), `ouro-hatch-${crypto.randomUUID()}`);
|
|
668
1205
|
try {
|
|
669
1206
|
const secretsRoot = path.join(os.homedir(), ".agentsecrets");
|
|
670
1207
|
const discovered = discoverExistingCredentials(secretsRoot);
|
|
1208
|
+
const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
|
|
1209
|
+
const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
|
|
1210
|
+
// Default models per provider (used when entering new credentials)
|
|
1211
|
+
const defaultModels = {
|
|
1212
|
+
anthropic: "claude-opus-4-6",
|
|
1213
|
+
minimax: "MiniMax-Text-01",
|
|
1214
|
+
"openai-codex": "gpt-5.4",
|
|
1215
|
+
"github-copilot": "claude-sonnet-4.6",
|
|
1216
|
+
azure: "",
|
|
1217
|
+
};
|
|
671
1218
|
if (discovered.length > 0) {
|
|
672
|
-
process.stdout.write(
|
|
1219
|
+
process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
|
|
673
1220
|
process.stdout.write("i found existing API credentials:\n\n");
|
|
674
1221
|
const unique = [...new Map(discovered.map((d) => [`${d.provider}`, d])).values()];
|
|
675
1222
|
for (let i = 0; i < unique.length; i++) {
|
|
676
|
-
|
|
1223
|
+
const model = unique[i].providerConfig.model || unique[i].providerConfig.deployment || "";
|
|
1224
|
+
const modelLabel = model ? `, ${model}` : "";
|
|
1225
|
+
process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (from ${unique[i].agentName})\n`);
|
|
677
1226
|
}
|
|
678
1227
|
process.stdout.write("\n");
|
|
679
1228
|
const choice = await coldPrompt("use one of these? enter number, or 'new' for a different key: ");
|
|
@@ -681,15 +1230,17 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
681
1230
|
if (idx >= 0 && idx < unique.length) {
|
|
682
1231
|
providerRaw = unique[idx].provider;
|
|
683
1232
|
credentials = unique[idx].credentials;
|
|
1233
|
+
providerConfig = unique[idx].providerConfig;
|
|
684
1234
|
}
|
|
685
1235
|
else {
|
|
686
|
-
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
|
|
1236
|
+
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
|
|
687
1237
|
if (!isAgentProvider(pRaw)) {
|
|
688
1238
|
process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
|
|
689
1239
|
coldRl.close();
|
|
690
1240
|
return null;
|
|
691
1241
|
}
|
|
692
1242
|
providerRaw = pRaw;
|
|
1243
|
+
providerConfig = { model: defaultModels[providerRaw] };
|
|
693
1244
|
if (providerRaw === "anthropic")
|
|
694
1245
|
credentials.setupToken = await coldPrompt("API key: ");
|
|
695
1246
|
if (providerRaw === "openai-codex")
|
|
@@ -704,15 +1255,16 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
704
1255
|
}
|
|
705
1256
|
}
|
|
706
1257
|
else {
|
|
707
|
-
process.stdout.write(
|
|
1258
|
+
process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
|
|
708
1259
|
process.stdout.write("i need an API key to power our conversation.\n\n");
|
|
709
|
-
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
|
|
1260
|
+
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
|
|
710
1261
|
if (!isAgentProvider(pRaw)) {
|
|
711
1262
|
process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
|
|
712
1263
|
coldRl.close();
|
|
713
1264
|
return null;
|
|
714
1265
|
}
|
|
715
1266
|
providerRaw = pRaw;
|
|
1267
|
+
providerConfig = { model: defaultModels[providerRaw] };
|
|
716
1268
|
if (providerRaw === "anthropic")
|
|
717
1269
|
credentials.setupToken = await coldPrompt("API key: ");
|
|
718
1270
|
if (providerRaw === "openai-codex")
|
|
@@ -731,17 +1283,31 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
731
1283
|
const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
|
|
732
1284
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
733
1285
|
const secretsRoot2 = path.join(os.homedir(), ".agentsecrets");
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1286
|
+
// Suppress non-critical log noise during adoption (no secrets.json, etc.)
|
|
1287
|
+
const { setRuntimeLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
|
|
1288
|
+
const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
|
|
1289
|
+
setRuntimeLogger(createLogger({ level: "error" }));
|
|
1290
|
+
// Configure runtime: set agent identity + config override so runAgent
|
|
1291
|
+
// doesn't try to read from ~/AgentBundles/AdoptionSpecialist.ouro/
|
|
740
1292
|
setAgentName("AdoptionSpecialist");
|
|
741
1293
|
// Build specialist system prompt
|
|
742
1294
|
const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
|
|
743
1295
|
const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
|
|
744
1296
|
const identity = (0, specialist_orchestrator_1.pickRandomIdentity)(identitiesDir);
|
|
1297
|
+
// Load identity-specific spinner phrases (falls back to DEFAULT_AGENT_PHRASES)
|
|
1298
|
+
const { loadIdentityPhrases } = await Promise.resolve().then(() => __importStar(require("./specialist-orchestrator")));
|
|
1299
|
+
const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
|
|
1300
|
+
setAgentConfigOverride({
|
|
1301
|
+
version: 1,
|
|
1302
|
+
enabled: true,
|
|
1303
|
+
provider: providerRaw,
|
|
1304
|
+
phrases,
|
|
1305
|
+
});
|
|
1306
|
+
patchRuntimeConfig({
|
|
1307
|
+
providers: {
|
|
1308
|
+
[providerRaw]: { ...providerConfig, ...credentials },
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
745
1311
|
const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
|
|
746
1312
|
const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
|
|
747
1313
|
tempDir,
|
|
@@ -763,6 +1329,10 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
763
1329
|
tools: specialistTools,
|
|
764
1330
|
execTool: specialistExecTool,
|
|
765
1331
|
exitOnToolCall: "complete_adoption",
|
|
1332
|
+
autoFirstTurn: true,
|
|
1333
|
+
banner: false,
|
|
1334
|
+
disableCommands: true,
|
|
1335
|
+
skipSystemPromptRefresh: true,
|
|
766
1336
|
messages: [
|
|
767
1337
|
{ role: "system", content: systemPrompt },
|
|
768
1338
|
{ role: "user", content: "hi" },
|
|
@@ -776,11 +1346,21 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
776
1346
|
}
|
|
777
1347
|
return null;
|
|
778
1348
|
}
|
|
779
|
-
catch {
|
|
1349
|
+
catch (err) {
|
|
1350
|
+
process.stderr.write(`\nouro adoption error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
780
1351
|
coldRl.close();
|
|
781
1352
|
return null;
|
|
782
1353
|
}
|
|
783
1354
|
finally {
|
|
1355
|
+
// Clear specialist config/identity so the hatched agent gets its own
|
|
1356
|
+
setAgentConfigOverride(null);
|
|
1357
|
+
const { resetProviderRuntime } = await Promise.resolve().then(() => __importStar(require("../core")));
|
|
1358
|
+
resetProviderRuntime();
|
|
1359
|
+
const { resetConfigCache } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
1360
|
+
resetConfigCache();
|
|
1361
|
+
// Restore default logging
|
|
1362
|
+
const { setRuntimeLogger: restoreLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
|
|
1363
|
+
restoreLogger(null);
|
|
784
1364
|
// Clean up temp dir if it still exists
|
|
785
1365
|
try {
|
|
786
1366
|
if (fs.existsSync(tempDir)) {
|
|
@@ -793,30 +1373,118 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
793
1373
|
}
|
|
794
1374
|
}
|
|
795
1375
|
/* v8 ignore stop */
|
|
796
|
-
function createDefaultOuroCliDeps(socketPath =
|
|
1376
|
+
function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH) {
|
|
797
1377
|
return {
|
|
798
1378
|
socketPath,
|
|
799
|
-
sendCommand:
|
|
1379
|
+
sendCommand: socket_client_1.sendDaemonCommand,
|
|
800
1380
|
startDaemonProcess: defaultStartDaemonProcess,
|
|
801
1381
|
writeStdout: defaultWriteStdout,
|
|
802
|
-
checkSocketAlive:
|
|
1382
|
+
checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
|
|
803
1383
|
cleanupStaleSocket: defaultCleanupStaleSocket,
|
|
804
1384
|
fallbackPendingMessage: defaultFallbackPendingMessage,
|
|
805
|
-
installSubagents: defaultInstallSubagents,
|
|
806
|
-
linkFriendIdentity: defaultLinkFriendIdentity,
|
|
807
1385
|
listDiscoveredAgents: defaultListDiscoveredAgents,
|
|
808
1386
|
runHatchFlow: hatch_flow_1.runHatchFlow,
|
|
809
1387
|
promptInput: defaultPromptInput,
|
|
810
1388
|
runAdoptionSpecialist: defaultRunAdoptionSpecialist,
|
|
1389
|
+
runAuthFlow: auth_flow_1.runRuntimeAuthFlow,
|
|
811
1390
|
registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
|
|
812
1391
|
installOuroCommand: ouro_path_installer_1.installOuroCommand,
|
|
1392
|
+
/* v8 ignore start -- self-healing: ensures active symlink matches running runtime version @preserve */
|
|
1393
|
+
ensureCurrentVersionInstalled: () => {
|
|
1394
|
+
const linkedVersion = (0, ouro_version_manager_1.getCurrentVersion)({});
|
|
1395
|
+
const version = (0, bundle_manifest_1.getPackageVersion)();
|
|
1396
|
+
if (linkedVersion === version)
|
|
1397
|
+
return;
|
|
1398
|
+
(0, ouro_version_manager_1.ensureLayout)({});
|
|
1399
|
+
const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
|
|
1400
|
+
const versionEntry = path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
|
|
1401
|
+
if (!fs.existsSync(versionEntry)) {
|
|
1402
|
+
(0, ouro_version_manager_1.installVersion)(version, {});
|
|
1403
|
+
}
|
|
1404
|
+
(0, ouro_version_manager_1.activateVersion)(version, {});
|
|
1405
|
+
},
|
|
1406
|
+
/* v8 ignore stop */
|
|
1407
|
+
/* v8 ignore start -- CLI version management defaults: integration code @preserve */
|
|
1408
|
+
checkForCliUpdate: async () => {
|
|
1409
|
+
const { checkForUpdate } = await Promise.resolve().then(() => __importStar(require("./update-checker")));
|
|
1410
|
+
return checkForUpdate((0, bundle_manifest_1.getPackageVersion)(), {
|
|
1411
|
+
fetchRegistryJson: async () => {
|
|
1412
|
+
const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli");
|
|
1413
|
+
return res.json();
|
|
1414
|
+
},
|
|
1415
|
+
distTag: "alpha",
|
|
1416
|
+
});
|
|
1417
|
+
},
|
|
1418
|
+
installCliVersion: async (version) => { (0, ouro_version_manager_1.installVersion)(version, {}); },
|
|
1419
|
+
activateCliVersion: (version) => { (0, ouro_version_manager_1.activateVersion)(version, {}); },
|
|
1420
|
+
getCurrentCliVersion: () => (0, ouro_version_manager_1.getCurrentVersion)({}),
|
|
1421
|
+
getPreviousCliVersion: () => (0, ouro_version_manager_1.getPreviousVersion)({}),
|
|
1422
|
+
listCliVersions: () => (0, ouro_version_manager_1.listInstalledVersions)({}),
|
|
1423
|
+
reExecFromNewVersion: (reArgs) => {
|
|
1424
|
+
const entry = path.join((0, ouro_version_manager_1.getOuroCliHome)(), "CurrentVersion", "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
|
|
1425
|
+
require("child_process").execFileSync("node", [entry, ...reArgs], { stdio: "inherit" });
|
|
1426
|
+
process.exit(0);
|
|
1427
|
+
},
|
|
1428
|
+
/* v8 ignore stop */
|
|
1429
|
+
syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
|
|
1430
|
+
ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
|
|
1431
|
+
ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
|
|
1432
|
+
/* v8 ignore start -- dev-mode defaults: tests inject mocks for mode detection and binary resolution @preserve */
|
|
1433
|
+
detectMode: () => (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()),
|
|
1434
|
+
getInstalledBinaryPath: () => {
|
|
1435
|
+
const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
|
|
1436
|
+
const binaryPath = path.join(cliHome, "bin", "ouro");
|
|
1437
|
+
return fs.existsSync(binaryPath) ? binaryPath : null;
|
|
1438
|
+
},
|
|
1439
|
+
execInstalledBinary: (binaryPath, binArgs) => {
|
|
1440
|
+
const { execFileSync } = require("child_process");
|
|
1441
|
+
execFileSync(binaryPath, binArgs, { stdio: "inherit" });
|
|
1442
|
+
process.exit(0);
|
|
1443
|
+
},
|
|
1444
|
+
/* v8 ignore stop */
|
|
813
1445
|
/* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
|
|
814
1446
|
startChat: async (agentName) => {
|
|
815
1447
|
const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
816
1448
|
await main(agentName);
|
|
817
1449
|
},
|
|
1450
|
+
scanSessions: async () => {
|
|
1451
|
+
const agentName = (0, identity_1.getAgentName)();
|
|
1452
|
+
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
1453
|
+
return (0, session_activity_1.listSessionActivity)({
|
|
1454
|
+
sessionsDir: path.join(agentRoot, "state", "sessions"),
|
|
1455
|
+
friendsDir: path.join(agentRoot, "friends"),
|
|
1456
|
+
agentName,
|
|
1457
|
+
}).map((entry) => ({
|
|
1458
|
+
friendId: entry.friendId,
|
|
1459
|
+
friendName: entry.friendName,
|
|
1460
|
+
channel: entry.channel,
|
|
1461
|
+
lastActivity: entry.lastActivityAt,
|
|
1462
|
+
}));
|
|
1463
|
+
},
|
|
818
1464
|
};
|
|
819
1465
|
}
|
|
1466
|
+
function formatMcpResponse(command, response) {
|
|
1467
|
+
if (command.kind === "mcp.list") {
|
|
1468
|
+
const allTools = response.data;
|
|
1469
|
+
if (!allTools || allTools.length === 0) {
|
|
1470
|
+
return response.message ?? "no tools available from connected MCP servers";
|
|
1471
|
+
}
|
|
1472
|
+
const lines = [];
|
|
1473
|
+
for (const entry of allTools) {
|
|
1474
|
+
lines.push(`[${entry.server}]`);
|
|
1475
|
+
for (const tool of entry.tools) {
|
|
1476
|
+
lines.push(` ${tool.name}: ${tool.description}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return lines.join("\n");
|
|
1480
|
+
}
|
|
1481
|
+
// mcp.call
|
|
1482
|
+
const result = response.data;
|
|
1483
|
+
if (!result) {
|
|
1484
|
+
return response.message ?? "no result";
|
|
1485
|
+
}
|
|
1486
|
+
return result.content.map((c) => c.text).join("\n");
|
|
1487
|
+
}
|
|
820
1488
|
function toDaemonCommand(command) {
|
|
821
1489
|
return command;
|
|
822
1490
|
}
|
|
@@ -824,28 +1492,17 @@ async function resolveHatchInput(command, deps) {
|
|
|
824
1492
|
const prompt = deps.promptInput;
|
|
825
1493
|
const agentName = command.agentName ?? (prompt ? await prompt("Hatchling name: ") : "");
|
|
826
1494
|
const humanName = command.humanName ?? (prompt ? await prompt("Your name: ") : os.userInfo().username);
|
|
827
|
-
const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex): ") : "");
|
|
1495
|
+
const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex|github-copilot): ") : "");
|
|
828
1496
|
if (!agentName || !humanName || !isAgentProvider(providerRaw)) {
|
|
829
1497
|
throw new Error(`Usage\n${usage()}`);
|
|
830
1498
|
}
|
|
831
|
-
const credentials =
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
if (providerRaw === "minimax" && !credentials.apiKey && prompt) {
|
|
839
|
-
credentials.apiKey = await prompt("MiniMax API key: ");
|
|
840
|
-
}
|
|
841
|
-
if (providerRaw === "azure") {
|
|
842
|
-
if (!credentials.apiKey && prompt)
|
|
843
|
-
credentials.apiKey = await prompt("Azure API key: ");
|
|
844
|
-
if (!credentials.endpoint && prompt)
|
|
845
|
-
credentials.endpoint = await prompt("Azure endpoint: ");
|
|
846
|
-
if (!credentials.deployment && prompt)
|
|
847
|
-
credentials.deployment = await prompt("Azure deployment: ");
|
|
848
|
-
}
|
|
1499
|
+
const credentials = await (0, auth_flow_1.resolveHatchCredentials)({
|
|
1500
|
+
agentName,
|
|
1501
|
+
provider: providerRaw,
|
|
1502
|
+
credentials: command.credentials,
|
|
1503
|
+
promptInput: prompt,
|
|
1504
|
+
runAuthFlow: deps.runAuthFlow,
|
|
1505
|
+
});
|
|
849
1506
|
return {
|
|
850
1507
|
agentName,
|
|
851
1508
|
humanName,
|
|
@@ -875,7 +1532,11 @@ async function performSystemSetup(deps) {
|
|
|
875
1532
|
// Install ouro command to PATH (non-blocking)
|
|
876
1533
|
if (deps.installOuroCommand) {
|
|
877
1534
|
try {
|
|
878
|
-
deps.installOuroCommand();
|
|
1535
|
+
const installResult = deps.installOuroCommand();
|
|
1536
|
+
/* v8 ignore next -- old-launcher repair hint: fires when stale ~/.local/bin/ouro is fixed @preserve */
|
|
1537
|
+
if (installResult.repairedOldLauncher) {
|
|
1538
|
+
deps.writeStdout("repaired stale ouro launcher at ~/.local/bin/ouro");
|
|
1539
|
+
}
|
|
879
1540
|
}
|
|
880
1541
|
catch (error) {
|
|
881
1542
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -887,28 +1548,296 @@ async function performSystemSetup(deps) {
|
|
|
887
1548
|
});
|
|
888
1549
|
}
|
|
889
1550
|
}
|
|
890
|
-
//
|
|
891
|
-
|
|
892
|
-
|
|
1551
|
+
// Self-healing: ensure current version is installed in ~/.ouro-cli/ layout.
|
|
1552
|
+
// Handles the case where the wrapper exists but CurrentVersion is missing
|
|
1553
|
+
// (e.g., first run after migration from old npx wrapper).
|
|
1554
|
+
if (deps.ensureCurrentVersionInstalled) {
|
|
1555
|
+
try {
|
|
1556
|
+
deps.ensureCurrentVersionInstalled();
|
|
1557
|
+
}
|
|
1558
|
+
catch (error) {
|
|
1559
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1560
|
+
level: "warn",
|
|
1561
|
+
component: "daemon",
|
|
1562
|
+
event: "daemon.system_setup_version_install_error",
|
|
1563
|
+
message: "failed to ensure current version installed",
|
|
1564
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error) },
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
893
1567
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1568
|
+
if (deps.syncGlobalOuroBotWrapper) {
|
|
1569
|
+
try {
|
|
1570
|
+
await Promise.resolve(deps.syncGlobalOuroBotWrapper());
|
|
1571
|
+
}
|
|
1572
|
+
catch (error) {
|
|
1573
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1574
|
+
level: "warn",
|
|
1575
|
+
component: "daemon",
|
|
1576
|
+
event: "daemon.system_setup_ouro_bot_wrapper_error",
|
|
1577
|
+
message: "failed to sync global ouro.bot wrapper",
|
|
1578
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// Ensure skill-management skill is available
|
|
1583
|
+
if (deps.ensureSkillManagement) {
|
|
1584
|
+
try {
|
|
1585
|
+
await deps.ensureSkillManagement();
|
|
1586
|
+
/* v8 ignore start -- defensive: ensureSkillManagement handles its own errors internally @preserve */
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1590
|
+
level: "warn",
|
|
1591
|
+
component: "daemon",
|
|
1592
|
+
event: "daemon.system_setup_skill_management_error",
|
|
1593
|
+
message: "failed to ensure skill-management skill",
|
|
1594
|
+
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
/* v8 ignore stop */
|
|
902
1598
|
}
|
|
903
1599
|
// Register .ouro bundle type (UTI on macOS)
|
|
904
1600
|
await registerOuroBundleTypeNonBlocking(deps);
|
|
905
1601
|
}
|
|
906
|
-
|
|
907
|
-
if (
|
|
1602
|
+
function executeTaskCommand(command, taskMod) {
|
|
1603
|
+
if (command.kind === "task.board") {
|
|
1604
|
+
if (command.status) {
|
|
1605
|
+
const lines = taskMod.boardStatus(command.status);
|
|
1606
|
+
return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
|
|
1607
|
+
}
|
|
1608
|
+
const board = taskMod.getBoard();
|
|
1609
|
+
return board.full || board.compact || "no tasks found";
|
|
1610
|
+
}
|
|
1611
|
+
if (command.kind === "task.create") {
|
|
1612
|
+
try {
|
|
1613
|
+
const created = taskMod.createTask({
|
|
1614
|
+
title: command.title,
|
|
1615
|
+
type: command.type ?? "one-shot",
|
|
1616
|
+
category: "general",
|
|
1617
|
+
body: "",
|
|
1618
|
+
});
|
|
1619
|
+
return `created: ${created}`;
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (command.kind === "task.update") {
|
|
1626
|
+
const result = taskMod.updateStatus(command.id, command.status);
|
|
1627
|
+
if (!result.ok) {
|
|
1628
|
+
return `error: ${result.reason ?? "status update failed"}`;
|
|
1629
|
+
}
|
|
1630
|
+
const archivedSuffix = result.archived && result.archived.length > 0
|
|
1631
|
+
? ` | archived: ${result.archived.join(", ")}`
|
|
1632
|
+
: "";
|
|
1633
|
+
return `updated: ${command.id} -> ${result.to}${archivedSuffix}`;
|
|
1634
|
+
}
|
|
1635
|
+
if (command.kind === "task.show") {
|
|
1636
|
+
const task = taskMod.getTask(command.id);
|
|
1637
|
+
if (!task)
|
|
1638
|
+
return `task not found: ${command.id}`;
|
|
1639
|
+
return [
|
|
1640
|
+
`title: ${task.title}`,
|
|
1641
|
+
`type: ${task.type}`,
|
|
1642
|
+
`status: ${task.status}`,
|
|
1643
|
+
`category: ${task.category}`,
|
|
1644
|
+
`created: ${task.created}`,
|
|
1645
|
+
`updated: ${task.updated}`,
|
|
1646
|
+
`path: ${task.path}`,
|
|
1647
|
+
task.body ? `\n${task.body}` : "",
|
|
1648
|
+
].filter(Boolean).join("\n");
|
|
1649
|
+
}
|
|
1650
|
+
if (command.kind === "task.actionable") {
|
|
1651
|
+
const lines = taskMod.boardAction();
|
|
1652
|
+
return lines.length > 0 ? lines.join("\n") : "no action required";
|
|
1653
|
+
}
|
|
1654
|
+
if (command.kind === "task.deps") {
|
|
1655
|
+
const lines = taskMod.boardDeps();
|
|
1656
|
+
return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
|
|
1657
|
+
}
|
|
1658
|
+
// command.kind === "task.sessions"
|
|
1659
|
+
const lines = taskMod.boardSessions();
|
|
1660
|
+
return lines.length > 0 ? lines.join("\n") : "no active sessions";
|
|
1661
|
+
}
|
|
1662
|
+
const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
|
|
1663
|
+
/* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
|
|
1664
|
+
function higherTrust(a, b) {
|
|
1665
|
+
const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
|
|
1666
|
+
const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
|
|
1667
|
+
return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
|
|
1668
|
+
}
|
|
1669
|
+
/* v8 ignore stop */
|
|
1670
|
+
async function executeFriendCommand(command, store) {
|
|
1671
|
+
if (command.kind === "friend.list") {
|
|
1672
|
+
const listAll = store.listAll;
|
|
1673
|
+
if (!listAll)
|
|
1674
|
+
return "friend store does not support listing";
|
|
1675
|
+
const friends = await listAll.call(store);
|
|
1676
|
+
if (friends.length === 0)
|
|
1677
|
+
return "no friends found";
|
|
1678
|
+
const lines = friends.map((f) => {
|
|
1679
|
+
const trust = f.trustLevel ?? "unknown";
|
|
1680
|
+
return `${f.id} ${f.name} ${trust}`;
|
|
1681
|
+
});
|
|
1682
|
+
return lines.join("\n");
|
|
1683
|
+
}
|
|
1684
|
+
if (command.kind === "friend.show") {
|
|
1685
|
+
const record = await store.get(command.friendId);
|
|
1686
|
+
if (!record)
|
|
1687
|
+
return `friend not found: ${command.friendId}`;
|
|
1688
|
+
return JSON.stringify(record, null, 2);
|
|
1689
|
+
}
|
|
1690
|
+
if (command.kind === "friend.create") {
|
|
1691
|
+
const now = new Date().toISOString();
|
|
1692
|
+
const id = (0, crypto_1.randomUUID)();
|
|
1693
|
+
const trustLevel = (command.trustLevel ?? "acquaintance");
|
|
1694
|
+
await store.put(id, {
|
|
1695
|
+
id,
|
|
1696
|
+
name: command.name,
|
|
1697
|
+
trustLevel,
|
|
1698
|
+
externalIds: [],
|
|
1699
|
+
tenantMemberships: [],
|
|
1700
|
+
toolPreferences: {},
|
|
1701
|
+
notes: {},
|
|
1702
|
+
totalTokens: 0,
|
|
1703
|
+
createdAt: now,
|
|
1704
|
+
updatedAt: now,
|
|
1705
|
+
schemaVersion: 1,
|
|
1706
|
+
});
|
|
1707
|
+
return `created: ${id} (${command.name}, ${trustLevel})`;
|
|
1708
|
+
}
|
|
1709
|
+
if (command.kind === "friend.update") {
|
|
1710
|
+
const current = await store.get(command.friendId);
|
|
1711
|
+
if (!current)
|
|
1712
|
+
return `friend not found: ${command.friendId}`;
|
|
1713
|
+
const now = new Date().toISOString();
|
|
1714
|
+
await store.put(command.friendId, {
|
|
1715
|
+
...current,
|
|
1716
|
+
trustLevel: command.trustLevel,
|
|
1717
|
+
role: command.trustLevel,
|
|
1718
|
+
updatedAt: now,
|
|
1719
|
+
});
|
|
1720
|
+
return `updated: ${command.friendId} → trust=${command.trustLevel}`;
|
|
1721
|
+
}
|
|
1722
|
+
if (command.kind === "friend.link") {
|
|
1723
|
+
const current = await store.get(command.friendId);
|
|
1724
|
+
if (!current)
|
|
1725
|
+
return `friend not found: ${command.friendId}`;
|
|
1726
|
+
const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
1727
|
+
if (alreadyLinked)
|
|
1728
|
+
return `identity already linked: ${command.provider}:${command.externalId}`;
|
|
1729
|
+
const now = new Date().toISOString();
|
|
1730
|
+
const newExternalIds = [
|
|
1731
|
+
...current.externalIds,
|
|
1732
|
+
{ provider: command.provider, externalId: command.externalId, linkedAt: now },
|
|
1733
|
+
];
|
|
1734
|
+
// Orphan cleanup: check if another friend has this externalId
|
|
1735
|
+
const orphan = await store.findByExternalId(command.provider, command.externalId);
|
|
1736
|
+
let mergeMessage = "";
|
|
1737
|
+
let mergedNotes = { ...current.notes };
|
|
1738
|
+
let mergedTrust = current.trustLevel;
|
|
1739
|
+
let orphanExternalIds = [];
|
|
1740
|
+
if (orphan && orphan.id !== command.friendId) {
|
|
1741
|
+
// Merge orphan's notes (target's notes take priority)
|
|
1742
|
+
mergedNotes = { ...orphan.notes, ...current.notes };
|
|
1743
|
+
// Keep higher trust level
|
|
1744
|
+
mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
|
|
1745
|
+
// Collect orphan's other externalIds (excluding the one being linked)
|
|
1746
|
+
orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
|
|
1747
|
+
await store.delete(orphan.id);
|
|
1748
|
+
mergeMessage = ` (merged orphan ${orphan.id})`;
|
|
1749
|
+
}
|
|
1750
|
+
await store.put(command.friendId, {
|
|
1751
|
+
...current,
|
|
1752
|
+
externalIds: [...newExternalIds, ...orphanExternalIds],
|
|
1753
|
+
notes: mergedNotes,
|
|
1754
|
+
trustLevel: mergedTrust,
|
|
1755
|
+
updatedAt: now,
|
|
1756
|
+
});
|
|
1757
|
+
return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
|
|
1758
|
+
}
|
|
1759
|
+
// command.kind === "friend.unlink"
|
|
1760
|
+
const current = await store.get(command.friendId);
|
|
1761
|
+
if (!current)
|
|
1762
|
+
return `friend not found: ${command.friendId}`;
|
|
1763
|
+
const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
|
|
1764
|
+
if (idx === -1)
|
|
1765
|
+
return `identity not linked: ${command.provider}:${command.externalId}`;
|
|
1766
|
+
const now = new Date().toISOString();
|
|
1767
|
+
const filtered = current.externalIds.filter((_, i) => i !== idx);
|
|
1768
|
+
await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
|
|
1769
|
+
return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
|
|
1770
|
+
}
|
|
1771
|
+
function executeReminderCommand(command, taskMod) {
|
|
1772
|
+
try {
|
|
1773
|
+
const created = taskMod.createTask({
|
|
1774
|
+
title: command.title,
|
|
1775
|
+
type: command.cadence ? "habit" : "one-shot",
|
|
1776
|
+
category: command.category ?? "reminder",
|
|
1777
|
+
body: command.body,
|
|
1778
|
+
scheduledAt: command.scheduledAt,
|
|
1779
|
+
cadence: command.cadence,
|
|
1780
|
+
requester: command.requester,
|
|
1781
|
+
});
|
|
1782
|
+
return `created: ${created}`;
|
|
1783
|
+
}
|
|
1784
|
+
catch (error) {
|
|
1785
|
+
return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
/* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
|
|
1789
|
+
function resolveDevRepoCwd(command, checkExists, deps) {
|
|
1790
|
+
if (command.repoPath)
|
|
1791
|
+
return path.resolve(command.repoPath);
|
|
1792
|
+
if (command.clone)
|
|
1793
|
+
return resolveClonePath(command, checkExists, deps);
|
|
1794
|
+
return deps.getRepoCwd ? deps.getRepoCwd() : (0, identity_1.getRepoRoot)();
|
|
1795
|
+
}
|
|
1796
|
+
/* v8 ignore stop */
|
|
1797
|
+
/* v8 ignore start -- clone/build: requires real git clone + npm install on disk @preserve */
|
|
1798
|
+
function resolveClonePath(command, checkExists, deps) {
|
|
1799
|
+
const cloneTarget = command.clonePath
|
|
1800
|
+
? path.resolve(command.clonePath)
|
|
1801
|
+
: path.join(os.homedir(), "Projects", "ouroboros");
|
|
1802
|
+
if (!checkExists(path.join(cloneTarget, ".git"))) {
|
|
1803
|
+
deps.writeStdout(`cloning ouroboros to ${cloneTarget}...`);
|
|
1804
|
+
try {
|
|
1805
|
+
(0, child_process_1.execSync)(`git clone ${identity_1.HARNESS_CANONICAL_REPO_URL} "${cloneTarget}"`, { stdio: "inherit" });
|
|
1806
|
+
}
|
|
1807
|
+
catch {
|
|
1808
|
+
throw new Error(`clone failed. check your network and try again, or clone manually and use --repo-path.`);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
deps.writeStdout(`repo already exists at ${cloneTarget}, pulling latest...`);
|
|
1813
|
+
try {
|
|
1814
|
+
(0, child_process_1.execSync)("git pull --ff-only", { cwd: cloneTarget, stdio: "inherit" });
|
|
1815
|
+
}
|
|
1816
|
+
catch {
|
|
1817
|
+
deps.writeStdout("pull failed (may have local changes). continuing with existing code.");
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
deps.writeStdout("building...");
|
|
1821
|
+
try {
|
|
1822
|
+
(0, child_process_1.execSync)("npm install && npm run build", { cwd: cloneTarget, stdio: "inherit" });
|
|
1823
|
+
}
|
|
1824
|
+
catch {
|
|
1825
|
+
throw new Error(`build failed in ${cloneTarget}. check the output above.`);
|
|
1826
|
+
}
|
|
1827
|
+
return cloneTarget;
|
|
1828
|
+
}
|
|
1829
|
+
/* v8 ignore stop */
|
|
1830
|
+
async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
1831
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
908
1832
|
const text = usage();
|
|
909
1833
|
deps.writeStdout(text);
|
|
910
1834
|
return text;
|
|
911
1835
|
}
|
|
1836
|
+
if (args.length === 1 && (args[0] === "-v" || args[0] === "--version")) {
|
|
1837
|
+
const text = formatVersionOutput();
|
|
1838
|
+
deps.writeStdout(text);
|
|
1839
|
+
return text;
|
|
1840
|
+
}
|
|
912
1841
|
let command;
|
|
913
1842
|
try {
|
|
914
1843
|
command = parseOuroCommand(args);
|
|
@@ -979,21 +1908,726 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
979
1908
|
meta: { kind: command.kind },
|
|
980
1909
|
});
|
|
981
1910
|
if (command.kind === "daemon.up") {
|
|
1911
|
+
// ── dev mode delegation: ouro up from a dev repo delegates to installed binary ──
|
|
1912
|
+
// Only runs when detectMode is explicitly injected (via createDefaultOuroCliDeps or tests)
|
|
1913
|
+
if (deps.detectMode) {
|
|
1914
|
+
const runtimeMode = deps.detectMode();
|
|
1915
|
+
if (runtimeMode === "dev") {
|
|
1916
|
+
/* v8 ignore next -- defensive: getInstalledBinaryPath always injected in tests @preserve */
|
|
1917
|
+
const installedBinary = deps.getInstalledBinaryPath ? deps.getInstalledBinaryPath() : null;
|
|
1918
|
+
if (installedBinary) {
|
|
1919
|
+
deps.writeStdout("delegating to installed ouro...");
|
|
1920
|
+
/* v8 ignore next 3 -- defensive: execInstalledBinary always injected; missing branch unreachable @preserve */
|
|
1921
|
+
if (deps.execInstalledBinary) {
|
|
1922
|
+
deps.execInstalledBinary(installedBinary, args);
|
|
1923
|
+
}
|
|
1924
|
+
/* v8 ignore next 2 -- unreachable after exec replaces process @preserve */
|
|
1925
|
+
return "";
|
|
1926
|
+
}
|
|
1927
|
+
const message = "no installed version found. run: npx @ouro.bot/cli@alpha";
|
|
1928
|
+
deps.writeStdout(message);
|
|
1929
|
+
return message;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
|
|
1933
|
+
// ── versioned CLI update check ──
|
|
1934
|
+
if (deps.checkForCliUpdate) {
|
|
1935
|
+
let pendingReExec = false;
|
|
1936
|
+
try {
|
|
1937
|
+
const updateResult = await deps.checkForCliUpdate();
|
|
1938
|
+
if (updateResult.available && updateResult.latestVersion) {
|
|
1939
|
+
/* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
|
|
1940
|
+
const currentVersion = linkedVersionBeforeUp ?? "unknown";
|
|
1941
|
+
await deps.installCliVersion(updateResult.latestVersion);
|
|
1942
|
+
deps.activateCliVersion(updateResult.latestVersion);
|
|
1943
|
+
deps.writeStdout(`ouro updated to ${updateResult.latestVersion} (was ${currentVersion})`);
|
|
1944
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
|
|
1945
|
+
/* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
|
|
1946
|
+
if (changelogCommand) {
|
|
1947
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
1948
|
+
}
|
|
1949
|
+
pendingReExec = true;
|
|
1950
|
+
}
|
|
1951
|
+
/* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
|
|
1952
|
+
}
|
|
1953
|
+
catch (error) {
|
|
1954
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1955
|
+
level: "warn",
|
|
1956
|
+
component: "daemon",
|
|
1957
|
+
event: "daemon.cli_update_check_error",
|
|
1958
|
+
message: "CLI update check failed",
|
|
1959
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
/* v8 ignore stop */
|
|
1963
|
+
if (pendingReExec) {
|
|
1964
|
+
deps.reExecFromNewVersion(args);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
982
1967
|
await performSystemSetup(deps);
|
|
1968
|
+
const linkedVersionAfterSetup = deps.getCurrentCliVersion?.() ?? null;
|
|
1969
|
+
const runtimeVersion = (0, bundle_manifest_1.getPackageVersion)();
|
|
1970
|
+
if (linkedVersionBeforeUp && linkedVersionBeforeUp !== runtimeVersion && linkedVersionAfterSetup === runtimeVersion) {
|
|
1971
|
+
deps.writeStdout(`ouro updated to ${runtimeVersion} (was ${linkedVersionBeforeUp})`);
|
|
1972
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(linkedVersionBeforeUp, runtimeVersion);
|
|
1973
|
+
if (changelogCommand) {
|
|
1974
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
1978
|
+
try {
|
|
1979
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
1980
|
+
}
|
|
1981
|
+
catch (error) {
|
|
1982
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1983
|
+
level: "warn",
|
|
1984
|
+
component: "daemon",
|
|
1985
|
+
event: "daemon.system_setup_launchd_error",
|
|
1986
|
+
message: "failed to persist daemon boot startup",
|
|
1987
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
// Run update hooks before starting daemon so user sees the output
|
|
1992
|
+
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
1993
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
1994
|
+
const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
|
|
1995
|
+
// Snapshot the previous CLI version from the first bundle-meta before
|
|
1996
|
+
// hooks overwrite it. This detects when npx downloaded a newer CLI.
|
|
1997
|
+
const previousCliVersion = readFirstBundleMetaVersion(bundlesRoot);
|
|
1998
|
+
const updateSummary = await (0, update_hooks_1.applyPendingUpdates)(bundlesRoot, currentVersion);
|
|
1999
|
+
// Notify about CLI binary update (npx downloaded a new version).
|
|
2000
|
+
// Skip when the symlink already points to the running version — that
|
|
2001
|
+
// means path 1 (checkForCliUpdate + reExecFromNewVersion) already
|
|
2002
|
+
// printed the update message before re-exec.
|
|
2003
|
+
/* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
|
|
2004
|
+
if (previousCliVersion && previousCliVersion !== currentVersion && linkedVersionBeforeUp !== currentVersion) {
|
|
2005
|
+
deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
|
|
2006
|
+
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(previousCliVersion, currentVersion);
|
|
2007
|
+
/* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
|
|
2008
|
+
if (changelogCommand) {
|
|
2009
|
+
deps.writeStdout(`review changes with: ${changelogCommand}`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
/* v8 ignore stop */
|
|
2013
|
+
if (updateSummary.updated.length > 0) {
|
|
2014
|
+
const agents = updateSummary.updated.map((e) => e.agent);
|
|
2015
|
+
const from = updateSummary.updated[0].from;
|
|
2016
|
+
const to = updateSummary.updated[0].to;
|
|
2017
|
+
const fromStr = from ? ` (was ${from})` : "";
|
|
2018
|
+
const count = agents.length;
|
|
2019
|
+
deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
|
|
2020
|
+
}
|
|
983
2021
|
const daemonResult = await ensureDaemonRunning(deps);
|
|
984
2022
|
deps.writeStdout(daemonResult.message);
|
|
985
2023
|
return daemonResult.message;
|
|
986
2024
|
}
|
|
2025
|
+
if (command.kind === "daemon.dev") {
|
|
2026
|
+
/* v8 ignore next -- defensive: existsSync always injected in tests @preserve */
|
|
2027
|
+
const checkExists = deps.existsSync ?? fs.existsSync;
|
|
2028
|
+
/* v8 ignore next -- repo resolution dispatched to v8-ignored helper @preserve */
|
|
2029
|
+
let repoCwd = resolveDevRepoCwd(command, checkExists, deps);
|
|
2030
|
+
let entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2031
|
+
if (!checkExists(entryPath) || !checkExists(path.join(repoCwd, ".git"))) {
|
|
2032
|
+
if (command.repoPath) {
|
|
2033
|
+
// Explicit --repo-path didn't have a valid repo — error
|
|
2034
|
+
const message = `no harness repo found at ${repoCwd}. run npm run build first.`;
|
|
2035
|
+
deps.writeStdout(message);
|
|
2036
|
+
return message;
|
|
2037
|
+
}
|
|
2038
|
+
/* v8 ignore start -- auto-clone: interactive prompt + existing repo discovery + real git/npm @preserve */
|
|
2039
|
+
const defaultClonePath = path.join(os.homedir(), "Projects", "ouroboros");
|
|
2040
|
+
if (checkExists(path.join(defaultClonePath, ".git"))) {
|
|
2041
|
+
deps.writeStdout(`found existing repo at ${defaultClonePath}`);
|
|
2042
|
+
try {
|
|
2043
|
+
repoCwd = resolveClonePath({ clonePath: defaultClonePath }, checkExists, deps);
|
|
2044
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2045
|
+
}
|
|
2046
|
+
catch (err) {
|
|
2047
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2048
|
+
deps.writeStdout(message);
|
|
2049
|
+
return message;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
else if (deps.promptInput) {
|
|
2053
|
+
deps.writeStdout("no harness repo found.");
|
|
2054
|
+
const answer = await deps.promptInput(`already have a checkout? enter its path, or press enter to clone to ${defaultClonePath}: `);
|
|
2055
|
+
const cloneTarget = answer.trim() || defaultClonePath;
|
|
2056
|
+
try {
|
|
2057
|
+
repoCwd = resolveClonePath({ clonePath: cloneTarget }, checkExists, deps);
|
|
2058
|
+
entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2059
|
+
}
|
|
2060
|
+
catch (err) {
|
|
2061
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2062
|
+
deps.writeStdout(message);
|
|
2063
|
+
return message;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
else {
|
|
2067
|
+
const message = `no harness repo found. run: ouro dev --repo-path /path/to/ouroboros`;
|
|
2068
|
+
deps.writeStdout(message);
|
|
2069
|
+
return message;
|
|
2070
|
+
}
|
|
2071
|
+
/* v8 ignore stop */
|
|
2072
|
+
}
|
|
2073
|
+
/* v8 ignore start -- defensive: ensureDaemonBootPersistence always injected in tests @preserve */
|
|
2074
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
2075
|
+
try {
|
|
2076
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
2077
|
+
/* v8 ignore next -- defensive: boot persistence error should not block dev mode @preserve */
|
|
2078
|
+
}
|
|
2079
|
+
catch (error) {
|
|
2080
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2081
|
+
level: "warn",
|
|
2082
|
+
component: "daemon",
|
|
2083
|
+
event: "daemon.dev_boot_persistence_error",
|
|
2084
|
+
message: "failed to persist daemon boot startup in dev mode",
|
|
2085
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
/* v8 ignore stop */
|
|
2089
|
+
}
|
|
2090
|
+
// Always force-restart in dev mode — you rebuilt, you want this code running
|
|
2091
|
+
/* v8 ignore start -- dev force-restart: socket alive/stop/spawn tested via integration; tests inject mocks @preserve */
|
|
2092
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
2093
|
+
if (alive) {
|
|
2094
|
+
try {
|
|
2095
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
2096
|
+
}
|
|
2097
|
+
catch { /* already stopping */ }
|
|
2098
|
+
}
|
|
2099
|
+
deps.cleanupStaleSocket(deps.socketPath);
|
|
2100
|
+
const devEntry = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
|
|
2101
|
+
const startDevDaemon = deps.startDaemonProcess === defaultStartDaemonProcess
|
|
2102
|
+
? async (sp) => {
|
|
2103
|
+
const child = (0, child_process_1.spawn)("node", [devEntry, "--socket", sp], { detached: true, stdio: "ignore" });
|
|
2104
|
+
child.unref();
|
|
2105
|
+
return { pid: child.pid ?? null };
|
|
2106
|
+
}
|
|
2107
|
+
: deps.startDaemonProcess;
|
|
2108
|
+
/* v8 ignore stop */
|
|
2109
|
+
const started = await startDevDaemon(deps.socketPath);
|
|
2110
|
+
/* v8 ignore next -- defensive: pid is null only when spawn fails silently @preserve */
|
|
2111
|
+
const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})`;
|
|
2112
|
+
deps.writeStdout(message);
|
|
2113
|
+
return message;
|
|
2114
|
+
}
|
|
2115
|
+
// ── rollback command (local, no daemon socket needed for symlinks) ──
|
|
2116
|
+
/* v8 ignore start -- rollback/versions: tested via daemon-cli-rollback/versions tests @preserve */
|
|
2117
|
+
if (command.kind === "rollback") {
|
|
2118
|
+
const currentVersion = deps.getCurrentCliVersion?.() ?? "unknown";
|
|
2119
|
+
if (command.version) {
|
|
2120
|
+
// Rollback to a specific version
|
|
2121
|
+
const installed = deps.listCliVersions?.() ?? [];
|
|
2122
|
+
if (!installed.includes(command.version)) {
|
|
2123
|
+
try {
|
|
2124
|
+
await deps.installCliVersion(command.version);
|
|
2125
|
+
}
|
|
2126
|
+
catch (error) {
|
|
2127
|
+
const message = `failed to install version ${command.version}: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
|
|
2128
|
+
deps.writeStdout(message);
|
|
2129
|
+
return message;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
deps.activateCliVersion(command.version);
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
// Rollback to previous version
|
|
2136
|
+
const previousVersion = deps.getPreviousCliVersion?.();
|
|
2137
|
+
if (!previousVersion) {
|
|
2138
|
+
const message = "no previous version to roll back to";
|
|
2139
|
+
deps.writeStdout(message);
|
|
2140
|
+
return message;
|
|
2141
|
+
}
|
|
2142
|
+
deps.activateCliVersion(previousVersion);
|
|
2143
|
+
command = { ...command, version: previousVersion };
|
|
2144
|
+
}
|
|
2145
|
+
// Stop daemon (non-fatal if not running)
|
|
2146
|
+
try {
|
|
2147
|
+
await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
|
|
2148
|
+
}
|
|
2149
|
+
catch {
|
|
2150
|
+
// Daemon may not be running — that's fine
|
|
2151
|
+
}
|
|
2152
|
+
const message = `rolled back to ${command.version} (was ${currentVersion})`;
|
|
2153
|
+
deps.writeStdout(message);
|
|
2154
|
+
return message;
|
|
2155
|
+
}
|
|
2156
|
+
// ── versions command (local install list + published update truth, no daemon socket needed) ──
|
|
2157
|
+
if (command.kind === "versions") {
|
|
2158
|
+
const versions = deps.listCliVersions?.() ?? [];
|
|
2159
|
+
const current = deps.getCurrentCliVersion?.();
|
|
2160
|
+
const previous = deps.getPreviousCliVersion?.();
|
|
2161
|
+
const localSection = versions.length === 0
|
|
2162
|
+
? "no versions installed"
|
|
2163
|
+
: versions.map((v) => {
|
|
2164
|
+
let line = v;
|
|
2165
|
+
if (v === current)
|
|
2166
|
+
line += " * current";
|
|
2167
|
+
if (v === previous)
|
|
2168
|
+
line += " (previous)";
|
|
2169
|
+
return line;
|
|
2170
|
+
}).join("\n");
|
|
2171
|
+
const sections = [localSection];
|
|
2172
|
+
if (deps.checkForCliUpdate) {
|
|
2173
|
+
try {
|
|
2174
|
+
const updateResult = await deps.checkForCliUpdate();
|
|
2175
|
+
if (updateResult.latestVersion) {
|
|
2176
|
+
sections.push(`published alpha: ${updateResult.latestVersion} (${updateResult.available ? "update available" : "up to date"})`);
|
|
2177
|
+
}
|
|
2178
|
+
else if (updateResult.error) {
|
|
2179
|
+
sections.push(`published alpha: unavailable (${updateResult.error})`);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
catch (err) {
|
|
2183
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2184
|
+
sections.push(`published alpha: unavailable (${reason})`);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
const message = sections.join("\n\n");
|
|
2188
|
+
deps.writeStdout(message);
|
|
2189
|
+
return message;
|
|
2190
|
+
}
|
|
2191
|
+
/* v8 ignore stop */
|
|
987
2192
|
if (command.kind === "daemon.logs" && deps.tailLogs) {
|
|
988
2193
|
deps.tailLogs();
|
|
989
2194
|
return "";
|
|
990
2195
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
2196
|
+
/* v8 ignore start — mcp-serve block binds to process.stdin/stdout; tested via mcp-server unit tests */
|
|
2197
|
+
// ── mcp-serve: start MCP server in-process on stdin/stdout ──
|
|
2198
|
+
if (command.kind === "mcp-serve") {
|
|
2199
|
+
const { createMcpServer } = await Promise.resolve().then(() => __importStar(require("./mcp-server")));
|
|
2200
|
+
const friendId = command.friendId ?? `local-${os.userInfo().username}`;
|
|
2201
|
+
const mcpSocketPath = command.socketOverride ?? deps.socketPath;
|
|
2202
|
+
const server = createMcpServer({
|
|
2203
|
+
agent: command.agent,
|
|
2204
|
+
friendId,
|
|
2205
|
+
socketPath: mcpSocketPath,
|
|
2206
|
+
stdin: process.stdin,
|
|
2207
|
+
stdout: process.stdout,
|
|
2208
|
+
});
|
|
2209
|
+
server.start();
|
|
2210
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2211
|
+
component: "daemon",
|
|
2212
|
+
event: "daemon.mcp_serve_started",
|
|
2213
|
+
message: "MCP server started via CLI",
|
|
2214
|
+
meta: { agent: command.agent, friendId },
|
|
2215
|
+
});
|
|
2216
|
+
// Keep process alive until stdin closes
|
|
2217
|
+
await new Promise((resolve) => {
|
|
2218
|
+
process.stdin.on("end", () => {
|
|
2219
|
+
server.stop();
|
|
2220
|
+
resolve();
|
|
2221
|
+
});
|
|
2222
|
+
});
|
|
2223
|
+
return "";
|
|
2224
|
+
}
|
|
2225
|
+
/* v8 ignore stop */
|
|
2226
|
+
// ── mcp subcommands (routed through daemon socket) ──
|
|
2227
|
+
if (command.kind === "mcp.list" || command.kind === "mcp.call") {
|
|
2228
|
+
const daemonCommand = toDaemonCommand(command);
|
|
2229
|
+
let response;
|
|
2230
|
+
try {
|
|
2231
|
+
response = await deps.sendCommand(deps.socketPath, daemonCommand);
|
|
2232
|
+
}
|
|
2233
|
+
catch {
|
|
2234
|
+
const message = "daemon unavailable — start with `ouro up` first";
|
|
2235
|
+
deps.writeStdout(message);
|
|
2236
|
+
return message;
|
|
2237
|
+
}
|
|
2238
|
+
if (!response.ok) {
|
|
2239
|
+
const message = response.error ?? "unknown error";
|
|
2240
|
+
deps.writeStdout(message);
|
|
2241
|
+
return message;
|
|
2242
|
+
}
|
|
2243
|
+
const message = formatMcpResponse(command, response);
|
|
2244
|
+
deps.writeStdout(message);
|
|
2245
|
+
return message;
|
|
2246
|
+
}
|
|
2247
|
+
// ── task subcommands (local, no daemon socket needed) ──
|
|
2248
|
+
if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
|
|
2249
|
+
command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
|
|
2250
|
+
command.kind === "task.sessions") {
|
|
2251
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2252
|
+
const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
|
|
2253
|
+
/* v8 ignore stop */
|
|
2254
|
+
const message = executeTaskCommand(command, taskMod);
|
|
2255
|
+
deps.writeStdout(message);
|
|
2256
|
+
return message;
|
|
2257
|
+
}
|
|
2258
|
+
// ── reminder subcommands (local, no daemon socket needed) ──
|
|
2259
|
+
if (command.kind === "reminder.create") {
|
|
2260
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2261
|
+
const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
|
|
2262
|
+
/* v8 ignore stop */
|
|
2263
|
+
const message = executeReminderCommand(command, taskMod);
|
|
2264
|
+
deps.writeStdout(message);
|
|
2265
|
+
return message;
|
|
2266
|
+
}
|
|
2267
|
+
// ── friend subcommands (local, no daemon socket needed) ──
|
|
2268
|
+
if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
|
|
2269
|
+
command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
|
|
2270
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2271
|
+
let store = deps.friendStore;
|
|
2272
|
+
if (!store) {
|
|
2273
|
+
// Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
|
|
2274
|
+
const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
|
|
2275
|
+
const friendsDir = agentName
|
|
2276
|
+
? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
|
|
2277
|
+
: path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
|
|
2278
|
+
store = new store_file_1.FileFriendStore(friendsDir);
|
|
2279
|
+
}
|
|
2280
|
+
/* v8 ignore stop */
|
|
2281
|
+
const message = await executeFriendCommand(command, store);
|
|
994
2282
|
deps.writeStdout(message);
|
|
995
2283
|
return message;
|
|
996
2284
|
}
|
|
2285
|
+
// ── auth (local, no daemon socket needed) ──
|
|
2286
|
+
if (command.kind === "auth.run") {
|
|
2287
|
+
const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent).config.provider;
|
|
2288
|
+
/* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
|
|
2289
|
+
const authRunner = deps.runAuthFlow ?? auth_flow_1.runRuntimeAuthFlow;
|
|
2290
|
+
const result = await authRunner({
|
|
2291
|
+
agentName: command.agent,
|
|
2292
|
+
provider,
|
|
2293
|
+
promptInput: deps.promptInput,
|
|
2294
|
+
});
|
|
2295
|
+
// Behavior: ouro auth stores credentials only — does NOT switch provider.
|
|
2296
|
+
// Use `ouro auth switch` to change the active provider.
|
|
2297
|
+
deps.writeStdout(result.message);
|
|
2298
|
+
// Verify the credentials actually work by pinging the provider
|
|
2299
|
+
/* v8 ignore start -- integration: real API ping after auth @preserve */
|
|
2300
|
+
try {
|
|
2301
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2302
|
+
const status = await verifyProviderCredentials(provider, secrets.providers);
|
|
2303
|
+
deps.writeStdout(`${provider}: ${status}`);
|
|
2304
|
+
}
|
|
2305
|
+
catch {
|
|
2306
|
+
// Verification failure is non-blocking — credentials were saved regardless
|
|
2307
|
+
}
|
|
2308
|
+
/* v8 ignore stop */
|
|
2309
|
+
return result.message;
|
|
2310
|
+
}
|
|
2311
|
+
// ── auth verify (local, no daemon socket needed) ──
|
|
2312
|
+
/* v8 ignore start -- auth verify/switch: tested in daemon-cli.test.ts but v8 traces differ in CI @preserve */
|
|
2313
|
+
if (command.kind === "auth.verify") {
|
|
2314
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2315
|
+
const providers = secrets.providers;
|
|
2316
|
+
if (command.provider) {
|
|
2317
|
+
const status = await verifyProviderCredentials(command.provider, providers);
|
|
2318
|
+
const message = `${command.provider}: ${status}`;
|
|
2319
|
+
deps.writeStdout(message);
|
|
2320
|
+
return message;
|
|
2321
|
+
}
|
|
2322
|
+
const lines = [];
|
|
2323
|
+
for (const p of Object.keys(providers)) {
|
|
2324
|
+
const status = await verifyProviderCredentials(p, providers);
|
|
2325
|
+
lines.push(`${p}: ${status}`);
|
|
2326
|
+
}
|
|
2327
|
+
const message = lines.join("\n");
|
|
2328
|
+
deps.writeStdout(message);
|
|
2329
|
+
return message;
|
|
2330
|
+
}
|
|
2331
|
+
// ── auth switch (local, no daemon socket needed) ──
|
|
2332
|
+
if (command.kind === "auth.switch") {
|
|
2333
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2334
|
+
const providerSecrets = secrets.providers[command.provider];
|
|
2335
|
+
if (!providerSecrets || !hasStoredCredentials(command.provider, providerSecrets)) {
|
|
2336
|
+
const message = `no credentials stored for ${command.provider}. Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`;
|
|
2337
|
+
deps.writeStdout(message);
|
|
2338
|
+
return message;
|
|
2339
|
+
}
|
|
2340
|
+
// Verify credentials actually work before switching
|
|
2341
|
+
const status = await verifyProviderCredentials(command.provider, secrets.providers);
|
|
2342
|
+
if (!status.startsWith("ok")) {
|
|
2343
|
+
const message = `${command.provider}: ${status}. fix credentials with \`ouro auth --agent ${command.agent} --provider ${command.provider}\` before switching.`;
|
|
2344
|
+
deps.writeStdout(message);
|
|
2345
|
+
return message;
|
|
2346
|
+
}
|
|
2347
|
+
(0, auth_flow_1.writeAgentProviderSelection)(command.agent, command.provider);
|
|
2348
|
+
const message = `switched ${command.agent} to ${command.provider} (verified working)`;
|
|
2349
|
+
deps.writeStdout(message);
|
|
2350
|
+
return message;
|
|
2351
|
+
}
|
|
2352
|
+
/* v8 ignore stop */
|
|
2353
|
+
// ── config models (local, no daemon socket needed) ──
|
|
2354
|
+
/* v8 ignore start -- config models: tested via daemon-cli.test.ts @preserve */
|
|
2355
|
+
if (command.kind === "config.models") {
|
|
2356
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
|
|
2357
|
+
const provider = config.provider;
|
|
2358
|
+
if (provider !== "github-copilot") {
|
|
2359
|
+
const message = `model listing not available for ${provider} — check provider documentation.`;
|
|
2360
|
+
deps.writeStdout(message);
|
|
2361
|
+
return message;
|
|
2362
|
+
}
|
|
2363
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2364
|
+
const ghConfig = secrets.providers["github-copilot"];
|
|
2365
|
+
if (!ghConfig.githubToken || !ghConfig.baseUrl) {
|
|
2366
|
+
throw new Error(`github-copilot credentials not configured. Run \`ouro auth --agent ${command.agent} --provider github-copilot\` first.`);
|
|
2367
|
+
}
|
|
2368
|
+
const fetchFn = deps.fetchImpl ?? fetch;
|
|
2369
|
+
const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
|
|
2370
|
+
if (models.length === 0) {
|
|
2371
|
+
const message = "no models found";
|
|
2372
|
+
deps.writeStdout(message);
|
|
2373
|
+
return message;
|
|
2374
|
+
}
|
|
2375
|
+
const lines = ["available models:"];
|
|
2376
|
+
for (const m of models) {
|
|
2377
|
+
const caps = m.capabilities?.length ? ` (${m.capabilities.join(", ")})` : "";
|
|
2378
|
+
lines.push(` ${m.id}${caps}`);
|
|
2379
|
+
}
|
|
2380
|
+
const message = lines.join("\n");
|
|
2381
|
+
deps.writeStdout(message);
|
|
2382
|
+
return message;
|
|
2383
|
+
}
|
|
2384
|
+
/* v8 ignore stop */
|
|
2385
|
+
// ── config model (local, no daemon socket needed) ──
|
|
2386
|
+
/* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
|
|
2387
|
+
if (command.kind === "config.model") {
|
|
2388
|
+
// Validate model availability for github-copilot before writing
|
|
2389
|
+
const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
|
|
2390
|
+
if (config.provider === "github-copilot") {
|
|
2391
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
|
|
2392
|
+
const ghConfig = secrets.providers["github-copilot"];
|
|
2393
|
+
if (ghConfig.githubToken && ghConfig.baseUrl) {
|
|
2394
|
+
const fetchFn = deps.fetchImpl ?? fetch;
|
|
2395
|
+
try {
|
|
2396
|
+
const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
|
|
2397
|
+
const available = models.map((m) => m.id);
|
|
2398
|
+
if (available.length > 0 && !available.includes(command.modelName)) {
|
|
2399
|
+
const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
|
|
2400
|
+
deps.writeStdout(message);
|
|
2401
|
+
return message;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
catch {
|
|
2405
|
+
// Catalog validation failed — fall through to ping test
|
|
2406
|
+
}
|
|
2407
|
+
// Ping test: verify the model actually works before switching
|
|
2408
|
+
const pingResult = await pingGithubCopilotModel(ghConfig.baseUrl, ghConfig.githubToken, command.modelName, fetchFn);
|
|
2409
|
+
if (!pingResult.ok) {
|
|
2410
|
+
const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
|
|
2411
|
+
deps.writeStdout(message);
|
|
2412
|
+
return message;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, command.modelName);
|
|
2417
|
+
const message = previousModel
|
|
2418
|
+
? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
|
|
2419
|
+
: `set ${command.agent} model on ${provider}: ${command.modelName}`;
|
|
2420
|
+
deps.writeStdout(message);
|
|
2421
|
+
return message;
|
|
2422
|
+
}
|
|
2423
|
+
/* v8 ignore stop */
|
|
2424
|
+
// ── whoami (local, no daemon socket needed) ──
|
|
2425
|
+
if (command.kind === "whoami") {
|
|
2426
|
+
if (command.agent) {
|
|
2427
|
+
const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
|
|
2428
|
+
const message = [
|
|
2429
|
+
`agent: ${command.agent}`,
|
|
2430
|
+
`home: ${agentRoot}`,
|
|
2431
|
+
`bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
|
|
2432
|
+
].join("\n");
|
|
2433
|
+
deps.writeStdout(message);
|
|
2434
|
+
return message;
|
|
2435
|
+
}
|
|
2436
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2437
|
+
try {
|
|
2438
|
+
const info = deps.whoamiInfo
|
|
2439
|
+
? deps.whoamiInfo()
|
|
2440
|
+
: {
|
|
2441
|
+
agentName: (0, identity_1.getAgentName)(),
|
|
2442
|
+
homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
|
|
2443
|
+
bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
|
|
2444
|
+
};
|
|
2445
|
+
const message = [
|
|
2446
|
+
`agent: ${info.agentName}`,
|
|
2447
|
+
`home: ${info.homePath}`,
|
|
2448
|
+
`bones: ${info.bonesVersion}`,
|
|
2449
|
+
].join("\n");
|
|
2450
|
+
deps.writeStdout(message);
|
|
2451
|
+
return message;
|
|
2452
|
+
}
|
|
2453
|
+
catch {
|
|
2454
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
2455
|
+
deps.writeStdout(message);
|
|
2456
|
+
return message;
|
|
2457
|
+
}
|
|
2458
|
+
/* v8 ignore stop */
|
|
2459
|
+
}
|
|
2460
|
+
// ── changelog (local, no daemon socket needed) ──
|
|
2461
|
+
if (command.kind === "changelog") {
|
|
2462
|
+
try {
|
|
2463
|
+
const changelogPath = deps.getChangelogPath
|
|
2464
|
+
? deps.getChangelogPath()
|
|
2465
|
+
: (0, bundle_manifest_1.getChangelogPath)();
|
|
2466
|
+
const raw = fs.readFileSync(changelogPath, "utf-8");
|
|
2467
|
+
const parsed = JSON.parse(raw);
|
|
2468
|
+
const entries = Array.isArray(parsed) ? parsed : (parsed.versions ?? []);
|
|
2469
|
+
let filtered = entries;
|
|
2470
|
+
if (command.from) {
|
|
2471
|
+
const fromVersion = command.from;
|
|
2472
|
+
filtered = entries.filter((e) => semver.valid(e.version) && semver.gt(e.version, fromVersion));
|
|
2473
|
+
}
|
|
2474
|
+
if (filtered.length === 0) {
|
|
2475
|
+
const message = "no changelog entries found.";
|
|
2476
|
+
deps.writeStdout(message);
|
|
2477
|
+
return message;
|
|
2478
|
+
}
|
|
2479
|
+
const lines = [];
|
|
2480
|
+
for (const entry of filtered) {
|
|
2481
|
+
lines.push(`## ${entry.version}${entry.date ? ` (${entry.date})` : ""}`);
|
|
2482
|
+
if (entry.changes) {
|
|
2483
|
+
for (const change of entry.changes) {
|
|
2484
|
+
lines.push(`- ${change}`);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
lines.push("");
|
|
2488
|
+
}
|
|
2489
|
+
const message = lines.join("\n").trim();
|
|
2490
|
+
deps.writeStdout(message);
|
|
2491
|
+
return message;
|
|
2492
|
+
}
|
|
2493
|
+
catch {
|
|
2494
|
+
const message = "no changelog entries found.";
|
|
2495
|
+
deps.writeStdout(message);
|
|
2496
|
+
return message;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
// ── thoughts (local, no daemon socket needed) ──
|
|
2500
|
+
if (command.kind === "thoughts") {
|
|
2501
|
+
try {
|
|
2502
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
2503
|
+
const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
|
|
2504
|
+
const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
|
|
2505
|
+
if (command.json) {
|
|
2506
|
+
try {
|
|
2507
|
+
const raw = fs.readFileSync(sessionFilePath, "utf-8");
|
|
2508
|
+
deps.writeStdout(raw);
|
|
2509
|
+
return raw;
|
|
2510
|
+
}
|
|
2511
|
+
catch {
|
|
2512
|
+
const message = "no inner dialog session found";
|
|
2513
|
+
deps.writeStdout(message);
|
|
2514
|
+
return message;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
|
|
2518
|
+
const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
|
|
2519
|
+
deps.writeStdout(message);
|
|
2520
|
+
if (command.follow) {
|
|
2521
|
+
deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
|
|
2522
|
+
/* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
|
|
2523
|
+
const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
|
|
2524
|
+
deps.writeStdout("\n" + formatted);
|
|
2525
|
+
});
|
|
2526
|
+
/* v8 ignore stop */
|
|
2527
|
+
// Block until process exit; cleanup watcher on SIGINT/SIGTERM
|
|
2528
|
+
return new Promise((resolve) => {
|
|
2529
|
+
const cleanup = () => { stop(); resolve(message); };
|
|
2530
|
+
process.once("SIGINT", cleanup);
|
|
2531
|
+
process.once("SIGTERM", cleanup);
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
return message;
|
|
2535
|
+
}
|
|
2536
|
+
catch {
|
|
2537
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
2538
|
+
deps.writeStdout(message);
|
|
2539
|
+
return message;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
// ── attention queue (local, no daemon socket needed) ──
|
|
2543
|
+
/* v8 ignore start -- CLI attention handler: requires real obligation store on disk @preserve */
|
|
2544
|
+
if (command.kind === "attention.list" || command.kind === "attention.show" || command.kind === "attention.history") {
|
|
2545
|
+
try {
|
|
2546
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
2547
|
+
const { listActiveObligations, readObligation } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
|
|
2548
|
+
if (command.kind === "attention.list") {
|
|
2549
|
+
const obligations = listActiveObligations(agentName);
|
|
2550
|
+
if (obligations.length === 0) {
|
|
2551
|
+
const message = "nothing held — attention queue is empty";
|
|
2552
|
+
deps.writeStdout(message);
|
|
2553
|
+
return message;
|
|
2554
|
+
}
|
|
2555
|
+
const lines = obligations.map((o) => `[${o.id}] ${o.origin.friendId} via ${o.origin.channel}/${o.origin.key} — ${o.delegatedContent.slice(0, 60)}${o.delegatedContent.length > 60 ? "..." : ""} (${o.status})`);
|
|
2556
|
+
const message = lines.join("\n");
|
|
2557
|
+
deps.writeStdout(message);
|
|
2558
|
+
return message;
|
|
2559
|
+
}
|
|
2560
|
+
if (command.kind === "attention.show") {
|
|
2561
|
+
const obligation = readObligation(agentName, command.id);
|
|
2562
|
+
if (!obligation) {
|
|
2563
|
+
const message = `no obligation found with id ${command.id}`;
|
|
2564
|
+
deps.writeStdout(message);
|
|
2565
|
+
return message;
|
|
2566
|
+
}
|
|
2567
|
+
const message = JSON.stringify(obligation, null, 2);
|
|
2568
|
+
deps.writeStdout(message);
|
|
2569
|
+
return message;
|
|
2570
|
+
}
|
|
2571
|
+
// attention.history: show returned obligations
|
|
2572
|
+
const { getObligationsDir } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
|
|
2573
|
+
const obligationsDir = getObligationsDir(agentName);
|
|
2574
|
+
let entries = [];
|
|
2575
|
+
try {
|
|
2576
|
+
entries = fs.readdirSync(obligationsDir);
|
|
2577
|
+
}
|
|
2578
|
+
catch { /* empty */ }
|
|
2579
|
+
const returned = entries
|
|
2580
|
+
.filter((e) => e.endsWith(".json"))
|
|
2581
|
+
.map((e) => { try {
|
|
2582
|
+
return JSON.parse(fs.readFileSync(path.join(obligationsDir, e), "utf-8"));
|
|
2583
|
+
}
|
|
2584
|
+
catch {
|
|
2585
|
+
return null;
|
|
2586
|
+
} })
|
|
2587
|
+
.filter((o) => o?.status === "returned")
|
|
2588
|
+
.sort((a, b) => (b.returnedAt ?? 0) - (a.returnedAt ?? 0))
|
|
2589
|
+
.slice(0, 20);
|
|
2590
|
+
if (returned.length === 0) {
|
|
2591
|
+
const message = "no surfacing history yet";
|
|
2592
|
+
deps.writeStdout(message);
|
|
2593
|
+
return message;
|
|
2594
|
+
}
|
|
2595
|
+
const lines = returned.map((o) => {
|
|
2596
|
+
const when = o.returnedAt ? new Date(o.returnedAt).toISOString() : "unknown";
|
|
2597
|
+
return `[${o.id}] → ${o.origin.friendId} via ${o.returnTarget ?? "unknown"} at ${when}`;
|
|
2598
|
+
});
|
|
2599
|
+
const message = lines.join("\n");
|
|
2600
|
+
deps.writeStdout(message);
|
|
2601
|
+
return message;
|
|
2602
|
+
}
|
|
2603
|
+
catch {
|
|
2604
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
2605
|
+
deps.writeStdout(message);
|
|
2606
|
+
return message;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
/* v8 ignore stop */
|
|
2610
|
+
// ── session list (local, no daemon socket needed) ──
|
|
2611
|
+
if (command.kind === "session.list") {
|
|
2612
|
+
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
2613
|
+
const scanner = deps.scanSessions ?? (async () => []);
|
|
2614
|
+
/* v8 ignore stop */
|
|
2615
|
+
const sessions = await scanner();
|
|
2616
|
+
if (sessions.length === 0) {
|
|
2617
|
+
const message = "no active sessions";
|
|
2618
|
+
deps.writeStdout(message);
|
|
2619
|
+
return message;
|
|
2620
|
+
}
|
|
2621
|
+
const lines = sessions.map((s) => `${s.friendId} ${s.friendName} ${s.channel} ${s.lastActivity}`);
|
|
2622
|
+
const message = lines.join("\n");
|
|
2623
|
+
deps.writeStdout(message);
|
|
2624
|
+
return message;
|
|
2625
|
+
}
|
|
2626
|
+
if (command.kind === "chat.connect" && deps.startChat) {
|
|
2627
|
+
await ensureDaemonRunning(deps);
|
|
2628
|
+
await deps.startChat(command.agent);
|
|
2629
|
+
return "";
|
|
2630
|
+
}
|
|
997
2631
|
if (command.kind === "hatch.start") {
|
|
998
2632
|
// Route through adoption specialist when no explicit hatch args were provided
|
|
999
2633
|
const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
|
|
@@ -1041,6 +2675,16 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
1041
2675
|
deps.writeStdout(message);
|
|
1042
2676
|
return message;
|
|
1043
2677
|
}
|
|
2678
|
+
if (command.kind === "daemon.status" && isDaemonUnavailableError(error)) {
|
|
2679
|
+
const message = daemonUnavailableStatusOutput(deps.socketPath);
|
|
2680
|
+
deps.writeStdout(message);
|
|
2681
|
+
return message;
|
|
2682
|
+
}
|
|
2683
|
+
if (command.kind === "daemon.stop" && isDaemonUnavailableError(error)) {
|
|
2684
|
+
const message = "daemon not running";
|
|
2685
|
+
deps.writeStdout(message);
|
|
2686
|
+
return message;
|
|
2687
|
+
}
|
|
1044
2688
|
throw error;
|
|
1045
2689
|
}
|
|
1046
2690
|
const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
|