@ouro.bot/cli 0.1.0-alpha.4 → 0.1.0-alpha.41
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 +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +117 -188
- package/assets/ouroboros.png +0 -0
- package/changelog.json +170 -0
- package/dist/heart/config.js +81 -8
- package/dist/heart/core.js +78 -45
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/daemon-cli.js +987 -77
- package/dist/heart/daemon/daemon-entry.js +14 -5
- package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
- package/dist/heart/daemon/daemon.js +177 -9
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +4 -20
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +134 -0
- package/dist/heart/daemon/message-router.js +15 -6
- package/dist/heart/daemon/ouro-bot-entry.js +0 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-entry.js +0 -0
- package/dist/heart/daemon/ouro-path-installer.js +178 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +1 -1
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-metadata.js +118 -0
- package/dist/heart/daemon/sense-manager.js +266 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +99 -0
- package/dist/heart/daemon/specialist-tools.js +283 -0
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/subagent-installer.js +10 -1
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/identity.js +96 -4
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/providers/anthropic.js +16 -2
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/streaming.js +96 -21
- package/dist/mind/bundle-manifest.js +70 -0
- package/dist/mind/context.js +7 -7
- package/dist/mind/first-impressions.js +2 -1
- package/dist/mind/friends/channel.js +43 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/types.js +9 -1
- package/dist/mind/memory.js +10 -3
- package/dist/mind/pending.js +10 -2
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +222 -7
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +62 -4
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/tasks/index.js +2 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +202 -219
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +55 -35
- package/dist/senses/bluebubbles-client.js +434 -0
- package/dist/senses/bluebubbles-entry.js +11 -0
- package/dist/senses/bluebubbles-media.js +338 -0
- package/dist/senses/bluebubbles-model.js +261 -0
- package/dist/senses/bluebubbles-mutation-log.js +74 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +832 -0
- package/dist/senses/cli.js +327 -138
- package/dist/senses/debug-activity.js +127 -0
- package/dist/senses/inner-dialog.js +103 -55
- package/dist/senses/pipeline.js +124 -0
- package/dist/senses/teams.js +427 -112
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +14 -3
- package/subagents/README.md +40 -53
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
- package/dist/inner-worker-entry.js +0 -4
package/dist/senses/cli.js
CHANGED
|
@@ -34,10 +34,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MarkdownStreamer = exports.InputController = exports.Spinner = void 0;
|
|
37
|
+
exports.formatPendingPrefix = formatPendingPrefix;
|
|
37
38
|
exports.handleSigint = handleSigint;
|
|
38
39
|
exports.addHistory = addHistory;
|
|
39
40
|
exports.renderMarkdown = renderMarkdown;
|
|
40
41
|
exports.createCliCallbacks = createCliCallbacks;
|
|
42
|
+
exports.createDebouncedLines = createDebouncedLines;
|
|
43
|
+
exports.runCliSession = runCliSession;
|
|
41
44
|
exports.main = main;
|
|
42
45
|
const readline = __importStar(require("readline"));
|
|
43
46
|
const os = __importStar(require("os"));
|
|
@@ -49,7 +52,6 @@ const format_1 = require("../mind/format");
|
|
|
49
52
|
const config_1 = require("../heart/config");
|
|
50
53
|
const context_1 = require("../mind/context");
|
|
51
54
|
const pending_1 = require("../mind/pending");
|
|
52
|
-
const prompt_refresh_1 = require("../mind/prompt-refresh");
|
|
53
55
|
const commands_1 = require("./commands");
|
|
54
56
|
const identity_1 = require("../heart/identity");
|
|
55
57
|
const nerves_1 = require("../nerves");
|
|
@@ -59,7 +61,25 @@ const tokens_1 = require("../mind/friends/tokens");
|
|
|
59
61
|
const cli_logging_1 = require("../nerves/cli-logging");
|
|
60
62
|
const runtime_1 = require("../nerves/runtime");
|
|
61
63
|
const trust_gate_1 = require("./trust-gate");
|
|
64
|
+
const pipeline_1 = require("./pipeline");
|
|
65
|
+
const channel_1 = require("../mind/friends/channel");
|
|
62
66
|
const session_lock_1 = require("./session-lock");
|
|
67
|
+
const update_hooks_1 = require("../heart/daemon/update-hooks");
|
|
68
|
+
const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
|
|
69
|
+
const bundle_manifest_1 = require("../mind/bundle-manifest");
|
|
70
|
+
/**
|
|
71
|
+
* Format pending messages as content-prefix strings for injection into
|
|
72
|
+
* the next user message. Self-messages (from === agentName) become
|
|
73
|
+
* `[inner thought: {content}]`, inter-agent messages become
|
|
74
|
+
* `[message from {name}: {content}]`.
|
|
75
|
+
*/
|
|
76
|
+
function formatPendingPrefix(messages, agentName) {
|
|
77
|
+
return messages
|
|
78
|
+
.map((msg) => msg.from === agentName
|
|
79
|
+
? `[inner thought: ${msg.content}]`
|
|
80
|
+
: `[message from ${msg.from}: ${msg.content}]`)
|
|
81
|
+
.join("\n");
|
|
82
|
+
}
|
|
63
83
|
// spinner that only touches stderr, cleans up after itself
|
|
64
84
|
// exported for direct testability (stop-without-start branch)
|
|
65
85
|
class Spinner {
|
|
@@ -70,12 +90,14 @@ class Spinner {
|
|
|
70
90
|
msg = "";
|
|
71
91
|
phrases = null;
|
|
72
92
|
lastPhrase = "";
|
|
93
|
+
stopped = false;
|
|
73
94
|
constructor(m = "working", phrases) {
|
|
74
95
|
this.msg = m;
|
|
75
96
|
if (phrases && phrases.length > 0)
|
|
76
97
|
this.phrases = phrases;
|
|
77
98
|
}
|
|
78
99
|
start() {
|
|
100
|
+
this.stopped = false;
|
|
79
101
|
process.stderr.write("\r\x1b[K");
|
|
80
102
|
this.spin();
|
|
81
103
|
this.iv = setInterval(() => this.spin(), 80);
|
|
@@ -84,15 +106,23 @@ class Spinner {
|
|
|
84
106
|
}
|
|
85
107
|
}
|
|
86
108
|
spin() {
|
|
87
|
-
|
|
109
|
+
// Guard: clearInterval can't prevent already-dequeued callbacks
|
|
110
|
+
/* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
|
|
111
|
+
if (this.stopped)
|
|
112
|
+
return;
|
|
113
|
+
process.stderr.write(`\r\x1b[K${this.frames[this.i]} ${this.msg}... `);
|
|
88
114
|
this.i = (this.i + 1) % this.frames.length;
|
|
89
115
|
}
|
|
90
116
|
rotatePhrase() {
|
|
117
|
+
/* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
|
|
118
|
+
if (this.stopped)
|
|
119
|
+
return;
|
|
91
120
|
const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
|
|
92
121
|
this.lastPhrase = next;
|
|
93
122
|
this.msg = next;
|
|
94
123
|
}
|
|
95
124
|
stop(ok) {
|
|
125
|
+
this.stopped = true;
|
|
96
126
|
if (this.iv) {
|
|
97
127
|
clearInterval(this.iv);
|
|
98
128
|
this.iv = null;
|
|
@@ -297,12 +327,25 @@ function createCliCallbacks() {
|
|
|
297
327
|
currentSpinner.start();
|
|
298
328
|
},
|
|
299
329
|
onModelStreamStart: () => {
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
// No-op: content callbacks (onTextChunk, onReasoningChunk) handle
|
|
331
|
+
// stopping the spinner. onModelStreamStart fires too early and
|
|
332
|
+
// doesn't fire at all for final_answer tool streaming.
|
|
333
|
+
},
|
|
334
|
+
onClearText: () => {
|
|
335
|
+
streamer.reset();
|
|
302
336
|
},
|
|
303
337
|
onTextChunk: (text) => {
|
|
338
|
+
// Stop spinner if still running — final_answer streaming and Anthropic
|
|
339
|
+
// tool-only responses bypass onModelStreamStart, so the spinner would
|
|
340
|
+
// otherwise keep running (and its \r writes overwrite response text).
|
|
341
|
+
if (currentSpinner) {
|
|
342
|
+
currentSpinner.stop();
|
|
343
|
+
currentSpinner = null;
|
|
344
|
+
}
|
|
304
345
|
if (hadReasoning) {
|
|
305
|
-
|
|
346
|
+
// Single newline to separate reasoning from reply — reasoning
|
|
347
|
+
// output often ends with its own trailing newline(s)
|
|
348
|
+
process.stdout.write("\n");
|
|
306
349
|
hadReasoning = false;
|
|
307
350
|
}
|
|
308
351
|
const rendered = streamer.push(text);
|
|
@@ -311,6 +354,10 @@ function createCliCallbacks() {
|
|
|
311
354
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
312
355
|
},
|
|
313
356
|
onReasoningChunk: (text) => {
|
|
357
|
+
if (currentSpinner) {
|
|
358
|
+
currentSpinner.stop();
|
|
359
|
+
currentSpinner = null;
|
|
360
|
+
}
|
|
314
361
|
hadReasoning = true;
|
|
315
362
|
process.stdout.write(`\x1b[2m${text}\x1b[0m`);
|
|
316
363
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
@@ -368,88 +415,86 @@ function createCliCallbacks() {
|
|
|
368
415
|
},
|
|
369
416
|
};
|
|
370
417
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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)());
|
|
418
|
+
// Debounced line iterator: collects rapid-fire lines (paste) into a single input.
|
|
419
|
+
// When the debounce timeout wins the race, the pending iter.next() is saved
|
|
420
|
+
// and reused in the next iteration to prevent it from silently consuming input.
|
|
421
|
+
async function* createDebouncedLines(source, debounceMs) {
|
|
422
|
+
if (debounceMs <= 0) {
|
|
423
|
+
yield* source;
|
|
424
|
+
return;
|
|
409
425
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
426
|
+
const iter = source[Symbol.asyncIterator]();
|
|
427
|
+
let pending = null;
|
|
428
|
+
while (true) {
|
|
429
|
+
const first = pending ? await pending : await iter.next();
|
|
430
|
+
pending = null;
|
|
431
|
+
if (first.done)
|
|
432
|
+
break;
|
|
433
|
+
const lines = [first.value];
|
|
434
|
+
let more = true;
|
|
435
|
+
while (more) {
|
|
436
|
+
const nextPromise = iter.next();
|
|
437
|
+
const raced = await Promise.race([
|
|
438
|
+
nextPromise.then((r) => ({ kind: "line", result: r })),
|
|
439
|
+
new Promise((r) => setTimeout(() => r({ kind: "timeout" }), debounceMs)),
|
|
440
|
+
]);
|
|
441
|
+
if (raced.kind === "timeout") {
|
|
442
|
+
pending = nextPromise;
|
|
443
|
+
more = false;
|
|
444
|
+
}
|
|
445
|
+
else if (raced.result.done) {
|
|
446
|
+
more = false;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
lines.push(raced.result.value);
|
|
450
|
+
}
|
|
415
451
|
}
|
|
416
|
-
|
|
417
|
-
/* v8 ignore stop */
|
|
452
|
+
yield lines.join("\n");
|
|
418
453
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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);
|
|
454
|
+
}
|
|
455
|
+
async function runCliSession(options) {
|
|
456
|
+
/* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
|
|
457
|
+
const pasteDebounceMs = options.pasteDebounceMs ?? 50;
|
|
458
|
+
const registry = (0, commands_1.createCommandRegistry)();
|
|
459
|
+
if (!options.disableCommands) {
|
|
460
|
+
(0, commands_1.registerDefaultCommands)(registry);
|
|
440
461
|
}
|
|
462
|
+
const messages = options.messages
|
|
463
|
+
?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
|
|
441
464
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
442
465
|
const ctrl = new InputController(rl);
|
|
443
466
|
let currentAbort = null;
|
|
444
467
|
const history = [];
|
|
445
468
|
let closed = false;
|
|
446
469
|
rl.on("close", () => { closed = true; });
|
|
447
|
-
|
|
448
|
-
|
|
470
|
+
if (options.banner !== false) {
|
|
471
|
+
const bannerText = typeof options.banner === "string"
|
|
472
|
+
? options.banner
|
|
473
|
+
: `${options.agentName} (type /commands for help)`;
|
|
474
|
+
// eslint-disable-next-line no-console -- terminal UX: startup banner
|
|
475
|
+
console.log(`\n${bannerText}\n`);
|
|
476
|
+
}
|
|
449
477
|
const cliCallbacks = createCliCallbacks();
|
|
450
|
-
|
|
478
|
+
// exitOnToolCall machinery: wrap execTool to detect target tool
|
|
479
|
+
let exitToolResult;
|
|
480
|
+
let exitToolFired = false;
|
|
481
|
+
const resolvedExecTool = options.execTool;
|
|
482
|
+
const wrappedExecTool = options.exitOnToolCall && resolvedExecTool
|
|
483
|
+
? async (name, args, ctx) => {
|
|
484
|
+
const result = await resolvedExecTool(name, args, ctx);
|
|
485
|
+
if (name === options.exitOnToolCall) {
|
|
486
|
+
exitToolResult = result;
|
|
487
|
+
exitToolFired = true;
|
|
488
|
+
// Abort immediately so the model doesn't generate more output
|
|
489
|
+
// (e.g. reasoning about calling final_answer after complete_adoption)
|
|
490
|
+
currentAbort?.abort();
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
: resolvedExecTool;
|
|
495
|
+
// Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
|
|
496
|
+
const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
|
|
451
497
|
// 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
498
|
rl.on("SIGINT", () => {
|
|
454
499
|
const rlInt = rl;
|
|
455
500
|
const currentLine = rlInt.line || "";
|
|
@@ -470,38 +515,58 @@ async function main(agentName, options) {
|
|
|
470
515
|
rl.close();
|
|
471
516
|
}
|
|
472
517
|
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
518
|
+
const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
|
|
519
|
+
(0, runtime_1.emitNervesEvent)({
|
|
520
|
+
component: "senses",
|
|
521
|
+
event: "senses.cli_session_start",
|
|
522
|
+
message: "runCliSession started",
|
|
523
|
+
meta: { agentName: options.agentName, hasExitOnToolCall: !!options.exitOnToolCall },
|
|
524
|
+
});
|
|
525
|
+
let exitReason = "user_quit";
|
|
526
|
+
// Auto-first-turn: process the last user message immediately so the agent
|
|
527
|
+
// speaks first (e.g. specialist greeting). Only triggers when explicitly opted in.
|
|
528
|
+
if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
|
|
529
|
+
currentAbort = new AbortController();
|
|
530
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
531
|
+
ctrl.suppress(() => currentAbort.abort());
|
|
532
|
+
let result;
|
|
533
|
+
try {
|
|
534
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
535
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
536
|
+
traceId,
|
|
537
|
+
tools: options.tools,
|
|
538
|
+
execTool: wrappedExecTool,
|
|
539
|
+
toolContext: options.toolContext,
|
|
540
|
+
});
|
|
478
541
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
// AbortError (Ctrl-C) -- silently continue to prompt
|
|
544
|
+
// All other errors: show the user what happened
|
|
545
|
+
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
546
|
+
process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
cliCallbacks.flushMarkdown();
|
|
550
|
+
ctrl.restore();
|
|
551
|
+
currentAbort = null;
|
|
552
|
+
if (exitToolFired) {
|
|
553
|
+
exitReason = "tool_exit";
|
|
554
|
+
rl.close();
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
const lastMsg = messages[messages.length - 1];
|
|
558
|
+
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
559
|
+
process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
|
|
560
|
+
}
|
|
561
|
+
process.stdout.write("\n\n");
|
|
562
|
+
if (options.onTurnEnd) {
|
|
563
|
+
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
501
564
|
}
|
|
502
|
-
yield lines.join("\n");
|
|
503
565
|
}
|
|
504
566
|
}
|
|
567
|
+
if (!exitToolFired) {
|
|
568
|
+
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
569
|
+
}
|
|
505
570
|
try {
|
|
506
571
|
for await (const input of debouncedLines(rl)) {
|
|
507
572
|
if (closed)
|
|
@@ -510,20 +575,18 @@ async function main(agentName, options) {
|
|
|
510
575
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
511
576
|
continue;
|
|
512
577
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
578
|
+
// Optional input gate (e.g. trust gate in main)
|
|
579
|
+
if (options.onInput) {
|
|
580
|
+
const gate = options.onInput(input);
|
|
581
|
+
if (!gate.allowed) {
|
|
582
|
+
if (gate.reply) {
|
|
583
|
+
process.stdout.write(`${gate.reply}\n`);
|
|
584
|
+
}
|
|
585
|
+
if (closed)
|
|
586
|
+
break;
|
|
587
|
+
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
588
|
+
continue;
|
|
522
589
|
}
|
|
523
|
-
if (closed)
|
|
524
|
-
break;
|
|
525
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
526
|
-
continue;
|
|
527
590
|
}
|
|
528
591
|
// Check for slash commands
|
|
529
592
|
const parsed = (0, commands_1.parseSlashCommand)(input);
|
|
@@ -536,7 +599,7 @@ async function main(agentName, options) {
|
|
|
536
599
|
else if (dispatchResult.result.action === "new") {
|
|
537
600
|
messages.length = 0;
|
|
538
601
|
messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
|
|
539
|
-
|
|
602
|
+
await options.onNewSession?.();
|
|
540
603
|
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
541
604
|
console.log("session cleared");
|
|
542
605
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
@@ -550,55 +613,181 @@ async function main(agentName, options) {
|
|
|
550
613
|
}
|
|
551
614
|
}
|
|
552
615
|
}
|
|
553
|
-
// Re-style the echoed input lines
|
|
554
|
-
// For multiline paste, each line was echoed separately — erase them all
|
|
616
|
+
// Re-style the echoed input lines
|
|
555
617
|
const cols = process.stdout.columns || 80;
|
|
556
618
|
const inputLines = input.split("\n");
|
|
557
619
|
let echoRows = 0;
|
|
558
620
|
for (const line of inputLines) {
|
|
559
|
-
echoRows += Math.ceil((2 + line.length) / cols);
|
|
621
|
+
echoRows += Math.ceil((2 + line.length) / cols);
|
|
560
622
|
}
|
|
561
623
|
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
624
|
addHistory(history, input);
|
|
564
625
|
currentAbort = new AbortController();
|
|
565
|
-
const traceId = (0, nerves_1.createTraceId)();
|
|
566
626
|
ctrl.suppress(() => currentAbort.abort());
|
|
567
627
|
let result;
|
|
568
628
|
try {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
629
|
+
if (options.runTurn) {
|
|
630
|
+
// Pipeline-based turn: the runTurn callback handles user message assembly,
|
|
631
|
+
// pending drain, trust gate, runAgent, postTurn, and token accumulation.
|
|
632
|
+
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// Legacy path: inline runAgent (used by adoption specialist and tests)
|
|
636
|
+
const prefix = options.getContentPrefix?.();
|
|
637
|
+
messages.push({ role: "user", content: prefix ? `${prefix}\n\n${input}` : input });
|
|
638
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
639
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
640
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
641
|
+
traceId,
|
|
642
|
+
tools: options.tools,
|
|
643
|
+
execTool: wrappedExecTool,
|
|
644
|
+
toolContext: options.toolContext,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
574
647
|
}
|
|
575
|
-
catch {
|
|
576
|
-
// AbortError
|
|
648
|
+
catch (err) {
|
|
649
|
+
// AbortError (Ctrl-C) -- silently return to prompt
|
|
650
|
+
// All other errors: show the user what happened
|
|
651
|
+
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
652
|
+
process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
|
|
653
|
+
}
|
|
577
654
|
}
|
|
578
655
|
cliCallbacks.flushMarkdown();
|
|
579
656
|
ctrl.restore();
|
|
580
657
|
currentAbort = null;
|
|
658
|
+
// Check if exit tool was fired during this turn
|
|
659
|
+
if (exitToolFired) {
|
|
660
|
+
exitReason = "tool_exit";
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
581
663
|
// Safety net: never silently swallow an empty response
|
|
582
664
|
const lastMsg = messages[messages.length - 1];
|
|
583
665
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
584
666
|
process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
|
|
585
667
|
}
|
|
586
668
|
process.stdout.write("\n\n");
|
|
587
|
-
(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
// Post-turn: refresh system prompt so active sessions metadata is current
|
|
592
|
-
await (0, prompt_refresh_1.refreshSystemPrompt)(messages, "cli", undefined, resolvedContext);
|
|
669
|
+
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
670
|
+
if (options.onTurnEnd) {
|
|
671
|
+
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
672
|
+
}
|
|
593
673
|
if (closed)
|
|
594
674
|
break;
|
|
595
675
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
596
676
|
}
|
|
597
677
|
}
|
|
598
678
|
finally {
|
|
599
|
-
sessionLock?.release();
|
|
600
679
|
rl.close();
|
|
601
|
-
|
|
602
|
-
|
|
680
|
+
if (options.banner !== false) {
|
|
681
|
+
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
682
|
+
console.log("bye");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/* v8 ignore stop */
|
|
686
|
+
return { exitReason, toolResult: exitToolResult };
|
|
687
|
+
}
|
|
688
|
+
async function main(agentName, options) {
|
|
689
|
+
if (agentName)
|
|
690
|
+
(0, identity_1.setAgentName)(agentName);
|
|
691
|
+
const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
|
|
692
|
+
// Fallback: apply pending updates for daemon-less direct CLI usage
|
|
693
|
+
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
694
|
+
await (0, update_hooks_1.applyPendingUpdates)((0, identity_1.getAgentBundlesRoot)(), (0, bundle_manifest_1.getPackageVersion)());
|
|
695
|
+
// Fail fast if provider is misconfigured (triggers human-readable error + exit)
|
|
696
|
+
(0, core_1.getProvider)();
|
|
697
|
+
// Resolve context kernel (identity + channel) for CLI
|
|
698
|
+
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
699
|
+
const friendStore = new store_file_1.FileFriendStore(friendsPath);
|
|
700
|
+
const username = os.userInfo().username;
|
|
701
|
+
const hostname = os.hostname();
|
|
702
|
+
const localExternalId = `${username}@${hostname}`;
|
|
703
|
+
const resolver = new resolver_1.FriendResolver(friendStore, {
|
|
704
|
+
provider: "local",
|
|
705
|
+
externalId: localExternalId,
|
|
706
|
+
displayName: username,
|
|
707
|
+
channel: "cli",
|
|
708
|
+
});
|
|
709
|
+
const resolvedContext = await resolver.resolve();
|
|
710
|
+
const friendId = resolvedContext.friend.id;
|
|
711
|
+
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
712
|
+
(0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
|
|
713
|
+
level: agentConfig.logging?.level,
|
|
714
|
+
sinks: agentConfig.logging?.sinks,
|
|
715
|
+
});
|
|
716
|
+
const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
|
|
717
|
+
let sessionLock = null;
|
|
718
|
+
try {
|
|
719
|
+
sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
/* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
|
|
723
|
+
if (error instanceof session_lock_1.SessionLockError) {
|
|
724
|
+
process.stderr.write(`${error.message}\n`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
throw error;
|
|
728
|
+
/* v8 ignore stop */
|
|
729
|
+
}
|
|
730
|
+
// Load existing session or start fresh
|
|
731
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
732
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
733
|
+
? existing.messages
|
|
734
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
|
|
735
|
+
// Per-turn pipeline input: CLI capabilities and pending dir
|
|
736
|
+
const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
|
|
737
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
|
|
738
|
+
const summarize = (0, core_1.createSummarize)();
|
|
739
|
+
try {
|
|
740
|
+
await runCliSession({
|
|
741
|
+
agentName: (0, identity_1.getAgentName)(),
|
|
742
|
+
pasteDebounceMs,
|
|
743
|
+
messages: sessionMessages,
|
|
744
|
+
runTurn: async (messages, userInput, callbacks, signal) => {
|
|
745
|
+
// Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
|
|
746
|
+
// User message passed via input.messages so the pipeline can prepend pending messages to it.
|
|
747
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
748
|
+
channel: "cli",
|
|
749
|
+
capabilities: cliCapabilities,
|
|
750
|
+
messages: [{ role: "user", content: userInput }],
|
|
751
|
+
callbacks,
|
|
752
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
753
|
+
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages, sessionPath: sessPath }) },
|
|
754
|
+
pendingDir,
|
|
755
|
+
friendStore,
|
|
756
|
+
provider: "local",
|
|
757
|
+
externalId: localExternalId,
|
|
758
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
759
|
+
drainPending: pending_1.drainPending,
|
|
760
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
761
|
+
...opts,
|
|
762
|
+
toolContext: {
|
|
763
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
764
|
+
signin: async () => undefined,
|
|
765
|
+
...opts?.toolContext,
|
|
766
|
+
summarize,
|
|
767
|
+
},
|
|
768
|
+
}),
|
|
769
|
+
postTurn: context_1.postTurn,
|
|
770
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
771
|
+
signal,
|
|
772
|
+
runAgentOptions: {
|
|
773
|
+
toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
|
|
774
|
+
traceId: (0, nerves_1.createTraceId)(),
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
// Handle gate rejection: display auto-reply if present
|
|
778
|
+
if (!result.gateResult.allowed) {
|
|
779
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
780
|
+
process.stdout.write(`${result.gateResult.autoReply}\n`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return { usage: result.usage };
|
|
784
|
+
},
|
|
785
|
+
onNewSession: () => {
|
|
786
|
+
(0, context_1.deleteSession)(sessPath);
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
finally {
|
|
791
|
+
sessionLock?.release();
|
|
603
792
|
}
|
|
604
793
|
}
|