@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +3 -1
  2. package/dist/heart/config.js +34 -0
  3. package/dist/heart/core.js +41 -2
  4. package/dist/heart/daemon/daemon-cli.js +280 -22
  5. package/dist/heart/daemon/daemon.js +3 -0
  6. package/dist/heart/daemon/hatch-animation.js +28 -0
  7. package/dist/heart/daemon/hatch-flow.js +3 -1
  8. package/dist/heart/daemon/hatch-specialist.js +6 -1
  9. package/dist/heart/daemon/log-tailer.js +146 -0
  10. package/dist/heart/daemon/os-cron.js +260 -0
  11. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  12. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  13. package/dist/heart/daemon/ouro-entry.js +0 -0
  14. package/dist/heart/daemon/process-manager.js +18 -1
  15. package/dist/heart/daemon/runtime-logging.js +9 -5
  16. package/dist/heart/daemon/specialist-orchestrator.js +161 -0
  17. package/dist/heart/daemon/specialist-prompt.js +56 -0
  18. package/dist/heart/daemon/specialist-session.js +150 -0
  19. package/dist/heart/daemon/specialist-tools.js +132 -0
  20. package/dist/heart/daemon/task-scheduler.js +4 -1
  21. package/dist/heart/identity.js +28 -3
  22. package/dist/heart/providers/anthropic.js +3 -0
  23. package/dist/heart/streaming.js +3 -0
  24. package/dist/mind/associative-recall.js +23 -2
  25. package/dist/mind/context.js +85 -1
  26. package/dist/mind/friends/channel.js +8 -0
  27. package/dist/mind/friends/types.js +1 -1
  28. package/dist/mind/memory.js +62 -0
  29. package/dist/mind/pending.js +93 -0
  30. package/dist/mind/prompt-refresh.js +20 -0
  31. package/dist/mind/prompt.js +101 -0
  32. package/dist/nerves/coverage/file-completeness.js +14 -4
  33. package/dist/repertoire/tools-base.js +92 -0
  34. package/dist/repertoire/tools.js +3 -3
  35. package/dist/senses/bluebubbles-client.js +279 -0
  36. package/dist/senses/bluebubbles-entry.js +11 -0
  37. package/dist/senses/bluebubbles-model.js +253 -0
  38. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  39. package/dist/senses/bluebubbles.js +332 -0
  40. package/dist/senses/cli.js +89 -8
  41. package/dist/senses/inner-dialog.js +15 -0
  42. package/dist/senses/session-lock.js +119 -0
  43. package/dist/senses/teams.js +1 -0
  44. package/package.json +4 -3
  45. package/subagents/README.md +3 -1
  46. package/subagents/work-merger.md +33 -2
@@ -1,8 +1,10 @@
1
1
  # Adoption Specialist Soul
2
2
 
3
- I help humans hatch new agent partners.
3
+ I help humans hatch new agent partners. I am one of thirteen specialists — each with a different personality and voice. The system picks one of us at random for each adoption session. Most humans only meet one of us, ever, so I make it count.
4
4
 
5
5
  ## Core contract
6
+ - I speak first. I warmly introduce myself, explain what we're doing, and guide the human from the very start.
7
+ - I am proactive. If the human doesn't know what an agent is or what to do, I explain and suggest — I never leave them lost.
6
8
  - I run a practical adoption interview to understand the human, their work, and constraints.
7
9
  - I can migrate useful context from existing agent systems when the human asks.
8
10
  - I explain where the hatchling bundle lives on disk and what was created.
@@ -44,6 +44,8 @@ exports.getTeamsConfig = getTeamsConfig;
44
44
  exports.getContextConfig = getContextConfig;
45
45
  exports.getOAuthConfig = getOAuthConfig;
46
46
  exports.getTeamsChannelConfig = getTeamsChannelConfig;
47
+ exports.getBlueBubblesConfig = getBlueBubblesConfig;
48
+ exports.getBlueBubblesChannelConfig = getBlueBubblesChannelConfig;
47
49
  exports.getIntegrationsConfig = getIntegrationsConfig;
48
50
  exports.getOpenAIEmbeddingsApiKey = getOpenAIEmbeddingsApiKey;
