@ouro.bot/cli 0.0.1-alpha.0 → 0.1.0-alpha.2

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 (119) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +20 -0
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +22 -0
  3. package/AdoptionSpecialist.ouro/psyche/identities/basilisk.md +31 -0
  4. package/AdoptionSpecialist.ouro/psyche/identities/jafar.md +31 -0
  5. package/AdoptionSpecialist.ouro/psyche/identities/jormungandr.md +31 -0
  6. package/AdoptionSpecialist.ouro/psyche/identities/kaa.md +31 -0
  7. package/AdoptionSpecialist.ouro/psyche/identities/medusa.md +31 -0
  8. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +31 -0
  9. package/AdoptionSpecialist.ouro/psyche/identities/nagini.md +31 -0
  10. package/AdoptionSpecialist.ouro/psyche/identities/ouroboros.md +31 -0
  11. package/AdoptionSpecialist.ouro/psyche/identities/python.md +31 -0
  12. package/AdoptionSpecialist.ouro/psyche/identities/quetzalcoatl.md +31 -0
  13. package/AdoptionSpecialist.ouro/psyche/identities/sir-hiss.md +31 -0
  14. package/AdoptionSpecialist.ouro/psyche/identities/the-serpent.md +31 -0
  15. package/AdoptionSpecialist.ouro/psyche/identities/the-snake.md +31 -0
  16. package/README.md +224 -6
  17. package/dist/heart/agent-entry.js +17 -0
  18. package/dist/heart/api-error.js +34 -0
  19. package/dist/heart/config.js +296 -0
  20. package/dist/heart/core.js +515 -0
  21. package/dist/heart/daemon/daemon-cli.js +675 -0
  22. package/dist/heart/daemon/daemon-entry.js +74 -0
  23. package/dist/heart/daemon/daemon.js +313 -0
  24. package/dist/heart/daemon/hatch-flow.js +285 -0
  25. package/dist/heart/daemon/hatch-specialist.js +107 -0
  26. package/dist/heart/daemon/health-monitor.js +79 -0
  27. package/dist/heart/daemon/log-tailer.js +146 -0
  28. package/dist/heart/daemon/message-router.js +98 -0
  29. package/dist/heart/daemon/os-cron.js +260 -0
  30. package/dist/heart/daemon/ouro-bot-entry.js +23 -0
  31. package/dist/heart/daemon/ouro-bot-wrapper.js +90 -0
  32. package/dist/heart/daemon/ouro-entry.js +23 -0
  33. package/dist/heart/daemon/ouro-uti.js +212 -0
  34. package/dist/heart/daemon/process-manager.js +237 -0
  35. package/dist/heart/daemon/runtime-logging.js +98 -0
  36. package/dist/heart/daemon/subagent-installer.js +125 -0
  37. package/dist/heart/daemon/task-scheduler.js +240 -0
  38. package/dist/heart/harness.js +26 -0
  39. package/dist/heart/identity.js +281 -0
  40. package/dist/heart/kicks.js +144 -0
  41. package/dist/heart/primitives.js +4 -0
  42. package/dist/heart/providers/anthropic.js +329 -0
  43. package/dist/heart/providers/azure.js +66 -0
  44. package/dist/heart/providers/minimax.js +53 -0
  45. package/dist/heart/providers/openai-codex.js +162 -0
  46. package/dist/heart/streaming.js +412 -0
  47. package/dist/heart/turn-coordinator.js +62 -0
  48. package/dist/inner-worker-entry.js +4 -0
  49. package/dist/mind/associative-recall.js +197 -0
  50. package/dist/mind/bundle-manifest.js +118 -0
  51. package/dist/mind/context.js +302 -0
  52. package/dist/mind/first-impressions.js +43 -0
  53. package/dist/mind/format.js +56 -0
  54. package/dist/mind/friends/channel.js +41 -0
  55. package/dist/mind/friends/resolver.js +84 -0
  56. package/dist/mind/friends/store-file.js +171 -0
  57. package/dist/mind/friends/store.js +4 -0
  58. package/dist/mind/friends/tokens.js +26 -0
  59. package/dist/mind/friends/types.js +21 -0
  60. package/dist/mind/memory.js +388 -0
  61. package/dist/mind/pending.js +93 -0
  62. package/dist/mind/phrases.js +43 -0
  63. package/dist/mind/prompt-refresh.js +20 -0
  64. package/dist/mind/prompt.js +352 -0
  65. package/dist/mind/token-estimate.js +119 -0
  66. package/dist/nerves/cli-logging.js +31 -0
  67. package/dist/nerves/coverage/audit-rules.js +81 -0
  68. package/dist/nerves/coverage/audit.js +200 -0
  69. package/dist/nerves/coverage/cli-main.js +5 -0
  70. package/dist/nerves/coverage/cli.js +51 -0
  71. package/dist/nerves/coverage/contract.js +23 -0
  72. package/dist/nerves/coverage/file-completeness.js +56 -0
  73. package/dist/nerves/coverage/run-artifacts.js +77 -0
  74. package/dist/nerves/coverage/source-scanner.js +34 -0
  75. package/dist/nerves/index.js +152 -0
  76. package/dist/nerves/runtime.js +38 -0
  77. package/dist/repertoire/ado-client.js +211 -0
  78. package/dist/repertoire/ado-context.js +73 -0
  79. package/dist/repertoire/ado-semantic.js +841 -0
  80. package/dist/repertoire/ado-templates.js +146 -0
  81. package/dist/repertoire/coding/index.js +36 -0
  82. package/dist/repertoire/coding/manager.js +489 -0
  83. package/dist/repertoire/coding/monitor.js +60 -0
  84. package/dist/repertoire/coding/reporter.js +45 -0
  85. package/dist/repertoire/coding/spawner.js +102 -0
  86. package/dist/repertoire/coding/tools.js +167 -0
  87. package/dist/repertoire/coding/types.js +2 -0
  88. package/dist/repertoire/data/ado-endpoints.json +122 -0
  89. package/dist/repertoire/data/graph-endpoints.json +212 -0
  90. package/dist/repertoire/github-client.js +64 -0
  91. package/dist/repertoire/graph-client.js +118 -0
  92. package/dist/repertoire/skills.js +156 -0
  93. package/dist/repertoire/tasks/board.js +122 -0
  94. package/dist/repertoire/tasks/index.js +210 -0
  95. package/dist/repertoire/tasks/lifecycle.js +80 -0
  96. package/dist/repertoire/tasks/middleware.js +65 -0
  97. package/dist/repertoire/tasks/parser.js +173 -0
  98. package/dist/repertoire/tasks/scanner.js +132 -0
  99. package/dist/repertoire/tasks/transitions.js +145 -0
  100. package/dist/repertoire/tasks/types.js +2 -0
  101. package/dist/repertoire/tools-base.js +714 -0
  102. package/dist/repertoire/tools-github.js +53 -0
  103. package/dist/repertoire/tools-teams.js +308 -0
  104. package/dist/repertoire/tools.js +199 -0
  105. package/dist/senses/cli-entry.js +15 -0
  106. package/dist/senses/cli.js +604 -0
  107. package/dist/senses/commands.js +98 -0
  108. package/dist/senses/inner-dialog-worker.js +61 -0
  109. package/dist/senses/inner-dialog.js +231 -0
  110. package/dist/senses/session-lock.js +119 -0
  111. package/dist/senses/teams-entry.js +15 -0
  112. package/dist/senses/teams.js +696 -0
  113. package/dist/senses/trust-gate.js +150 -0
  114. package/package.json +34 -11
  115. package/subagents/README.md +73 -0
  116. package/subagents/work-doer.md +233 -0
  117. package/subagents/work-merger.md +624 -0
  118. package/subagents/work-planner.md +373 -0
  119. package/bin/ouro.js +0 -6
