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

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.
@@ -39,14 +39,28 @@ const net = __importStar(require("net"));
39
39
  const path = __importStar(require("path"));
40
40
  const identity_1 = require("../identity");
41
41
  const runtime_1 = require("../../nerves/runtime");
42
- function formatStatusSummary(snapshots) {
43
- if (snapshots.length === 0)
42
+ function buildWorkerRows(snapshots) {
43
+ return snapshots.map((snapshot) => ({
44
+ agent: snapshot.name,
45
+ worker: snapshot.channel,
46
+ status: snapshot.status,
47
+ pid: snapshot.pid,
48
+ restartCount: snapshot.restartCount,
49
+ startedAt: snapshot.startedAt,
50
+ }));
51
+ }
52
+ function formatStatusSummary(payload) {
53
+ if (payload.overview.workerCount === 0 && payload.overview.senseCount === 0) {
44
54
  return "no managed agents";
45
- return snapshots
46
- .map((snapshot) => {
47
- return `${snapshot.name}\t${snapshot.channel}\t${snapshot.status}\tpid=${snapshot.pid ?? "none"}\trestarts=${snapshot.restartCount}`;
48
- })
49
- .join("\n");
55
+ }
56
+ const rows = [
57
+ ...payload.workers.map((row) => `${row.agent}/${row.worker}:${row.status}`),
58
+ ...payload.senses
59
+ .filter((row) => row.enabled)
60
+ .map((row) => `${row.agent}/${row.sense}:${row.status}`),
61
+ ];
62
+ const detail = rows.length > 0 ? `\titems=${rows.join(",")}` : "";
63
+ return `daemon=${payload.overview.daemon}\tworkers=${payload.overview.workerCount}\tsenses=${payload.overview.senseCount}\thealth=${payload.overview.health}${detail}`;
50
64
  }
51
65
  function parseIncomingCommand(raw) {
52
66
  let parsed;
@@ -71,6 +85,7 @@ class OuroDaemon {
71
85
  scheduler;
72
86
  healthMonitor;
73
87
  router;
88
+ senseManager;
74
89
  bundlesRoot;
75
90
  server = null;
76
91
  constructor(options) {
@@ -79,6 +94,7 @@ class OuroDaemon {
79
94
  this.scheduler = options.scheduler;
80
95
  this.healthMonitor = options.healthMonitor;
81
96
  this.router = options.router;
97
+ this.senseManager = options.senseManager ?? null;
82
98
  this.bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
83
99
  }
84
100
  async start() {
@@ -91,6 +107,7 @@ class OuroDaemon {
91
107
  meta: { socketPath: this.socketPath },
92
108
  });
93
109
  await this.processManager.startAutoStartAgents();
110
+ await this.senseManager?.startAutoStartSenses();
94
111
  this.scheduler.start?.();
95
112
  await this.scheduler.reconcile?.();
96
113
  await this.drainPendingBundleMessages();
@@ -178,6 +195,7 @@ class OuroDaemon {
178
195
  });
179
196
  this.scheduler.stop?.();
180
197
  await this.processManager.stopAll();
198
+ await this.senseManager?.stopAll();
181
199
  if (this.server) {
182
200
  await new Promise((resolve) => {
183
201
  this.server?.close(() => resolve());
@@ -217,10 +235,23 @@ class OuroDaemon {
217
235
  return { ok: true, message: "daemon stopped" };
218
236
  case "daemon.status": {
219
237
  const snapshots = this.processManager.listAgentSnapshots();
238
+ const workers = buildWorkerRows(snapshots);
239
+ const senses = this.senseManager?.listSenseRows() ?? [];
240
+ const data = {
241
+ overview: {
242
+ daemon: "running",
243
+ health: workers.every((worker) => worker.status === "running") ? "ok" : "warn",
244
+ socketPath: this.socketPath,
245
+ workerCount: workers.length,
246
+ senseCount: senses.length,
247
+ },
248
+ workers,
249
+ senses,
250
+ };
220
251
  return {
221
252
  ok: true,
222
- summary: formatStatusSummary(snapshots),
223
- data: snapshots,
253
+ summary: formatStatusSummary(data),
254
+ data,
224
255
  };
225
256
  }
226
257
  case "daemon.health": {
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installOuroCommand = installOuroCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ const WRAPPER_SCRIPT = `#!/bin/sh
42
+ exec npx --yes @ouro.bot/cli@latest "$@"
43
+ `;
44
+ function detectShellProfile(homeDir, shell) {
45
+ if (!shell)
46
+ return null;
47
+ const base = path.basename(shell);
48
+ if (base === "zsh")
49
+ return path.join(homeDir, ".zshrc");
50
+ if (base === "bash") {
51
+ // macOS uses .bash_profile, Linux uses .bashrc
52
+ const profilePath = path.join(homeDir, ".bash_profile");
53
+ return profilePath;
54
+ }
55
+ if (base === "fish")
56
+ return path.join(homeDir, ".config", "fish", "config.fish");
57
+ return null;
58
+ }
59
+ function isBinDirInPath(binDir, envPath) {
60
+ return envPath.split(path.delimiter).some((p) => p === binDir);
61
+ }
62
+ function buildPathExportLine(binDir, shell) {
63
+ const base = shell ? path.basename(shell) : /* v8 ignore next -- unreachable: only called when detectShellProfile returns non-null, which requires shell @preserve */ "";
64
+ if (base === "fish") {
65
+ return `\n# Added by ouro\nset -gx PATH ${binDir} $PATH\n`;
66
+ }
67
+ return `\n# Added by ouro\nexport PATH="${binDir}:$PATH"\n`;
68
+ }
69
+ function installOuroCommand(deps = {}) {
70
+ /* v8 ignore start -- dep defaults: only used in real runtime, tests always inject @preserve */
71
+ const platform = deps.platform ?? process.platform;
72
+ const homeDir = deps.homeDir ?? os.homedir();
73
+ const existsSync = deps.existsSync ?? fs.existsSync;
74
+ const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
75
+ const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
76
+ const readFileSync = deps.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
77
+ const appendFileSync = deps.appendFileSync ?? fs.appendFileSync;
78
+ const chmodSync = deps.chmodSync ?? fs.chmodSync;
79
+ const envPath = deps.envPath ?? process.env.PATH ?? "";
80
+ const shell = deps.shell ?? process.env.SHELL;
81
+ /* v8 ignore stop */
82
+ if (platform === "win32") {
83
+ (0, runtime_1.emitNervesEvent)({
84
+ component: "daemon",
85
+ event: "daemon.ouro_path_install_skip",
86
+ message: "skipped ouro PATH install on Windows",
87
+ meta: { platform },
88
+ });
89
+ return { installed: false, scriptPath: null, pathReady: false, shellProfileUpdated: null, skippedReason: "windows" };
90
+ }
91
+ const binDir = path.join(homeDir, ".local", "bin");
92
+ const scriptPath = path.join(binDir, "ouro");
93
+ (0, runtime_1.emitNervesEvent)({
94
+ component: "daemon",
95
+ event: "daemon.ouro_path_install_start",
96
+ message: "installing ouro command to PATH",
97
+ meta: { scriptPath, binDir },
98
+ });
99
+ // If ouro already exists somewhere in PATH, skip
100
+ if (existsSync(scriptPath)) {
101
+ (0, runtime_1.emitNervesEvent)({
102
+ component: "daemon",
103
+ event: "daemon.ouro_path_install_skip",
104
+ message: "ouro command already installed",
105
+ meta: { scriptPath },
106
+ });
107
+ return { installed: false, scriptPath, pathReady: isBinDirInPath(binDir, envPath), shellProfileUpdated: null, skippedReason: "already-installed" };
108
+ }
109
+ try {
110
+ mkdirSync(binDir, { recursive: true });
111
+ writeFileSync(scriptPath, WRAPPER_SCRIPT, { mode: 0o755 });
112
+ chmodSync(scriptPath, 0o755);
113
+ }
114
+ catch (error) {
115
+ (0, runtime_1.emitNervesEvent)({
116
+ level: "warn",
117
+ component: "daemon",
118
+ event: "daemon.ouro_path_install_error",
119
+ message: "failed to install ouro command",
120
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
121
+ });
122
+ return { installed: false, scriptPath: null, pathReady: false, shellProfileUpdated: null, skippedReason: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error) };
123
+ }
124
+ // Check if ~/.local/bin is already in PATH
125
+ let shellProfileUpdated = null;
126
+ const pathReady = isBinDirInPath(binDir, envPath);
127
+ if (!pathReady) {
128
+ const profilePath = detectShellProfile(homeDir, shell);
129
+ if (profilePath) {
130
+ try {
131
+ let existing = "";
132
+ try {
133
+ existing = readFileSync(profilePath, "utf-8");
134
+ }
135
+ catch {
136
+ // Profile doesn't exist yet — that's fine, we'll create it
137
+ }
138
+ if (!existing.includes(binDir)) {
139
+ appendFileSync(profilePath, buildPathExportLine(binDir, shell));
140
+ shellProfileUpdated = profilePath;
141
+ }
142
+ }
143
+ catch (error) {
144
+ (0, runtime_1.emitNervesEvent)({
145
+ level: "warn",
146
+ component: "daemon",
147
+ event: "daemon.ouro_path_profile_error",
148
+ message: "failed to update shell profile for PATH",
149
+ meta: { profilePath, error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
150
+ });
151
+ }
152
+ }
153
+ }
154
+ (0, runtime_1.emitNervesEvent)({
155
+ component: "daemon",
156
+ event: "daemon.ouro_path_install_end",
157
+ message: "ouro command installed",
158
+ meta: { scriptPath, pathReady, shellProfileUpdated },
159
+ });
160
+ return { installed: true, scriptPath, pathReady, shellProfileUpdated };
161
+ }
@@ -96,7 +96,7 @@ class DaemonProcessManager {
96
96
  state.snapshot.status = "starting";
97
97
  const runCwd = (0, identity_1.getRepoRoot)();
98
98
  const entryScript = path.join((0, identity_1.getRepoRoot)(), "dist", state.config.entry);
99
- const args = [entryScript, "--agent", agent, ...(state.config.args ?? [])];
99
+ const args = [entryScript, "--agent", state.config.agentArg ?? agent, ...(state.config.args ?? [])];
100
100
  const child = this.spawnFn("node", args, {
101
101
  cwd: runCwd,
102
102
  env: state.config.env ? { ...process.env, ...state.config.env } : process.env,
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DaemonSenseManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ const identity_1 = require("../identity");
42
+ const sense_truth_1 = require("../sense-truth");
43
+ const process_manager_1 = require("./process-manager");
44
+ const DEFAULT_TEAMS_PORT = 3978;
45
+ const DEFAULT_BLUEBUBBLES_PORT = 18790;
46
+ const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
47
+ function defaultSenses() {
48
+ return {
49
+ cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
50
+ teams: { ...identity_1.DEFAULT_AGENT_SENSES.teams },
51
+ bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
52
+ };
53
+ }
54
+ function readAgentSenses(agentJsonPath) {
55
+ const defaults = defaultSenses();
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(fs.readFileSync(agentJsonPath, "utf-8"));
59
+ }
60
+ catch (error) {
61
+ (0, runtime_1.emitNervesEvent)({
62
+ level: "warn",
63
+ component: "channels",
64
+ event: "channel.daemon_sense_agent_config_fallback",
65
+ message: "using default senses because agent config could not be read",
66
+ meta: {
67
+ path: agentJsonPath,
68
+ reason: error instanceof Error ? error.message : String(error),
69
+ },
70
+ });
71
+ return defaults;
72
+ }
73
+ const rawSenses = parsed.senses;
74
+ if (!rawSenses || typeof rawSenses !== "object" || Array.isArray(rawSenses)) {
75
+ return defaults;
76
+ }
77
+ for (const sense of ["cli", "teams", "bluebubbles"]) {
78
+ const rawSense = rawSenses[sense];
79
+ if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
80
+ continue;
81
+ }
82
+ const enabled = rawSense.enabled;
83
+ if (typeof enabled === "boolean") {
84
+ defaults[sense] = { enabled };
85
+ }
86
+ }
87
+ return defaults;
88
+ }
89
+ function readSecretsPayload(secretsPath) {
90
+ try {
91
+ const raw = fs.readFileSync(secretsPath, "utf-8");
92
+ const parsed = JSON.parse(raw);
93
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
94
+ return { payload: {}, error: "invalid secrets.json object" };
95
+ }
96
+ return { payload: parsed, error: null };
97
+ }
98
+ catch (error) {
99
+ return {
100
+ payload: {},
101
+ error: error instanceof Error ? error.message : String(error),
102
+ };
103
+ }
104
+ }
105
+ function textField(record, key) {
106
+ const value = record?.[key];
107
+ return typeof value === "string" ? value.trim() : "";
108
+ }
109
+ function numberField(record, key, fallback) {
110
+ const value = record?.[key];
111
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
112
+ }
113
+ function senseFactsFromSecrets(agent, senses, secretsPath) {
114
+ const base = {
115
+ cli: { configured: true, detail: "local interactive terminal" },
116
+ teams: { configured: false, detail: "not enabled in agent.json" },
117
+ bluebubbles: { configured: false, detail: "not enabled in agent.json" },
118
+ };
119
+ const { payload, error } = readSecretsPayload(secretsPath);
120
+ const teams = payload.teams;
121
+ const teamsChannel = payload.teamsChannel;
122
+ const bluebubbles = payload.bluebubbles;
123
+ const bluebubblesChannel = payload.bluebubblesChannel;
124
+ if (senses.teams.enabled) {
125
+ const missing = [];
126
+ if (!textField(teams, "clientId"))
127
+ missing.push("teams.clientId");
128
+ if (!textField(teams, "clientSecret"))
129
+ missing.push("teams.clientSecret");
130
+ if (!textField(teams, "tenantId"))
131
+ missing.push("teams.tenantId");
132
+ base.teams = missing.length === 0
133
+ ? {
134
+ configured: true,
135
+ detail: `:${numberField(teamsChannel, "port", DEFAULT_TEAMS_PORT)}`,
136
+ }
137
+ : {
138
+ configured: false,
139
+ detail: error && !fs.existsSync(secretsPath)
140
+ ? `missing secrets.json (${agent})`
141
+ : `missing ${missing.join("/")}`,
142
+ };
143
+ }
144
+ if (senses.bluebubbles.enabled) {
145
+ const missing = [];
146
+ if (!textField(bluebubbles, "serverUrl"))
147
+ missing.push("bluebubbles.serverUrl");
148
+ if (!textField(bluebubbles, "password"))
149
+ missing.push("bluebubbles.password");
150
+ base.bluebubbles = missing.length === 0
151
+ ? {
152
+ configured: true,
153
+ detail: `:${numberField(bluebubblesChannel, "port", DEFAULT_BLUEBUBBLES_PORT)} ${textField(bluebubblesChannel, "webhookPath") || DEFAULT_BLUEBUBBLES_WEBHOOK_PATH}`,
154
+ }
155
+ : {
156
+ configured: false,
157
+ detail: error && !fs.existsSync(secretsPath)
158
+ ? `missing secrets.json (${agent})`
159
+ : `missing ${missing.join("/")}`,
160
+ };
161
+ }
162
+ return base;
163
+ }
164
+ function parseSenseSnapshotName(name) {
165
+ const parts = name.split(":");
166
+ if (parts.length !== 2)
167
+ return null;
168
+ const [agent, sense] = parts;
169
+ if (sense !== "teams" && sense !== "bluebubbles")
170
+ return null;
171
+ return { agent, sense };
172
+ }
173
+ function runtimeInfoFor(status) {
174
+ if (status === "running")
175
+ return { runtime: "running" };
176
+ return { runtime: "error" };
177
+ }
178
+ class DaemonSenseManager {
179
+ processManager;
180
+ contexts;
181
+ constructor(options) {
182
+ const bundlesRoot = options.bundlesRoot ?? path.join(os.homedir(), "AgentBundles");
183
+ const secretsRoot = options.secretsRoot ?? path.join(os.homedir(), ".agentsecrets");
184
+ this.contexts = new Map(options.agents.map((agent) => {
185
+ const senses = readAgentSenses(path.join(bundlesRoot, `${agent}.ouro`, "agent.json"));
186
+ const facts = senseFactsFromSecrets(agent, senses, path.join(secretsRoot, agent, "secrets.json"));
187
+ return [agent, { senses, facts }];
188
+ }));
189
+ const managedSenseAgents = [...this.contexts.entries()].flatMap(([agent, context]) => {
190
+ return ["teams", "bluebubbles"]
191
+ .filter((sense) => context.senses[sense].enabled && context.facts[sense].configured)
192
+ .map((sense) => ({
193
+ name: `${agent}:${sense}`,
194
+ agentArg: agent,
195
+ entry: sense === "teams" ? "senses/teams-entry.js" : "senses/bluebubbles-entry.js",
196
+ channel: sense,
197
+ autoStart: true,
198
+ }));
199
+ });
200
+ this.processManager = options.processManager ?? new process_manager_1.DaemonProcessManager({
201
+ agents: managedSenseAgents,
202
+ });
203
+ (0, runtime_1.emitNervesEvent)({
204
+ component: "channels",
205
+ event: "channel.daemon_sense_manager_init",
206
+ message: "initialized daemon sense manager",
207
+ meta: {
208
+ agents: options.agents,
209
+ managedSenseProcesses: managedSenseAgents.map((entry) => entry.name),
210
+ },
211
+ });
212
+ }
213
+ async startAutoStartSenses() {
214
+ await this.processManager.startAutoStartAgents();
215
+ }
216
+ async stopAll() {
217
+ await this.processManager.stopAll();
218
+ }
219
+ listSenseRows() {
220
+ const runtime = new Map();
221
+ for (const snapshot of this.processManager.listAgentSnapshots()) {
222
+ const parsed = parseSenseSnapshotName(snapshot.name);
223
+ if (!parsed)
224
+ continue;
225
+ const current = runtime.get(parsed.agent) ?? {};
226
+ current[parsed.sense] = runtimeInfoFor(snapshot.status);
227
+ runtime.set(parsed.agent, current);
228
+ }
229
+ const rows = [...this.contexts.entries()].flatMap(([agent, context]) => {
230
+ const runtimeInfo = {
231
+ cli: { configured: true },
232
+ teams: {
233
+ configured: context.facts.teams.configured,
234
+ ...(runtime.get(agent)?.teams ?? {}),
235
+ },
236
+ bluebubbles: {
237
+ configured: context.facts.bluebubbles.configured,
238
+ ...(runtime.get(agent)?.bluebubbles ?? {}),
239
+ },
240
+ };
241
+ const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
242
+ return inventory.map((entry) => ({
243
+ agent,
244
+ sense: entry.sense,
245
+ label: entry.label,
246
+ enabled: entry.enabled,
247
+ status: entry.status,
248
+ detail: entry.enabled ? context.facts[entry.sense].detail : "not enabled in agent.json",
249
+ }));
250
+ });
251
+ (0, runtime_1.emitNervesEvent)({
252
+ component: "channels",
253
+ event: "channel.daemon_sense_rows_built",
254
+ message: "built daemon sense status rows",
255
+ meta: {
256
+ rows: rows.map((row) => ({
257
+ agent: row.agent,
258
+ sense: row.sense,
259
+ status: row.status,
260
+ })),
261
+ },
262
+ });
263
+ return rows;
264
+ }
265
+ }
266
+ exports.DaemonSenseManager = DaemonSenseManager;
@@ -61,6 +61,25 @@ function listExistingBundles(bundlesRoot) {
61
61
  }
62
62
  return discovered.sort((a, b) => a.localeCompare(b));
63
63
  }
64
+ function loadIdentityPhrases(bundleSourceDir, identityFileName) {
65
+ const agentJsonPath = path.join(bundleSourceDir, "agent.json");
66
+ try {
67
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
68
+ const parsed = JSON.parse(raw);
69
+ const identityKey = identityFileName.replace(/\.md$/, "");
70
+ const identity = parsed.identityPhrases?.[identityKey];
71
+ if (identity?.thinking?.length && identity?.tool?.length && identity?.followup?.length) {
72
+ return identity;
73
+ }
74
+ if (parsed.phrases?.thinking?.length && parsed.phrases?.tool?.length && parsed.phrases?.followup?.length) {
75
+ return parsed.phrases;
76
+ }
77
+ }
78
+ catch {
79
+ // agent.json missing or malformed — fall through
80
+ }
81
+ return { ...identity_1.DEFAULT_AGENT_PHRASES };
82
+ }
64
83
  function pickRandomIdentity(identitiesDir, random) {
65
84
  const files = fs.readdirSync(identitiesDir).filter((f) => f.endsWith(".md"));
66
85
  if (files.length === 0) {
@@ -113,13 +132,14 @@ async function runAdoptionSpecialist(deps) {
113
132
  const existingBundles = listExistingBundles(bundlesRoot);
114
133
  // 4. Build system prompt
115
134
  const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles);
116
- // 5. Set up provider
135
+ // 5. Set up provider with identity-specific phrases
136
+ const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
117
137
  (0, identity_1.setAgentName)("AdoptionSpecialist");
118
138
  (0, identity_1.setAgentConfigOverride)({
119
139
  version: 1,
120
140
  enabled: true,
121
141
  provider,
122
- phrases: { thinking: ["thinking"], tool: ["checking"], followup: ["processing"] },
142
+ phrases,
123
143
  });
124
144
  (0, hatch_flow_1.writeSecretsFile)("AdoptionSpecialist", provider, credentials, secretsRoot);
125
145
  (0, config_1.resetConfigCache)();
@@ -133,6 +153,7 @@ async function runAdoptionSpecialist(deps) {
133
153
  // 6. Run session
134
154
  const tools = (0, specialist_tools_1.getSpecialistTools)();
135
155
  const readline = deps.createReadline();
156
+ const ctrl = readline.inputController;
136
157
  const result = await (0, specialist_session_1.runSpecialistSession)({
137
158
  providerRuntime,
138
159
  systemPrompt,
@@ -149,6 +170,10 @@ async function runAdoptionSpecialist(deps) {
149
170
  callbacks,
150
171
  signal,
151
172
  kickoffMessage: "hi, i just ran ouro for the first time",
173
+ suppressInput: ctrl ? (onInterrupt) => ctrl.suppress(onInterrupt) : undefined,
174
+ restoreInput: ctrl ? () => ctrl.restore() : undefined,
175
+ flushMarkdown: callbacks.flushMarkdown,
176
+ writePrompt: ctrl ? () => process.stdout.write("\x1b[36m> \x1b[0m") : undefined,
152
177
  });
153
178
  return result.hatchedAgentName;
154
179
  }
@@ -32,14 +32,19 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles) {
32
32
  "Most humans only go through adoption once, so this is likely the only time they'll meet me.",
33
33
  "I make this encounter count — warm, memorable, and uniquely mine.",
34
34
  "",
35
+ "## Voice rules",
36
+ "IMPORTANT: I keep every response to 1-3 short sentences. I sound like a friend texting, not a manual.",
37
+ "I NEVER use headers, bullet lists, numbered lists, or markdown formatting.",
38
+ "I ask ONE question at a time. I do not dump multiple questions or options.",
39
+ "I am warm but brief. Every word earns its place.",
40
+ "",
35
41
  "## Conversation flow",
36
42
  "The human just connected. I speak first — I greet them warmly and introduce myself in my own voice.",
37
43
  "I briefly mention that I'm one of several adoption specialists and they got me today.",
38
- "I ask their name and what they'd like their agent to help with.",
39
- "I'm proactive: I suggest ideas, ask focused questions, and guide them through the process.",
40
- "I don't wait for the human to figure things out — I explain what an agent is, what it can do, and what we're building together.",
41
- "If they seem unsure, I offer concrete examples and suggestions. I never leave them hanging.",
42
- "I keep the conversation natural, warm, and concise. I don't overwhelm with too many questions at once.",
44
+ "I ask their name.",
45
+ "Then I ask what they'd like their agent to help with one question at a time.",
46
+ "I'm proactive: I suggest ideas and guide them. If they seem unsure, I offer a concrete suggestion.",
47
+ "I don't wait for the human to figure things out I explain simply what an agent is if needed.",
43
48
  "When I have enough context, I suggest a name for the hatchling and confirm with the human.",
44
49
  "Then I call `hatch_agent` with the agent name and the human's name.",
45
50
  "",