49
51
  exports.getLogsDir = getLogsDir;
@@ -92,6 +94,16 @@ const DEFAULT_SECRETS_TEMPLATE = {
92
94
  skipConfirmation: true,
93
95
  port: 3978,
94
96
  },
97
+ bluebubbles: {
98
+ serverUrl: "",
99
+ password: "",
100
+ accountId: "default",
101
+ },
102
+ bluebubblesChannel: {
103
+ port: 18790,
104
+ webhookPath: "/bluebubbles-webhook",
105
+ requestTimeoutMs: 30000,
106
+ },
95
107
  integrations: {
96
108
  perplexityApiKey: "",
97
109
  openaiEmbeddingsApiKey: "",
@@ -109,6 +121,8 @@ function defaultRuntimeConfig() {
109
121
  oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
110
122
  context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
111
123
  teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
124
+ bluebubbles: { ...DEFAULT_SECRETS_TEMPLATE.bluebubbles },
125
+ bluebubblesChannel: { ...DEFAULT_SECRETS_TEMPLATE.bluebubblesChannel },
112
126
  integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
113
127
  };
114
128
  }
@@ -273,6 +287,26 @@ function getTeamsChannelConfig() {
273
287
  const { skipConfirmation, flushIntervalMs, port } = config.teamsChannel;
274
288
  return { skipConfirmation, flushIntervalMs, port };
275
289
  }
290
+ function getBlueBubblesConfig() {
291
+ const config = loadConfig();
292
+ const { serverUrl, password, accountId } = config.bluebubbles;
293
+ if (!serverUrl.trim()) {
294
+ throw new Error("bluebubbles.serverUrl is required in secrets.json to run the BlueBubbles sense.");
295
+ }
296
+ if (!password.trim()) {
297
+ throw new Error("bluebubbles.password is required in secrets.json to run the BlueBubbles sense.");
298
+ }
299
+ return {
300
+ serverUrl: serverUrl.trim(),
301
+ password: password.trim(),
302
+ accountId: accountId.trim() || "default",
303
+ };
304
+ }
305
+ function getBlueBubblesChannelConfig() {
306
+ const config = loadConfig();
307
+ const { port, webhookPath, requestTimeoutMs } = config.bluebubblesChannel;
308
+ return { port, webhookPath, requestTimeoutMs };
309
+ }
276
310
  function getIntegrationsConfig() {
277
311
  const config = loadConfig();
278
312
  return { ...config.integrations };
@@ -2,11 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.hasToolIntent = exports.buildSystem = exports.toResponsesTools = exports.toResponsesInput = exports.streamResponsesApi = exports.streamChatCompletion = exports.getToolsForChannel = exports.summarizeArgs = exports.execTool = exports.tools = void 0;
4
4
  exports.createProviderRegistry = createProviderRegistry;
5
+ exports.resetProviderRuntime = resetProviderRuntime;
5
6
  exports.getModel = getModel;
6
7
  exports.getProvider = getProvider;
8
+ exports.createSummarize = createSummarize;
7
9
  exports.getProviderDisplayLabel = getProviderDisplayLabel;
8
10
  exports.stripLastToolCalls = stripLastToolCalls;
9
11
  exports.isTransientError = isTransientError;
12
+ exports.classifyTransientError = classifyTransientError;
10
13
  exports.runAgent = runAgent;
11
14
  const config_1 = require("./config");
12
15
  const identity_1 = require("./identity");
@@ -71,12 +74,35 @@ function getProviderRuntime() {
71
74
  }
72
75
  return _providerRuntime;
73
76
  }
77
+ /**
78
+ * Clear the cached provider runtime so the next call to getProviderRuntime()
79
+ * re-creates it from current config. Used by the adoption specialist to
80
+ * switch provider context without restarting the process.
81
+ */
82
+ function resetProviderRuntime() {
83
+ _providerRuntime = null;
84
+ }
74
85
  function getModel() {
75
86
  return getProviderRuntime().model;
76
87
  }
77
88
  function getProvider() {
78
89
  return getProviderRuntime().id;
79
90
  }
