@ouro.bot/cli 0.1.0-alpha.11 → 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.
@@ -52,6 +52,139 @@ const ouro_path_installer_1 = require("./ouro-path-installer");
52
52
  const subagent_installer_1 = require("./subagent-installer");
53
53
  const hatch_flow_1 = require("./hatch-flow");
54
54
  const specialist_orchestrator_1 = require("./specialist-orchestrator");
55
+ function stringField(value) {
56
+ return typeof value === "string" ? value : null;
57
+ }
58
+ function numberField(value) {
59
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
60
+ }
61
+ function booleanField(value) {
62
+ return typeof value === "boolean" ? value : null;
63
+ }
64
+ function parseStatusPayload(data) {
65
+ if (!data || typeof data !== "object" || Array.isArray(data))
66
+ return null;
67
+ const raw = data;
68
+ const overview = raw.overview;
69
+ const senses = raw.senses;
70
+ const workers = raw.workers;
71
+ if (!overview || typeof overview !== "object" || Array.isArray(overview))
72
+ return null;
73
+ if (!Array.isArray(senses) || !Array.isArray(workers))
74
+ return null;
75
+ const parsedOverview = {
76
+ daemon: stringField(overview.daemon) ?? "unknown",
77
+ health: stringField(overview.health) ?? "unknown",
78
+ socketPath: stringField(overview.socketPath) ?? "unknown",
79
+ workerCount: numberField(overview.workerCount) ?? 0,
80
+ senseCount: numberField(overview.senseCount) ?? 0,
81
+ };
82
+ const parsedSenses = senses.map((entry) => {
83
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
84
+ return null;
85
+ const row = entry;
86
+ const agent = stringField(row.agent);
87
+ const sense = stringField(row.sense);
88
+ const status = stringField(row.status);
89
+ const detail = stringField(row.detail);
90
+ const enabled = booleanField(row.enabled);
91
+ if (!agent || !sense || !status || detail === null || enabled === null)
92
+ return null;
93
+ return {
94
+ agent,
95
+ sense,
96
+ label: stringField(row.label) ?? undefined,
97
+ enabled,
98
+ status,
99
+ detail,
100
+ };
101
+ });
102
+ const parsedWorkers = workers.map((entry) => {
103
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
104
+ return null;
105
+ const row = entry;
106
+ const agent = stringField(row.agent);
107
+ const worker = stringField(row.worker);
108
+ const status = stringField(row.status);
109
+ const restartCount = numberField(row.restartCount);
110
+ const hasPid = Object.prototype.hasOwnProperty.call(row, "pid");
111
+ const pid = row.pid === null ? null : numberField(row.pid);
112
+ const pidInvalid = !hasPid || (row.pid !== null && pid === null);
113
+ if (!agent || !worker || !status || restartCount === null || pidInvalid)
114
+ return null;
115
+ return {
116
+ agent,
117
+ worker,
118
+ status,
119
+ pid,
120
+ restartCount,
121
+ };
122
+ });
123
+ if (parsedSenses.some((row) => row === null) || parsedWorkers.some((row) => row === null))
124
+ return null;
125
+ return {
126
+ overview: parsedOverview,
127
+ senses: parsedSenses,
128
+ workers: parsedWorkers,
129
+ };
130
+ }
131
+ function humanizeSenseName(sense, label) {
132
+ if (label)
133
+ return label;
134
+ if (sense === "cli")
135
+ return "CLI";
136
+ if (sense === "bluebubbles")
137
+ return "BlueBubbles";
138
+ if (sense === "teams")
139
+ return "Teams";
140
+ return sense;
141
+ }
142
+ function formatTable(headers, rows) {
143
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
144
+ const renderRow = (row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
145
+ const divider = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
146
+ return [
147
+ renderRow(headers),
148
+ divider,
149
+ ...rows.map(renderRow),
150
+ ].join("\n");
151
+ }
152
+ function formatDaemonStatusOutput(response, fallback) {
153
+ const payload = parseStatusPayload(response.data);
154
+ if (!payload)
155
+ return fallback;
156
+ const overviewRows = [
157
+ ["Daemon", payload.overview.daemon],
158
+ ["Socket", payload.overview.socketPath],
159
+ ["Workers", String(payload.overview.workerCount)],
160
+ ["Senses", String(payload.overview.senseCount)],
161
+ ["Health", payload.overview.health],
162
+ ];
163
+ const senseRows = payload.senses.map((row) => [
164
+ row.agent,
165
+ humanizeSenseName(row.sense, row.label),
166
+ row.enabled ? "ON" : "OFF",
167
+ row.status,
168
+ row.detail,
169
+ ]);
170
+ const workerRows = payload.workers.map((row) => [
171
+ row.agent,
172
+ row.worker,
173
+ row.status,
174
+ row.pid === null ? "n/a" : String(row.pid),
175
+ String(row.restartCount),
176
+ ]);
177
+ return [
178
+ "Overview",
179
+ formatTable(["Item", "Value"], overviewRows),
180
+ "",
181
+ "Senses",
182
+ formatTable(["Agent", "Sense", "Enabled", "State", "Detail"], senseRows),
183
+ "",
184
+ "Workers",
185
+ formatTable(["Agent", "Worker", "State", "PID", "Restarts"], workerRows),
186
+ ].join("\n");
187
+ }
55
188
  async function ensureDaemonRunning(deps) {
56
189
  const alive = await deps.checkSocketAlive(deps.socketPath);
57
190
  if (alive) {
@@ -867,7 +1000,10 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
867
1000
  }
868
1001
  throw error;
869
1002
  }
870
- const message = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
1003
+ const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
1004
+ const message = command.kind === "daemon.status"
1005
+ ? formatDaemonStatusOutput(response, fallbackMessage)
1006
+ : fallbackMessage;
871
1007
  deps.writeStdout(message);
872
1008
  return message;
873
1009
  }