@@ -0,0 +1,604 @@
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.MarkdownStreamer = exports.InputController = exports.Spinner = void 0;
37
+ exports.handleSigint = handleSigint;
38
+ exports.addHistory = addHistory;
39
+ exports.renderMarkdown = renderMarkdown;
40
+ exports.createCliCallbacks = createCliCallbacks;
41
+ exports.main = main;
42
+ const readline = __importStar(require("readline"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ const core_1 = require("../heart/core");
46
+ const prompt_1 = require("../mind/prompt");
47
+ const phrases_1 = require("../mind/phrases");
48
+ const format_1 = require("../mind/format");
49
+ const config_1 = require("../heart/config");
50
+ const context_1 = require("../mind/context");
51
+ const pending_1 = require("../mind/pending");
52
+ const prompt_refresh_1 = require("../mind/prompt-refresh");
53
+ const commands_1 = require("./commands");
54
+ const identity_1 = require("../heart/identity");
55
+ const nerves_1 = require("../nerves");
56
+ const store_file_1 = require("../mind/friends/store-file");
57
+ const resolver_1 = require("../mind/friends/resolver");
58
+ const tokens_1 = require("../mind/friends/tokens");
59
+ const cli_logging_1 = require("../nerves/cli-logging");
60
+ const runtime_1 = require("../nerves/runtime");
61
+ const trust_gate_1 = require("./trust-gate");
62
+ const session_lock_1 = require("./session-lock");
63
+ // spinner that only touches stderr, cleans up after itself
64
+ // exported for direct testability (stop-without-start branch)
65
+ class Spinner {
66
+ frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
67
+ i = 0;
68
+ iv = null;
69
+ piv = null;
70
+ msg = "";
71
+ phrases = null;
72
+ lastPhrase = "";
73
+ constructor(m = "working", phrases) {
74
+ this.msg = m;
75
+ if (phrases && phrases.length > 0)
76
+ this.phrases = phrases;
77
+ }
78
+ start() {
79
+ process.stderr.write("\r\x1b[K");
80
+ this.spin();
81
+ this.iv = setInterval(() => this.spin(), 80);
82
+ if (this.phrases) {
83
+ this.piv = setInterval(() => this.rotatePhrase(), 1500);
84
+ }
85
+ }
86
+ spin() {
87
+ process.stderr.write(`\r${this.frames[this.i]} ${this.msg}... `);
88
+ this.i = (this.i + 1) % this.frames.length;
89
+ }
90
+ rotatePhrase() {
91
+ const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
92
+ this.lastPhrase = next;
93
+ this.msg = next;
94
+ }
95
+ stop(ok) {
96
+ if (this.iv) {
97
+ clearInterval(this.iv);
98
+ this.iv = null;
99
+ }
100
+ if (this.piv) {
101
+ clearInterval(this.piv);
102
+ this.piv = null;
103
+ }
104
+ process.stderr.write("\r\x1b[K");
105
+ /* v8 ignore next -- ok parameter currently unused by callers @preserve */
106
+ if (ok)
107
+ process.stderr.write(`\x1b[32m\u2713\x1b[0m ${ok}\n`);
108
+ }
109
+ fail(msg) {
110
+ this.stop();
111
+ process.stderr.write(`\x1b[31m\u2717\x1b[0m ${msg}\n`);
112
+ }
113
+ }
114
+ exports.Spinner = Spinner;
115
+ // Input controller: pauses readline during model/tool execution.
116
+ // Does NOT touch raw mode — readline with terminal:true manages raw mode
117
+ // internally. Touching it causes ^C to be echoed by the terminal driver.
118
+ // During suppress, we consume stdin data ourselves to swallow stray
119
+ // keystrokes and catch Ctrl-C (0x03) for interrupt.
120
+ class InputController {
121
+ rl;
122
+ suppressed = false;
123
+ dataHandler = null;
124
+ onInterrupt = null;
125
+ constructor(rl) {
126
+ this.rl = rl;
127
+ }
128
+ suppress(onInterrupt) {
129
+ if (this.suppressed)
130
+ return;
131
+ this.suppressed = true;
132
+ this.onInterrupt = onInterrupt || null;
133
+ this.rl.pause();
134
+ // Consume stdin to swallow keystrokes; catch Ctrl-C (0x03)
135
+ this.dataHandler = (data) => {
136
+ if (data[0] === 0x03 && this.onInterrupt) {
137
+ this.onInterrupt();
138
+ }
139
+ // All other input is swallowed
140
+ };
141
+ process.stdin.on("data", this.dataHandler);
142
+ // rl.pause() paused stdin — resume it so our data handler receives keypresses
143
+ process.stdin.resume();
144
+ }
145
+ restore() {
146
+ if (!this.suppressed)
147
+ return;
148
+ this.suppressed = false;
149
+ if (this.dataHandler) {
150
+ process.stdin.removeListener("data", this.dataHandler);
151
+ this.dataHandler = null;
152
+ }
153
+ this.onInterrupt = null;
154
+ this.rl.resume();
155
+ }
156
+ }
157
+ exports.InputController = InputController;
158
+ // Ctrl-C handling: returns "clear" if input was non-empty, "warn" on first empty press, "exit" on second
159
+ let _ctrlCWarned = false;
160
+ function handleSigint(_rl, currentInput) {
161
+ if (currentInput.length > 0) {
162
+ _ctrlCWarned = false;
163
+ return "clear";
164
+ }
165
+ if (_ctrlCWarned) {
166
+ _ctrlCWarned = false;
167
+ return "exit";
168
+ }
169
+ _ctrlCWarned = true;
170
+ return "warn";
171
+ }
172
+ // History management
173
+ function addHistory(history, entry) {
174
+ if (!entry.trim())
175
+ return;
176
+ if (history.length > 0 && history[history.length - 1] === entry)
177
+ return;
178
+ history.push(entry);
179
+ }
180
+ function renderMarkdown(text) {
181
+ const placeholders = [];
182
+ // Protect fenced code blocks
183
+ let result = text.replace(/```(?:\w*\n)?([\s\S]*?)```/g, (_m, code) => {
184
+ const idx = placeholders.length;
185
+ placeholders.push(`\x1b[2m${code.replace(/\n$/, "")}\x1b[22m`);
186
+ return `\x00${idx}\x00`;
187
+ });
188
+ // Protect inline code
189
+ result = result.replace(/`([^`\n]+)`/g, (_m, code) => {
190
+ const idx = placeholders.length;
191
+ placeholders.push(`\x1b[36m${code}\x1b[39m`);
192
+ return `\x00${idx}\x00`;
193
+ });
194
+ // Bold
195
+ result = result.replace(/\*\*(.+?)\*\*/g, "\x1b[1m$1\x1b[22m");
196
+ // Italic (avoid matching inside bold remnants)
197
+ result = result.replace(/(?<!\*)\*(.+?)\*(?!\*)/g, "\x1b[3m$1\x1b[23m");
198
+ // Restore placeholders
199
+ result = result.replace(/\x00(\d+)\x00/g, (_m, idx) => placeholders[parseInt(idx)]);
200
+ return result;
201
+ }
202
+ // Ordered longest-first so we match ``` before ` and ** before *
203
+ const MARKERS = ["```", "**", "*", "`"];
204
+ class MarkdownStreamer {
205
+ buf = "";
206
+ openMarker = null;
207
+ push(text) {
208
+ this.buf += text;
209
+ return this.drain(false);
210
+ }
211
+ flush() {
212
+ return this.drain(true);
213
+ }
214
+ reset() {
215
+ this.buf = "";
216
+ this.openMarker = null;
217
+ }
218
+ drain(final) {
219
+ let out = "";
220
+ while (this.buf.length > 0) {
221
+ if (this.openMarker) {
222
+ const closeIdx = this.buf.indexOf(this.openMarker);
223
+ if (closeIdx !== -1) {
224
+ const segment = this.openMarker + this.buf.slice(0, closeIdx + this.openMarker.length);
225
+ out += renderMarkdown(segment);
226
+ this.buf = this.buf.slice(closeIdx + this.openMarker.length);
227
+ this.openMarker = null;
228
+ continue;
229
+ }
230
+ if (final) {
231
+ out += renderMarkdown(this.openMarker + this.buf);
232
+ this.buf = "";
233
+ this.openMarker = null;
234
+ }
235
+ break;
236
+ }
237
+ // Normal mode — look for the next opening marker
238
+ let earliest = -1;
239
+ let matched = null;
240
+ for (const m of MARKERS) {
241
+ const idx = this.buf.indexOf(m);
242
+ if (idx !== -1 && (earliest === -1 || idx < earliest)) {
243
+ earliest = idx;
244
+ matched = m;
245
+ }
246
+ }
247
+ if (matched !== null && earliest !== -1) {
248
+ // If the tail from the match to end-of-buffer is a proper prefix of a
249
+ // longer marker, hold it back rather than consuming it prematurely.
250
+ // E.g. a trailing `*` could be the start of `**`, trailing `` ` `` could be `` ``` ``.
251
+ const tail = this.buf.slice(earliest);
252
+ if (!final && MARKERS.some(m => m.length > tail.length && m.startsWith(tail))) {
253
+ if (earliest > 0) {
254
+ out += renderMarkdown(this.buf.slice(0, earliest));
255
+ this.buf = this.buf.slice(earliest);
256
+ }
257
+ break;
258
+ }
259
+ if (earliest > 0) {
260
+ out += renderMarkdown(this.buf.slice(0, earliest));
261
+ }
262
+ this.buf = this.buf.slice(earliest + matched.length);
263
+ this.openMarker = matched;
264
+ continue;
265
+ }
266
+ out += renderMarkdown(this.buf);
267
+ this.buf = "";
268
+ break;
269
+ }
270
+ return out;
271
+ }
272
+ }
273
+ exports.MarkdownStreamer = MarkdownStreamer;
274
+ function createCliCallbacks() {
275
+ (0, runtime_1.emitNervesEvent)({
276
+ component: "senses",
277
+ event: "senses.cli_callbacks_created",
278
+ message: "cli callbacks created",
279
+ meta: {},
280
+ });
281
+ let currentSpinner = null;
282
+ let hadReasoning = false;
283
+ let hadToolRun = false;
284
+ let textDirty = false; // true when text/reasoning was written without a trailing newline
285
+ const streamer = new MarkdownStreamer();
286
+ return {
287
+ onModelStart: () => {
288
+ currentSpinner?.stop();
289
+ currentSpinner = null;
290
+ hadReasoning = false;
291
+ textDirty = false;
292
+ streamer.reset();
293
+ const phrases = (0, phrases_1.getPhrases)();
294
+ const pool = hadToolRun ? phrases.followup : phrases.thinking;
295
+ const first = (0, phrases_1.pickPhrase)(pool);
296
+ currentSpinner = new Spinner(first, pool);
297
+ currentSpinner.start();
298
+ },
299
+ onModelStreamStart: () => {
300
+ currentSpinner?.stop();
301
+ currentSpinner = null;
302
+ },
303
+ onTextChunk: (text) => {
304
+ if (hadReasoning) {
305
+ process.stdout.write("\n\n");
306
+ hadReasoning = false;
307
+ }
308
+ const rendered = streamer.push(text);
309
+ if (rendered)
310
+ process.stdout.write(rendered);
311
+ textDirty = text.length > 0 && !text.endsWith("\n");
312
+ },
313
+ onReasoningChunk: (text) => {
314
+ hadReasoning = true;
315
+ process.stdout.write(`\x1b[2m${text}\x1b[0m`);
316
+ textDirty = text.length > 0 && !text.endsWith("\n");
317
+ },
318
+ onToolStart: (_name, _args) => {
319
+ // Stop the model-start spinner: when the model returns only tool calls
320
+ // (no content/reasoning), onModelStreamStart never fires, so the old
321
+ // spinner's intervals would leak.
322
+ currentSpinner?.stop();
323
+ // Ensure the spinner starts on a fresh line so it doesn't overwrite
324
+ // the last line of text/reasoning output via \r\x1b[K
325
+ if (textDirty) {
326
+ process.stdout.write("\n");
327
+ textDirty = false;
328
+ }
329
+ const toolPhrases = (0, phrases_1.getPhrases)().tool;
330
+ const first = (0, phrases_1.pickPhrase)(toolPhrases);
331
+ currentSpinner = new Spinner(first, toolPhrases);
332
+ currentSpinner.start();
333
+ hadToolRun = true;
334
+ },
335
+ onToolEnd: (name, argSummary, success) => {
336
+ currentSpinner?.stop();
337
+ currentSpinner = null;
338
+ const msg = (0, format_1.formatToolResult)(name, argSummary, success);
339
+ const color = success ? "\x1b[32m" : "\x1b[31m";
340
+ process.stderr.write(`${color}${msg}\x1b[0m\n`);
341
+ },
342
+ onError: (error, severity) => {
343
+ if (severity === "transient") {
344
+ currentSpinner?.fail(error.message);
345
+ currentSpinner = null;
346
+ }
347
+ else {
348
+ currentSpinner?.stop();
349
+ currentSpinner = null;
350
+ process.stderr.write(`\x1b[31m${(0, format_1.formatError)(error)}\x1b[0m\n`);
351
+ }
352
+ },
353
+ onKick: () => {
354
+ currentSpinner?.stop();
355
+ currentSpinner = null;
356
+ if (textDirty) {
357
+ process.stdout.write("\n");
358
+ textDirty = false;
359
+ }
360
+ process.stderr.write(`\x1b[33m${(0, format_1.formatKick)()}\x1b[0m\n`);
361
+ },
362
+ flushMarkdown: () => {
363
+ currentSpinner?.stop();
364
+ currentSpinner = null;
365
+ const remaining = streamer.flush();
366
+ if (remaining)
367
+ process.stdout.write(remaining);
368
+ },
369
+ };
370
+ }
371
+ async function main(agentName, options) {
372
+ if (agentName)
373
+ (0, identity_1.setAgentName)(agentName);
374
+ const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
375
+ // Fail fast if provider is misconfigured (triggers human-readable error + exit)
376
+ (0, core_1.getProvider)();
377
+ const registry = (0, commands_1.createCommandRegistry)();
378
+ (0, commands_1.registerDefaultCommands)(registry);
379
+ // Resolve context kernel (identity + channel) for CLI
380
+ const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
381
+ const friendStore = new store_file_1.FileFriendStore(friendsPath);
382
+ const username = os.userInfo().username;
383
+ const hostname = os.hostname();
384
+ const localExternalId = `${username}@${hostname}`;
385
+ const resolver = new resolver_1.FriendResolver(friendStore, {
386
+ provider: "local",
387
+ externalId: localExternalId,
388
+ displayName: username,
389
+ channel: "cli",
390
+ });
391
+ const resolvedContext = await resolver.resolve();
392
+ const cliToolContext = {
393
+ /* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
394
+ signin: async () => undefined,
395
+ context: resolvedContext,
396
+ friendStore,
397
+ summarize: (0, core_1.createSummarize)(),
398
+ };
399
+ const friendId = resolvedContext.friend.id;
400
+ const agentConfig = (0, identity_1.loadAgentConfig)();
401
+ (0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
402
+ level: agentConfig.logging?.level,
403
+ sinks: agentConfig.logging?.sinks,
404
+ });
405
+ const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
406
+ let sessionLock = null;
407
+ try {
408
+ sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
409
+ }
410
+ catch (error) {
411
+ /* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
412
+ if (error instanceof session_lock_1.SessionLockError) {
413
+ process.stderr.write(`${error.message}\n`);
414
+ return;
415
+ }
416
+ throw error;
417
+ /* v8 ignore stop */
418
+ }
419
+ // Load existing session or start fresh
420
+ const existing = (0, context_1.loadSession)(sessPath);
421
+ const messages = existing?.messages && existing.messages.length > 0
422
+ ? existing.messages
423
+ : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
424
+ // Pending queue drain: inject pending messages as harness-context + assistant-content pairs
425
+ const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
426
+ const drainToMessages = () => {
427
+ const pending = (0, pending_1.drainPending)(pendingDir);
428
+ if (pending.length === 0)
429
+ return 0;
430
+ for (const msg of pending) {
431
+ messages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
432
+ messages.push({ role: "assistant", content: msg.content });
433
+ }
434
+ return pending.length;
435
+ };
436
+ // Startup drain: deliver offline messages
437
+ const startupCount = drainToMessages();
438
+ if (startupCount > 0) {
439
+ (0, context_1.saveSession)(sessPath, messages);
440
+ }
441
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
442
+ const ctrl = new InputController(rl);
443
+ let currentAbort = null;
444
+ const history = [];
445
+ let closed = false;
446
+ rl.on("close", () => { closed = true; });
447
+ // eslint-disable-next-line no-console -- terminal UX: startup banner
448
+ console.log(`\n${(0, identity_1.getAgentName)()} (type /commands for help)\n`);
449
+ const cliCallbacks = createCliCallbacks();
450
+ process.stdout.write("\x1b[36m> \x1b[0m");
451
+ // Ctrl-C at the input prompt: clear line or warn/exit
452
+ // readline with terminal:true catches Ctrl-C in raw mode (no ^C echo)
453
+ rl.on("SIGINT", () => {
454
+ const rlInt = rl;
455
+ const currentLine = rlInt.line || "";
456
+ const result = handleSigint(rl, currentLine);
457
+ if (result === "clear") {
458
+ rlInt.line = "";
459
+ rlInt.cursor = 0;
460
+ process.stdout.write("\r\x1b[K\x1b[36m> \x1b[0m");
461
+ }
462
+ else if (result === "warn") {
463
+ rlInt.line = "";
464
+ rlInt.cursor = 0;
465
+ process.stdout.write("\r\x1b[K");
466
+ process.stderr.write("press Ctrl-C again to exit\n");
467
+ process.stdout.write("\x1b[36m> \x1b[0m");
468
+ }
469
+ else {
470
+ rl.close();
471
+ }
472
+ });
473
+ // Debounced line iterator: collects rapid-fire lines (paste) into a single input
474
+ async function* debouncedLines(source) {
475
+ if (pasteDebounceMs <= 0) {
476
+ yield* source;
477
+ return;
478
+ }
479
+ const iter = source[Symbol.asyncIterator]();
480
+ while (true) {
481
+ const first = await iter.next();
482
+ if (first.done)
483
+ break;
484
+ // Collect any lines that arrive within the debounce window (paste detection)
485
+ const lines = [first.value];
486
+ let more = true;
487
+ while (more) {
488
+ const raced = await Promise.race([
489
+ iter.next().then((r) => ({ kind: "line", result: r })),
490
+ new Promise((r) => setTimeout(() => r({ kind: "timeout" }), pasteDebounceMs)),
491
+ ]);
492
+ if (raced.kind === "timeout") {
493
+ more = false;
494
+ }
495
+ else if (raced.result.done) {
496
+ more = false;
497
+ }
498
+ else {
499
+ lines.push(raced.result.value);
500
+ }
501
+ }
502
+ yield lines.join("\n");
503
+ }
504
+ }
505
+ try {
506
+ for await (const input of debouncedLines(rl)) {
507
+ if (closed)
508
+ break;
509
+ if (!input.trim()) {
510
+ process.stdout.write("\x1b[36m> \x1b[0m");
511
+ continue;
512
+ }
513
+ const trustGate = (0, trust_gate_1.enforceTrustGate)({
514
+ friend: resolvedContext.friend,
515
+ provider: "local",
516
+ externalId: localExternalId,
517
+ channel: "cli",
518
+ });
519
+ if (!trustGate.allowed) {
520
+ if (trustGate.reason === "stranger_first_reply") {
521
+ process.stdout.write(`${trustGate.autoReply}\n`);
522
+ }
523
+ if (closed)
524
+ break;
525
+ process.stdout.write("\x1b[36m> \x1b[0m");
526
+ continue;
527
+ }
528
+ // Check for slash commands
529
+ const parsed = (0, commands_1.parseSlashCommand)(input);
530
+ if (parsed) {
531
+ const dispatchResult = registry.dispatch(parsed.command, { channel: "cli" });
532
+ if (dispatchResult.handled && dispatchResult.result) {
533
+ if (dispatchResult.result.action === "exit") {
534
+ break;
535
+ }
536
+ else if (dispatchResult.result.action === "new") {
537
+ messages.length = 0;
538
+ messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
539
+ (0, context_1.deleteSession)(sessPath);
540
+ // eslint-disable-next-line no-console -- terminal UX: session cleared
541
+ console.log("session cleared");
542
+ process.stdout.write("\x1b[36m> \x1b[0m");
543
+ continue;
544
+ }
545
+ else if (dispatchResult.result.action === "response") {
546
+ // eslint-disable-next-line no-console -- terminal UX: command dispatch result
547
+ console.log(dispatchResult.result.message || "");
548
+ process.stdout.write("\x1b[36m> \x1b[0m");
549
+ continue;
550
+ }
551
+ }
552
+ }
553
+ // Re-style the echoed input lines (readline terminal:true echoes each line)
554
+ // For multiline paste, each line was echoed separately — erase them all
555
+ const cols = process.stdout.columns || 80;
556
+ const inputLines = input.split("\n");
557
+ let echoRows = 0;
558
+ for (const line of inputLines) {
559
+ echoRows += Math.ceil((2 + line.length) / cols); // "> " prefix + line content
560
+ }
561
+ process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
562
+ messages.push({ role: "user", content: input });
563
+ addHistory(history, input);
564
+ currentAbort = new AbortController();
565
+ const traceId = (0, nerves_1.createTraceId)();
566
+ ctrl.suppress(() => currentAbort.abort());
567
+ let result;
568
+ try {
569
+ result = await (0, core_1.runAgent)(messages, cliCallbacks, "cli", currentAbort.signal, {
570
+ toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
571
+ toolContext: cliToolContext,
572
+ traceId,
573
+ });
574
+ }
575
+ catch {
576
+ // AbortError — silently return to prompt
577
+ }
578
+ cliCallbacks.flushMarkdown();
579
+ ctrl.restore();
580
+ currentAbort = null;
581
+ // Safety net: never silently swallow an empty response
582
+ const lastMsg = messages[messages.length - 1];
583
+ if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
584
+ process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
585
+ }
586
+ process.stdout.write("\n\n");
587
+ (0, context_1.postTurn)(messages, sessPath, result?.usage);
588
+ await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result?.usage);
589
+ // Post-turn: drain any pending messages that arrived during runAgent
590
+ drainToMessages();
591
+ // Post-turn: refresh system prompt so active sessions metadata is current
592
+ await (0, prompt_refresh_1.refreshSystemPrompt)(messages, "cli", undefined, resolvedContext);
593
+ if (closed)
594
+ break;
595
+ process.stdout.write("\x1b[36m> \x1b[0m");
596
+ }
597
+ }
598
+ finally {
599
+ sessionLock?.release();
600
+ rl.close();
601
+ // eslint-disable-next-line no-console -- terminal UX: goodbye
602
+ console.log("bye");
603
+ }
604
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCommandRegistry = createCommandRegistry;
4
+ exports.getToolChoiceRequired = getToolChoiceRequired;
5
+ exports.resetToolChoiceRequired = resetToolChoiceRequired;
6
+ exports.registerDefaultCommands = registerDefaultCommands;
7
+ exports.parseSlashCommand = parseSlashCommand;
8
+ const identity_1 = require("../heart/identity");
9
+ const runtime_1 = require("../nerves/runtime");
10
+ function createCommandRegistry() {
11
+ const commands = new Map();
12
+ return {
13
+ register(cmd) {
14
+ commands.set(cmd.name, cmd);
15
+ },
16
+ get(name) {
17
+ return commands.get(name);
18
+ },
19
+ list(channel) {
20
+ return [...commands.values()].filter((c) => c.channels.includes(channel));
21
+ },
22
+ dispatch(name, ctx) {
23
+ const cmd = commands.get(name);
24
+ if (!cmd)
25
+ return { handled: false };
26
+ return { handled: true, result: cmd.handler(ctx) };
27
+ },
28
+ };
29
+ }
30
+ // Module-level toggle for tool-required mode
31
+ let _toolChoiceRequired = false;
32
+ function getToolChoiceRequired() {
33
+ return _toolChoiceRequired;
34
+ }
35
+ function resetToolChoiceRequired() {
36
+ _toolChoiceRequired = false;
37
+ }
38
+ function registerDefaultCommands(registry) {
39
+ (0, runtime_1.emitNervesEvent)({
40
+ event: "repertoire.load_start",
41
+ component: "repertoire",
42
+ message: "registering default commands",
43
+ meta: {},
44
+ });
45
+ registry.register({
46
+ name: "exit",
47
+ description: `quit ${(0, identity_1.getAgentName)()}`,
48
+ channels: ["cli"],
49
+ handler: () => ({ action: "exit" }),
50
+ });
51
+ registry.register({
52
+ name: "new",
53
+ description: "start a new conversation",
54
+ channels: ["cli", "teams"],
55
+ handler: () => ({ action: "new" }),
56
+ });
57
+ registry.register({
58
+ name: "commands",
59
+ description: "list available commands",
60
+ channels: ["cli", "teams"],
61
+ handler: (ctx) => {
62
+ const cmds = registry.list(ctx.channel);
63
+ const lines = cmds.map((c) => `/${c.name} - ${c.description}`);
64
+ return { action: "response", message: lines.join("\n") };
65
+ },
66
+ });
67
+ registry.register({
68
+ name: "tool-required",
69
+ description: "toggle tool_choice required mode (forces tool calls)",
70
+ channels: ["cli"],
71
+ handler: () => {
72
+ _toolChoiceRequired = !_toolChoiceRequired;
73
+ return { action: "response", message: `tool-required mode: ${_toolChoiceRequired ? "ON" : "OFF"}` };
74
+ },
75
+ });
76
+ (0, runtime_1.emitNervesEvent)({
77
+ event: "repertoire.load_end",
78
+ component: "repertoire",
79
+ message: "registered default commands",
80
+ meta: {},
81
+ });
82
+ }
83
+ function parseSlashCommand(input) {
84
+ const trimmed = input.trim();
85
+ if (!trimmed.startsWith("/"))
86
+ return null;
87
+ // Reject // (double slash)
88
+ if (trimmed.startsWith("//"))
89
+ return null;
90
+ const rest = trimmed.slice(1);
91
+ if (!rest)
92
+ return null;
93
+ const spaceIdx = rest.indexOf(" ");
94
+ if (spaceIdx === -1) {
95
+ return { command: rest.toLowerCase(), args: "" };
96
+ }
97
+ return { command: rest.slice(0, spaceIdx).toLowerCase(), args: rest.slice(spaceIdx + 1) };
98
+ }