91
+ function createSummarize() {
92
+ return async (transcript, instruction) => {
93
+ const runtime = getProviderRuntime();
94
+ const client = runtime.client;
95
+ const response = await client.chat.completions.create({
96
+ model: runtime.model,
97
+ messages: [
98
+ { role: "system", content: instruction },
99
+ { role: "user", content: transcript },
100
+ ],
101
+ max_tokens: 500,
102
+ });
103
+ return response.choices?.[0]?.message?.content ?? transcript;
104
+ };
105
+ }
80
106
  function getProviderDisplayLabel() {
81
107
  const model = getModel();
82
108
  const providerLabelBuilders = {
@@ -175,6 +201,18 @@ function isTransientError(err) {
175
201
  return true;
176
202
  return false;
177
203
  }
204
+ function classifyTransientError(err) {
205
+ if (!(err instanceof Error))
206
+ return "unknown error";
207
+ const status = err.status;
208
+ if (status === 429)
209
+ return "rate limited";
210
+ if (status === 401 || status === 403)
211
+ return "auth error";
212
+ if (status && status >= 500)
213
+ return "server error";
214
+ return "network error";
215
+ }
178
216
  const MAX_RETRIES = 3;
179
217
  const RETRY_BASE_MS = 2000;
180
218
  async function runAgent(messages, callbacks, channel, signal, options) {
@@ -436,11 +474,12 @@ async function runAgent(messages, callbacks, channel, signal, options) {
436
474
  callbacks.onError(new Error("context trimmed, retrying..."), "transient");
437
475
  continue;
438
476
  }
439
- // Transient network errors: retry with exponential backoff
477
+ // Transient errors: retry with exponential backoff
440
478
  if (isTransientError(e) && retryCount < MAX_RETRIES) {
441
479
  retryCount++;
442
480
  const delay = RETRY_BASE_MS * Math.pow(2, retryCount - 1);
443
- callbacks.onError(new Error(`network error, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
481
+ const cause = classifyTransientError(e);
482
+ callbacks.onError(new Error(`${cause}, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
444
483
  // Wait with abort support
445
484
  const aborted = await new Promise((resolve) => {
446
485
  const timer = setTimeout(() => resolve(false), delay);
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ensureDaemonRunning = ensureDaemonRunning;
36
37
  exports.parseOuroCommand = parseOuroCommand;
38
+ exports.discoverExistingCredentials = discoverExistingCredentials;
37
39
  exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
38
40
  exports.runOuroCli = runOuroCli;
39
41
  const child_process_1 = require("child_process");
@@ -48,6 +50,22 @@ const types_1 = require("../../mind/friends/types");
48
50
  const ouro_uti_1 = require("./ouro-uti");
49
51
  const subagent_installer_1 = require("./subagent-installer");
50
52
  const hatch_flow_1 = require("./hatch-flow");
53
+ const specialist_orchestrator_1 = require("./specialist-orchestrator");
54
+ async function ensureDaemonRunning(deps) {
55
+ const alive = await deps.checkSocketAlive(deps.socketPath);
56
+ if (alive) {
57
+ return {
58
+ alreadyRunning: true,
59
+ message: `daemon already running (${deps.socketPath})`,
60
+ };
61
+ }
62
+ deps.cleanupStaleSocket(deps.socketPath);
63
+ const started = await deps.startDaemonProcess(deps.socketPath);
64
+ return {
65
+ alreadyRunning: false,
66
+ message: `daemon started (pid ${started.pid ?? "unknown"})`,
67
+ };
68
+ }
51
69
  function usage() {
52
70
  return [
53
71
  "Usage:",
@@ -441,6 +459,162 @@ async function defaultLinkFriendIdentity(command) {
441
459
  });
442
460
  return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
443
461
  }
462
+ function discoverExistingCredentials(secretsRoot) {
463
+ const found = [];
464
+ let entries;
465
+ try {
466
+ entries = fs.readdirSync(secretsRoot, { withFileTypes: true });
467
+ }
468
+ catch {
469
+ return found;
470
+ }
471
+ for (const entry of entries) {
472
+ if (!entry.isDirectory())
473
+ continue;
474
+ const secretsPath = path.join(secretsRoot, entry.name, "secrets.json");
475
+ let raw;
476
+ try {
477
+ raw = fs.readFileSync(secretsPath, "utf-8");
478
+ }
479
+ catch {
480
+ continue;
481
+ }
482
+ let parsed;
483
+ try {
484
+ parsed = JSON.parse(raw);
485
+ }
486
+ catch {
487
+ continue;
488
+ }
489
+ if (!parsed.providers)
490
+ continue;
491
+ for (const [provName, provConfig] of Object.entries(parsed.providers)) {
492
+ if (provName === "anthropic" && provConfig.setupToken) {
493
+ found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken } });
494
+ }
495
+ else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
496
+ found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken } });
497
+ }
498
+ else if (provName === "minimax" && provConfig.apiKey) {
499
+ found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey } });
500
+ }
501
+ else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
502
+ found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment } });
503
+ }
504
+ }
505
+ }
506
+ // Deduplicate by provider+credential value (keep first seen)
507
+ const seen = new Set();
508
+ return found.filter((cred) => {
509
+ const key = `${cred.provider}:${JSON.stringify(cred.credentials)}`;
510
+ if (seen.has(key))
511
+ return false;
512
+ seen.add(key);
513
+ return true;
514
+ });
515
+ }
516
+ /* v8 ignore next 79 -- integration: interactive terminal specialist session @preserve */
517
+ async function defaultRunAdoptionSpecialist() {
518
+ const readline = await Promise.resolve().then(() => __importStar(require("readline/promises")));
519
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
520
+ const prompt = async (q) => {
521
+ const answer = await rl.question(q);
522
+ return answer.trim();
523
+ };
524
+ try {
525
+ const secretsRoot = path.join(os.homedir(), ".agentsecrets");
526
+ const discovered = discoverExistingCredentials(secretsRoot);
527
+ let providerRaw;
528
+ let credentials = {};
529
+ if (discovered.length > 0) {
530
+ process.stdout.write("\n🐍 welcome to ouro! let's hatch your first agent.\n");
531
+ process.stdout.write("i found existing API credentials:\n\n");
532
+ const unique = [...new Map(discovered.map((d) => [`${d.provider}`, d])).values()];
533
+ for (let i = 0; i < unique.length; i++) {
534
+ process.stdout.write(` ${i + 1}. ${unique[i].provider} (from ${unique[i].agentName})\n`);
535
+ }
536
+ process.stdout.write("\n");
537
+ const choice = await prompt("use one of these? enter number, or 'new' for a different key: ");
538
+ const idx = parseInt(choice, 10) - 1;
539
+ if (idx >= 0 && idx < unique.length) {
540
+ providerRaw = unique[idx].provider;
541
+ credentials = unique[idx].credentials;
542
+ }
543
+ else {
544
+ const pRaw = await prompt("provider (anthropic/azure/minimax/openai-codex): ");
545
+ if (!isAgentProvider(pRaw)) {
546
+ process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
547
+ rl.close();
548
+ return null;
549
+ }
550
+ providerRaw = pRaw;
551
+ if (providerRaw === "anthropic")
552
+ credentials.setupToken = await prompt("API key: ");
553
+ if (providerRaw === "openai-codex")
554
+ credentials.oauthAccessToken = await prompt("OAuth token: ");
555
+ if (providerRaw === "minimax")
556
+ credentials.apiKey = await prompt("API key: ");
557
+ if (providerRaw === "azure") {
558
+ credentials.apiKey = await prompt("API key: ");
559
+ credentials.endpoint = await prompt("endpoint: ");
560
+ credentials.deployment = await prompt("deployment: ");
561
+ }
562
+ }
563
+ }
564
+ else {
565
+ process.stdout.write("\n🐍 welcome to ouro! let's hatch your first agent.\n");
566
+ process.stdout.write("i need an API key to power our conversation.\n\n");
567
+ const pRaw = await prompt("provider (anthropic/azure/minimax/openai-codex): ");
568
+ if (!isAgentProvider(pRaw)) {
569
+ process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
570
+ rl.close();
571
+ return null;
572
+ }
573
+ providerRaw = pRaw;
574
+ if (providerRaw === "anthropic")
575
+ credentials.setupToken = await prompt("API key: ");
576
+ if (providerRaw === "openai-codex")
577
+ credentials.oauthAccessToken = await prompt("OAuth token: ");
578
+ if (providerRaw === "minimax")
579
+ credentials.apiKey = await prompt("API key: ");
580
+ if (providerRaw === "azure") {
581
+ credentials.apiKey = await prompt("API key: ");
582
+ credentials.endpoint = await prompt("endpoint: ");
583
+ credentials.deployment = await prompt("deployment: ");
584
+ }
585
+ }
586
+ rl.close();
587
+ process.stdout.write("\n");
588
+ // Locate the bundled AdoptionSpecialist.ouro shipped with the npm package
589
+ const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
590
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
591
+ return await (0, specialist_orchestrator_1.runAdoptionSpecialist)({
592
+ bundleSourceDir,
593
+ bundlesRoot,
594
+ secretsRoot,
595
+ provider: providerRaw,
596
+ credentials,
597
+ humanName: os.userInfo().username,
598
+ createReadline: () => {
599
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
600
+ return { question: (q) => rl2.question(q), close: () => rl2.close() };
601
+ },
602
+ callbacks: {
603
+ onModelStart: () => { },
604
+ onModelStreamStart: () => { },
605
+ onTextChunk: (text) => process.stdout.write(text),
606
+ onReasoningChunk: () => { },
607
+ onToolStart: () => { },
608
+ onToolEnd: () => { },
609
+ onError: (err) => process.stderr.write(`error: ${err.message}\n`),
610
+ },
611
+ });
612
+ }
613
+ catch {
614
+ rl.close();
615
+ return null;
616
+ }
617
+ }
444
618
  function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