@@ -8,6 +8,7 @@ const message_router_1 = require("./message-router");
8
8
  const health_monitor_1 = require("./health-monitor");
9
9
  const task_scheduler_1 = require("./task-scheduler");
10
10
  const runtime_logging_1 = require("./runtime-logging");
11
+ const sense_manager_1 = require("./sense-manager");
11
12
  function parseSocketPath(argv) {
12
13
  const socketIndex = argv.indexOf("--socket");
13
14
  if (socketIndex >= 0) {
@@ -25,16 +26,22 @@ const socketPath = parseSocketPath(process.argv);
25
26
  message: "starting daemon entrypoint",
26
27
  meta: { socketPath },
27
28
  });
29
+ const managedAgents = ["ouroboros", "slugger"];
28
30
  const processManager = new process_manager_1.DaemonProcessManager({
29
- agents: [
30
- { name: "ouroboros", entry: "heart/agent-entry.js", channel: "cli", autoStart: true },
31
- { name: "slugger", entry: "heart/agent-entry.js", channel: "cli", autoStart: true },
32
- ],
31
+ agents: managedAgents.map((agent) => ({
32
+ name: agent,
33
+ entry: "heart/agent-entry.js",
34
+ channel: "inner-dialog",
35
+ autoStart: true,
36
+ })),
33
37
  });
34
38
  const scheduler = new task_scheduler_1.TaskDrivenScheduler({
35
- agents: ["ouroboros", "slugger"],
39
+ agents: [...managedAgents],
36
40
  });
37
41
  const router = new message_router_1.FileMessageRouter();
