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