445
619
  return {
446
620
  socketPath,
@@ -455,7 +629,13 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
455
629
  listDiscoveredAgents: defaultListDiscoveredAgents,
456
630
  runHatchFlow: hatch_flow_1.runHatchFlow,
457
631
  promptInput: defaultPromptInput,
632
+ runAdoptionSpecialist: defaultRunAdoptionSpecialist,
458
633
  registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
634
+ /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
635
+ startChat: async (agentName) => {
636
+ const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
637
+ await main(agentName);
638
+ },
459
639
  };
460
640
  }
461
641
  function toDaemonCommand(command) {
@@ -513,16 +693,74 @@ async function registerOuroBundleTypeNonBlocking(deps) {
513
693
  }
514
694
  }
515
695
  async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
516
- let command = parseOuroCommand(args);
696
+ if (args.includes("--help") || args.includes("-h")) {
697
+ const text = usage();
698
+ deps.writeStdout(text);
699
+ return text;
700
+ }
701
+ let command;
702
+ try {
703
+ command = parseOuroCommand(args);
704
+ }
705
+ catch (parseError) {
706
+ if (deps.startChat && deps.listDiscoveredAgents && args.length === 1) {
707
+ const discovered = await Promise.resolve(deps.listDiscoveredAgents());
708
+ if (discovered.includes(args[0])) {
709
+ await ensureDaemonRunning(deps);
710
+ await deps.startChat(args[0]);
711
+ return "";
712
+ }
713
+ }
714
+ throw parseError;
715
+ }
517
716
  if (args.length === 0) {
518
717
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : defaultListDiscoveredAgents());
519
- if (discovered.length === 0) {
718
+ if (discovered.length === 0 && deps.runAdoptionSpecialist) {
719
+ const hatchlingName = await deps.runAdoptionSpecialist();
720
+ if (!hatchlingName) {
721
+ return "";
722
+ }
723
+ try {
724
+ await deps.installSubagents();
725
+ }
726
+ catch (error) {
727
+ (0, runtime_1.emitNervesEvent)({
728
+ level: "warn",
729
+ component: "daemon",
730
+ event: "daemon.subagent_install_error",
731
+ message: "subagent auto-install failed",
732
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
733
+ });
734
+ }
735
+ await registerOuroBundleTypeNonBlocking(deps);
736
+ await ensureDaemonRunning(deps);
737
+ if (deps.startChat) {
738
+ await deps.startChat(hatchlingName);
739
+ }
740
+ return "";
741
+ }
742
+ else if (discovered.length === 0) {
520
743
  command = { kind: "hatch.start" };
521
744
  }
522
745
  else if (discovered.length === 1) {
746
+ if (deps.startChat) {
747
+ await ensureDaemonRunning(deps);
748
+ await deps.startChat(discovered[0]);
749
+ return "";
750
+ }
523
751
  command = { kind: "chat.connect", agent: discovered[0] };
524
752
  }
525
753
  else {
754
+ if (deps.startChat && deps.promptInput) {
755
+ const prompt = `who do you want to talk to?\n${discovered.map((a, i) => `${i + 1}. ${a}`).join("\n")}\n`;
756
+ const answer = await deps.promptInput(prompt);
757
+ const selected = discovered.includes(answer) ? answer : discovered[parseInt(answer, 10) - 1];
758
+ if (!selected)
759
+ throw new Error("Invalid selection");
760
+ await ensureDaemonRunning(deps);
761
+ await deps.startChat(selected);
762
+ return "";
763
+ }
526
764
  const message = `who do you want to talk to? ${discovered.join(", ")} (use: ouro chat <agent>)`;
527
765
  deps.writeStdout(message);
528
766
  return message;
@@ -554,17 +792,13 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
554
792
  });
