@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.
- package/AdoptionSpecialist.ouro/agent.json +20 -0
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +22 -0
- package/AdoptionSpecialist.ouro/psyche/identities/basilisk.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/jafar.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/jormungandr.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/kaa.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/medusa.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/nagini.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/ouroboros.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/python.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/quetzalcoatl.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/sir-hiss.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/the-serpent.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/the-snake.md +31 -0
- package/README.md +224 -6
- package/dist/heart/agent-entry.js +17 -0
- package/dist/heart/api-error.js +34 -0
- package/dist/heart/config.js +296 -0
- package/dist/heart/core.js +515 -0
- package/dist/heart/daemon/daemon-cli.js +675 -0
- package/dist/heart/daemon/daemon-entry.js +74 -0
- package/dist/heart/daemon/daemon.js +313 -0
- package/dist/heart/daemon/hatch-flow.js +285 -0
- package/dist/heart/daemon/hatch-specialist.js +107 -0
- package/dist/heart/daemon/health-monitor.js +79 -0
- package/dist/heart/daemon/log-tailer.js +146 -0
- package/dist/heart/daemon/message-router.js +98 -0
- package/dist/heart/daemon/os-cron.js +260 -0
- package/dist/heart/daemon/ouro-bot-entry.js +23 -0
- package/dist/heart/daemon/ouro-bot-wrapper.js +90 -0
- package/dist/heart/daemon/ouro-entry.js +23 -0
- package/dist/heart/daemon/ouro-uti.js +212 -0
- package/dist/heart/daemon/process-manager.js +237 -0
- package/dist/heart/daemon/runtime-logging.js +98 -0
- package/dist/heart/daemon/subagent-installer.js +125 -0
- package/dist/heart/daemon/task-scheduler.js +240 -0
- package/dist/heart/harness.js +26 -0
- package/dist/heart/identity.js +281 -0
- package/dist/heart/kicks.js +144 -0
- package/dist/heart/primitives.js +4 -0
- package/dist/heart/providers/anthropic.js +329 -0
- package/dist/heart/providers/azure.js +66 -0
- package/dist/heart/providers/minimax.js +53 -0
- package/dist/heart/providers/openai-codex.js +162 -0
- package/dist/heart/streaming.js +412 -0
- package/dist/heart/turn-coordinator.js +62 -0
- package/dist/inner-worker-entry.js +4 -0
- package/dist/mind/associative-recall.js +197 -0
- package/dist/mind/bundle-manifest.js +118 -0
- package/dist/mind/context.js +302 -0
- package/dist/mind/first-impressions.js +43 -0
- package/dist/mind/format.js +56 -0
- package/dist/mind/friends/channel.js +41 -0
- package/dist/mind/friends/resolver.js +84 -0
- package/dist/mind/friends/store-file.js +171 -0
- package/dist/mind/friends/store.js +4 -0
- package/dist/mind/friends/tokens.js +26 -0
- package/dist/mind/friends/types.js +21 -0
- package/dist/mind/memory.js +388 -0
- package/dist/mind/pending.js +93 -0
- package/dist/mind/phrases.js +43 -0
- package/dist/mind/prompt-refresh.js +20 -0
- package/dist/mind/prompt.js +352 -0
- package/dist/mind/token-estimate.js +119 -0
- package/dist/nerves/cli-logging.js +31 -0
- package/dist/nerves/coverage/audit-rules.js +81 -0
- package/dist/nerves/coverage/audit.js +200 -0
- package/dist/nerves/coverage/cli-main.js +5 -0
- package/dist/nerves/coverage/cli.js +51 -0
- package/dist/nerves/coverage/contract.js +23 -0
- package/dist/nerves/coverage/file-completeness.js +56 -0
- package/dist/nerves/coverage/run-artifacts.js +77 -0
- package/dist/nerves/coverage/source-scanner.js +34 -0
- package/dist/nerves/index.js +152 -0
- package/dist/nerves/runtime.js +38 -0
- package/dist/repertoire/ado-client.js +211 -0
- package/dist/repertoire/ado-context.js +73 -0
- package/dist/repertoire/ado-semantic.js +841 -0
- package/dist/repertoire/ado-templates.js +146 -0
- package/dist/repertoire/coding/index.js +36 -0
- package/dist/repertoire/coding/manager.js +489 -0
- package/dist/repertoire/coding/monitor.js +60 -0
- package/dist/repertoire/coding/reporter.js +45 -0
- package/dist/repertoire/coding/spawner.js +102 -0
- package/dist/repertoire/coding/tools.js +167 -0
- package/dist/repertoire/coding/types.js +2 -0
- package/dist/repertoire/data/ado-endpoints.json +122 -0
- package/dist/repertoire/data/graph-endpoints.json +212 -0
- package/dist/repertoire/github-client.js +64 -0
- package/dist/repertoire/graph-client.js +118 -0
- package/dist/repertoire/skills.js +156 -0
- package/dist/repertoire/tasks/board.js +122 -0
- package/dist/repertoire/tasks/index.js +210 -0
- package/dist/repertoire/tasks/lifecycle.js +80 -0
- package/dist/repertoire/tasks/middleware.js +65 -0
- package/dist/repertoire/tasks/parser.js +173 -0
- package/dist/repertoire/tasks/scanner.js +132 -0
- package/dist/repertoire/tasks/transitions.js +145 -0
- package/dist/repertoire/tasks/types.js +2 -0
- package/dist/repertoire/tools-base.js +714 -0
- package/dist/repertoire/tools-github.js +53 -0
- package/dist/repertoire/tools-teams.js +308 -0
- package/dist/repertoire/tools.js +199 -0
- package/dist/senses/cli-entry.js +15 -0
- package/dist/senses/cli.js +604 -0
- package/dist/senses/commands.js +98 -0
- package/dist/senses/inner-dialog-worker.js +61 -0
- package/dist/senses/inner-dialog.js +231 -0
- package/dist/senses/session-lock.js +119 -0
- package/dist/senses/teams-entry.js +15 -0
- package/dist/senses/teams.js +696 -0
- package/dist/senses/trust-gate.js +150 -0
- package/package.json +34 -11
- package/subagents/README.md +73 -0
- package/subagents/work-doer.md +233 -0
- package/subagents/work-merger.md +624 -0
- package/subagents/work-planner.md +373 -0
- 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
|
+
}
|