@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.
@@ -15,7 +15,7 @@ const runtime_1 = require("../../nerves/runtime");
15
15
  * 7. Return { hatchedAgentName } -- name from hatch_agent if called
16
16
  */
17
17
  async function runSpecialistSession(deps) {
18
- const { providerRuntime, systemPrompt, tools, execTool, readline, callbacks, signal, kickoffMessage } = deps;
18
+ const { providerRuntime, systemPrompt, tools, execTool, readline, callbacks, signal, kickoffMessage, suppressInput, restoreInput, flushMarkdown, writePrompt, } = deps;
19
19
  (0, runtime_1.emitNervesEvent)({
20
20
  component: "daemon",
21
21
  event: "daemon.specialist_session_start",
@@ -28,6 +28,7 @@ async function runSpecialistSession(deps) {
28
28
  let hatchedAgentName = null;
29
29
  let done = false;
30
30
  let isFirstTurn = true;
31
+ let currentAbort = null;
31
32
  try {
32
33
  while (!done) {
33
34
  if (signal?.aborted)
@@ -39,16 +40,25 @@ async function runSpecialistSession(deps) {
39
40
  }
40
41
  else {
41
42
  // Get user input
42
- const userInput = await readline.question("> ");
43
- if (!userInput.trim())
43
+ const userInput = await readline.question(writePrompt ? "" : "> ");
44
+ if (!userInput.trim()) {
45
+ if (writePrompt)
46
+ writePrompt();
44
47
  continue;
48
+ }
45
49
  messages.push({ role: "user", content: userInput });
46
50
  }
47
51
  providerRuntime.resetTurnState(messages);
52
+ // Suppress input during model execution
53
+ currentAbort = new AbortController();
54
+ const mergedSignal = signal;
55
+ if (suppressInput) {
56
+ suppressInput(() => currentAbort.abort());
57
+ }
48
58
  // Inner loop: process tool calls until we get a final_answer or plain text
49
59
  let turnDone = false;
50
60
  while (!turnDone) {
51
- if (signal?.aborted) {
61
+ if (mergedSignal?.aborted || currentAbort.signal.aborted) {
52
62
  done = true;
53
63
  break;
54
64
  }
@@ -57,7 +67,7 @@ async function runSpecialistSession(deps) {
57
67
  messages,
58
68
  activeTools: tools,
59
69
  callbacks,
60
- signal,
70
+ signal: mergedSignal,
61
71
  });
62
72
  // Build assistant message
63
73
  const assistantMsg = {
@@ -73,7 +83,9 @@ async function runSpecialistSession(deps) {
73
83
  }));
74
84
  }
75
85
  if (!result.toolCalls.length) {
76
- // Plain text response -- push and re-prompt
86
+ // Plain text response -- flush markdown, push and re-prompt
87
+ if (flushMarkdown)
88
+ flushMarkdown();
77
89
  messages.push(assistantMsg);
78
90
  turnDone = true;
79
91
  continue;
@@ -96,6 +108,8 @@ async function runSpecialistSession(deps) {
96
108
  }
97
109
  if (answer != null) {
98
110
  callbacks.onTextChunk(answer);
111
+ if (flushMarkdown)
112
+ flushMarkdown();
99
113
  messages.push(assistantMsg);
100
114
  done = true;
101
115
  turnDone = true;
@@ -114,7 +128,7 @@ async function runSpecialistSession(deps) {
114
128
  // Execute tool calls
115
129
  messages.push(assistantMsg);
116
130
  for (const tc of result.toolCalls) {
117
- if (signal?.aborted)
131
+ if (mergedSignal?.aborted)
118
132
  break;
119
133
  let args = {};
120
134
  try {
@@ -141,9 +155,22 @@ async function runSpecialistSession(deps) {
141
155
  }
142
156
  // After processing tool calls, continue inner loop for tool result processing
143
157
  }
158
+ // Restore input and show prompt for next turn
159
+ if (flushMarkdown)
160
+ flushMarkdown();
161
+ if (restoreInput)
162
+ restoreInput();
163
+ currentAbort = null;
164
+ if (!done) {
165
+ process.stdout.write("\n\n");
166
+ if (writePrompt)
167
+ writePrompt();
168
+ }
144
169
  }
145
170
  }
146
171
  finally {
172
+ if (restoreInput)
173
+ restoreInput();
147
174
  readline.close();
148
175
  }
149
176
  return { hatchedAgentName };
@@ -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.10",
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",