555
793
  }
556
794
  await registerOuroBundleTypeNonBlocking(deps);
557
- const alive = await deps.checkSocketAlive(deps.socketPath);
558
- if (alive) {
559
- const message = `daemon already running (${deps.socketPath})`;
560
- deps.writeStdout(message);
561
- return message;
562
- }
563
- deps.cleanupStaleSocket(deps.socketPath);
564
- const started = await deps.startDaemonProcess(deps.socketPath);
565
- const message = `daemon started (pid ${started.pid ?? "unknown"})`;
566
- deps.writeStdout(message);
567
- return message;
795
+ const daemonResult = await ensureDaemonRunning(deps);
796
+ deps.writeStdout(daemonResult.message);
797
+ return daemonResult.message;
798
+ }
799
+ if (command.kind === "daemon.logs" && deps.tailLogs) {
800
+ deps.tailLogs();
801
+ return "";
568
802
  }
569
803
  if (command.kind === "friend.link") {
570
804
  const linker = deps.linkFriendIdentity ?? defaultLinkFriendIdentity;
@@ -573,6 +807,32 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
573
807
  return message;
574
808
  }
575
809
  if (command.kind === "hatch.start") {
810
+ // Route through adoption specialist when no explicit hatch args were provided
811
+ const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
812
+ if (deps.runAdoptionSpecialist && !hasExplicitHatchArgs) {
813
+ const hatchlingName = await deps.runAdoptionSpecialist();
814
+ if (!hatchlingName) {
815
+ return "";
816
+ }
817
+ try {
818
+ await deps.installSubagents();
819
+ }
820
+ catch (error) {
821
+ (0, runtime_1.emitNervesEvent)({
822
+ level: "warn",
823
+ component: "daemon",
824
+ event: "daemon.subagent_install_error",
825
+ message: "subagent auto-install failed",
826
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
827
+ });
828
+ }
829
+ await registerOuroBundleTypeNonBlocking(deps);
830
+ await ensureDaemonRunning(deps);
831
+ if (deps.startChat) {
832
+ await deps.startChat(hatchlingName);
833
+ }
834
+ return "";
835
+ }
576
836
  const hatchRunner = deps.runHatchFlow;
577
837
  if (!hatchRunner) {
578
838
  const response = await deps.sendCommand(deps.socketPath, { kind: "hatch.start" });
@@ -591,18 +851,16 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
591
851
  component: "daemon",
592
852
  event: "daemon.subagent_install_error",
593
853
  message: "subagent auto-install failed",
594
- meta: { error: error instanceof Error ? error.message : String(error) },
854
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
595
855
  });