42
+ const senseManager = new sense_manager_1.DaemonSenseManager({
43
+ agents: [...managedAgents],
44
+ });
38
45
  const healthMonitor = new health_monitor_1.HealthMonitor({
39
46
  processManager,
40
47
  scheduler,
@@ -51,6 +58,7 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
51
58
  const daemon = new daemon_1.OuroDaemon({
52
59
  socketPath,
53
60
  processManager,
61
+ senseManager,
54
62
  scheduler,
55
63
  healthMonitor,
56
64
  router,
@@ -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": {
@@ -39,7 +39,7 @@ const os = __importStar(require("os"));
39
39
  const path = __importStar(require("path"));
40
40
  const runtime_1 = require("../../nerves/runtime");
41
41
  const WRAPPER_SCRIPT = `#!/bin/sh
42
- exec npx --yes @ouro.bot/cli "$@"
42
+ exec npx --yes @ouro.bot/cli@latest "$@"
43
43
  `;
44
44
  function detectShellProfile(homeDir, shell) {
45
45
  if (!shell)
@@ -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;
@@ -55,9 +55,18 @@ function listSubagentSources(subagentsDir) {
55
55
  .map((name) => path.join(subagentsDir, name))
56
56
  .sort((a, b) => a.localeCompare(b));
57
57
  }
58
+ function pathExists(target) {
59
+ try {
60
+ fs.lstatSync(target);
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
58
67
  function ensureSymlink(source, target) {
59
68
  fs.mkdirSync(path.dirname(target), { recursive: true });
60
- if (fs.existsSync(target)) {
69
+ if (pathExists(target)) {
61
70
  const stats = fs.lstatSync(target);
62
71
  if (stats.isSymbolicLink()) {
63
72
  const linkedPath = fs.readlinkSync(target);
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
36
+ exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
37
37
  exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
38
38
  exports.getAgentName = getAgentName;
39
39
  exports.getRepoRoot = getRepoRoot;
@@ -57,12 +57,73 @@ exports.DEFAULT_AGENT_PHRASES = {
57
57
  tool: ["running tool"],
58
58
  followup: ["processing"],
59
59
  };
60
+ exports.DEFAULT_AGENT_SENSES = {
61
+ cli: { enabled: true },
62
+ teams: { enabled: false },
63
+ bluebubbles: { enabled: false },
64
+ };
65
+ function normalizeSenses(value, configFile) {
66
+ const defaults = {
67
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
68
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
69
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
70
+ };
71
+ if (value === undefined) {
72
+ return defaults;
73
+ }
74
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
75
+ (0, runtime_1.emitNervesEvent)({
76
+ level: "error",
77
+ event: "config_identity.error",
78
+ component: "config/identity",
79
+ message: "agent config has invalid senses block",
80
+ meta: { path: configFile },
81
+ });
82
+ throw new Error(`agent.json at ${configFile} must include senses as an object when present.`);
83
+ }
84
+ const raw = value;
85
+ const senseNames = ["cli", "teams", "bluebubbles"];
86
+ for (const senseName of senseNames) {
87
+ const rawSense = raw[senseName];
88
+ if (rawSense === undefined) {
89
+ continue;
90
+ }
91
+ if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
92
+ (0, runtime_1.emitNervesEvent)({
93
+ level: "error",
94
+ event: "config_identity.error",
95
+ component: "config/identity",
96
+ message: "agent config has invalid sense config",
97
+ meta: { path: configFile, sense: senseName },
98
+ });
99
+ throw new Error(`agent.json at ${configFile} has invalid senses.${senseName} config.`);
100
+ }
101
+ const enabled = rawSense.enabled;
102
+ if (typeof enabled !== "boolean") {
103
+ (0, runtime_1.emitNervesEvent)({
104
+ level: "error",
105
+ event: "config_identity.error",
106
+ component: "config/identity",
107
+ message: "agent config has invalid sense enabled flag",
108
+ meta: { path: configFile, sense: senseName, enabled: enabled ?? null },
109
+ });
110
+ throw new Error(`agent.json at ${configFile} must include senses.${senseName}.enabled as boolean.`);
111
+ }
112
+ defaults[senseName] = { enabled };
113
+ }
114
+ return defaults;
115
+ }
60
116
  function buildDefaultAgentTemplate(_agentName) {
61
117
  return {
62
118
  version: 1,
63
119
  enabled: true,
64
120
  provider: "anthropic",
65
121
  context: { ...exports.DEFAULT_AGENT_CONTEXT },
122
+ senses: {
123
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
124
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
125
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
126
+ },
66
127
  phrases: {
67
128
  thinking: [...exports.DEFAULT_AGENT_PHRASES.thinking],
68
129
  tool: [...exports.DEFAULT_AGENT_PHRASES.tool],
@@ -257,6 +318,7 @@ function loadAgentConfig() {
257
318
  provider: rawProvider,
258
319
  context: parsed.context,
259
320
  logging: parsed.logging,
321
+ senses: normalizeSenses(parsed.senses, configFile),
260
322
  phrases: parsed.phrases,
261
323
  };
262
324
  (0, runtime_1.emitNervesEvent)({
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSenseInventory = getSenseInventory;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ const identity_1 = require("./identity");
6
+ const SENSES = [
7
+ { sense: "cli", label: "CLI", daemonManaged: false },
8
+ { sense: "teams", label: "Teams", daemonManaged: true },
9
+ { sense: "bluebubbles", label: "BlueBubbles", daemonManaged: true },
10
+ ];
11
+ function configuredSenses(senses) {
12
+ return senses ?? {
13
+ cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
14
+ teams: { ...identity_1.DEFAULT_AGENT_SENSES.teams },
15
+ bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
16
+ };
17
+ }
18
+ function resolveStatus(enabled, daemonManaged, runtimeInfo) {
19
+ if (!enabled) {
20
+ return "disabled";
21
+ }
22
+ if (!daemonManaged) {
23
+ return "interactive";
24
+ }
25
+ if (runtimeInfo?.runtime === "error") {
26
+ return "error";
27
+ }
28
+ if (runtimeInfo?.runtime === "running") {
29
+ return "running";
30
+ }
31
+ if (runtimeInfo?.configured === false) {
32
+ return "needs_config";
33
+ }
34
+ return "ready";
35
+ }
36
+ function getSenseInventory(agent, runtime = {}) {
37
+ const senses = configuredSenses(agent.senses);
38
+ const inventory = SENSES.map(({ sense, label, daemonManaged }) => {
39
+ const enabled = senses[sense].enabled;
40
+ return {
41
+ sense,
42
+ label,
43
+ enabled,
44
+ daemonManaged,
45
+ status: resolveStatus(enabled, daemonManaged, runtime[sense]),
46
+ };
47
+ });
48
+ (0, runtime_1.emitNervesEvent)({
49
+ component: "channels",
50
+ event: "channel.sense_inventory_built",
51
+ message: "built sense inventory",
52
+ meta: {
53
+ senses: inventory.map((entry) => ({
54
+ sense: entry.sense,
55
+ enabled: entry.enabled,
56
+ status: entry.status,
57
+ })),
58
+ },
59
+ });
60
+ return inventory;
61
+ }
@@ -51,6 +51,7 @@ const first_impressions_1 = require("./first-impressions");
51
51
  const tasks_1 = require("../repertoire/tasks");
52
52
  // Lazy-loaded psyche text cache
53
53
  let _psycheCache = null;
54
+ let _senseStatusLinesCache = null;
54
55
  function loadPsycheFile(name) {
55
56
  try {
56
57
  const psycheDir = path.join((0, identity_1.getAgentRoot)(), "psyche");
@@ -74,6 +75,7 @@ function loadPsyche() {
74
75
  }
75
76
  function resetPsycheCache() {
76
77
  _psycheCache = null;
78
+ _senseStatusLinesCache = null;
77
79
  }
78
80
  const DEFAULT_ACTIVE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
79
81
  function resolveFriendName(friendId, friendsDir, agentName) {
@@ -194,6 +196,7 @@ function runtimeInfoSection(channel) {
194
196
  lines.push(`agent: ${agentName}`);
195
197
  lines.push(`cwd: ${process.cwd()}`);
196
198
  lines.push(`channel: ${channel}`);
199
+ lines.push(`current sense: ${channel}`);
197
200
  lines.push(`i can read and modify my own source code.`);
198
201
  if (channel === "cli") {
199
202
  lines.push("i introduce myself on boot with a fun random greeting.");
@@ -204,8 +207,73 @@ function runtimeInfoSection(channel) {
204
207
  else {
205
208
  lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
206
209
  }
210
+ lines.push("");
211
+ lines.push(...senseRuntimeGuidance(channel));
207
212
  return lines.join("\n");
208
213
  }
214
+ function hasTextField(record, key) {
215
+ return typeof record?.[key] === "string" && record[key].trim().length > 0;
216
+ }
217
+ function localSenseStatusLines() {
218
+ if (_senseStatusLinesCache) {
219
+ return [..._senseStatusLinesCache];
220
+ }
221
+ const config = (0, identity_1.loadAgentConfig)();
222
+ const senses = config.senses ?? {
223
+ cli: { enabled: true },
224
+ teams: { enabled: false },
225
+ bluebubbles: { enabled: false },
226
+ };
227
+ let payload = {};
228
+ try {
229
+ const raw = fs.readFileSync((0, identity_1.getAgentSecretsPath)(), "utf-8");
230
+ const parsed = JSON.parse(raw);
231
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
232
+ payload = parsed;
233
+ }
234
+ }
235
+ catch {
236
+ payload = {};
237
+ }
238
+ const teams = payload.teams;
239
+ const bluebubbles = payload.bluebubbles;
240
+ const configured = {
241
+ cli: true,
242
+ teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
243
+ bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
244
+ };
245
+ const rows = [
246
+ { label: "CLI", status: "interactive" },
247
+ {
248
+ label: "Teams",
249
+ status: !senses.teams.enabled ? "disabled" : configured.teams ? "ready" : "needs_config",
250
+ },
251
+ {
252
+ label: "BlueBubbles",
253
+ status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "needs_config",
254
+ },
255
+ ];
256
+ _senseStatusLinesCache = rows.map((row) => `- ${row.label}: ${row.status}`);
257
+ return [..._senseStatusLinesCache];
258
+ }
259
+ function senseRuntimeGuidance(channel) {
260
+ const lines = ["available senses:"];
261
+ lines.push(...localSenseStatusLines());
262
+ lines.push("sense states:");
263
+ lines.push("- interactive = available when opened by the user instead of kept running by the daemon");
264
+ lines.push("- disabled = turned off in agent.json");
265
+ lines.push("- needs_config = enabled but missing required secrets.json values");
266
+ lines.push("- ready = enabled and configured; `ouro up` should bring it online");
267
+ lines.push("- running = enabled and currently active");
268
+ lines.push("- error = enabled but unhealthy");
269
+ lines.push("If asked how to enable another sense, I explain the relevant agent.json senses entry and required secrets.json fields instead of guessing.");
270
+ lines.push("teams setup truth: enable `senses.teams.enabled`, then provide `teams.clientId`, `teams.clientSecret`, and `teams.tenantId` in secrets.json.");
271
+ lines.push("bluebubbles setup truth: enable `senses.bluebubbles.enabled`, then provide `bluebubbles.serverUrl` and `bluebubbles.password` in secrets.json.");
272
+ if (channel === "cli") {
273
+ lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
274
+ }
275
+ return lines;
276
+ }
209
277
  function providerSection() {
210
278
  return `## my provider\n${(0, core_1.getProviderDisplayLabel)()}`;
211
279
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "ouro": "dist/heart/daemon/ouro-entry.js",