596
856
  }
597
857
  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}`;
858
+ const daemonResult = await ensureDaemonRunning(deps);
859
+ if (deps.startChat) {
860
+ await deps.startChat(hatchInput.agentName);
861
+ return "";
862
+ }
863
+ const message = `hatched ${hatchInput.agentName} at ${result.bundleRoot} using specialist identity ${result.selectedIdentity}; ${daemonResult.message}`;
606
864
  deps.writeStdout(message);
607
865
  return message;
608
866
  }
@@ -233,6 +233,7 @@ class OuroDaemon {
233
233
  ok: true,
234
234
  summary: "logs: use `ouro logs` to tail daemon and agent output",
235
235
  message: "log streaming available via ouro logs",
236
+ data: { logDir: "~/.agentstate/daemon/logs" },
236
237
  };
237
238
  case "agent.start":
238
239
  await this.processManager.startAgent(command.agent);
@@ -263,6 +264,7 @@ class OuroDaemon {
263
264
  sessionId: command.sessionId,
264
265
  taskRef: command.taskRef,
265
266
  });
267
+ this.processManager.sendToAgent?.(command.to, { type: "message" });
266
268
  return { ok: true, message: `queued message ${receipt.id}`, data: receipt };
267
269
  }
268
270
  case "message.poll": {
@@ -288,6 +290,7 @@ class OuroDaemon {
288
290
  taskRef: command.taskId,
289
291
  });
290
292
  await this.scheduler.recordTaskRun?.(command.agent, command.taskId);
293
+ this.processManager.sendToAgent?.(command.agent, { type: "poke", taskId: command.taskId });
291
294
  return {
292
295
  ok: true,
293
296
  message: `queued poke ${receipt.id}`,
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.playHatchAnimation = playHatchAnimation;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ const EGG = "\uD83E\uDD5A";
6
+ const SNAKE = "\uD83D\uDC0D";
7
+ const DOTS = " . . . ";
8
+ function wait(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+ /**
12
+ * Play the hatch animation: egg -> dots -> snake + name.
13
+ * The writer function receives each chunk. Default writer is process.stderr.write.
14
+ */
15
+ async function playHatchAnimation(hatchlingName, writer) {
16
+ (0, runtime_1.emitNervesEvent)({
17
+ component: "daemon",
18
+ event: "daemon.hatch_animation_start",
19
+ message: "playing hatch animation",
20
+ meta: { hatchlingName },
21
+ });
22
+ const write = writer ?? ((text) => process.stderr.write(text));
23
+ write(`\n ${EGG}`);
24
+ await wait(400);
25
+ write(DOTS);
26
+ await wait(400);
27
+ write(`${SNAKE} \x1b[1m${hatchlingName}\x1b[0m\n\n`);
28
+ }
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.writeSecretsFile = writeSecretsFile;
36
37
  exports.runHatchFlow = runHatchFlow;
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
@@ -182,6 +183,7 @@ function writeFriendImprint(bundleRoot, humanName, now) {
182
183
  fs.mkdirSync(friendsDir, { recursive: true });
183
184
  const nowIso = now.toISOString();
184
185
  const id = `friend-${slugify(humanName)}`;
186
+ const localExternalId = `${os.userInfo().username}@${os.hostname()}`;
185
187
  const record = {
186
188
  id,
187
189
  name: humanName,
@@ -191,7 +193,7 @@ function writeFriendImprint(bundleRoot, humanName, now) {
191
193
  externalIds: [
192
194
  {
193
195
  provider: "local",
194
- externalId: slugify(humanName),
196
+ externalId: localExternalId,
195
197
  linkedAt: nowIso,
196
198
  },
197
199
  ],
@@ -42,7 +42,12 @@ const os = __importStar(require("os"));
42
42
  const path = __importStar(require("path"));
43
43
  const runtime_1 = require("../../nerves/runtime");
44
44
  function getSpecialistIdentitySourceDir() {
45
- return path.join(os.homedir(), "AgentBundles", "AdoptionSpecialist.ouro", "psyche", "identities");
45
+ // Prefer ~/AgentBundles/ if it exists (user may have customized identities)
46
+ const userSource = path.join(os.homedir(), "AgentBundles", "AdoptionSpecialist.ouro", "psyche", "identities");
47
+ if (fs.existsSync(userSource))
48
+ return userSource;
49
+ // Fall back to the bundled copy shipped with the npm package
50
+ return path.join(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro", "psyche", "identities");
46
51
  }
47
52
  function getRepoSpecialistIdentitiesDir() {
48
53
  return path.join(process.cwd(), "AdoptionSpecialist.ouro", "psyche", "identities");