@ouro.bot/cli 0.1.0-alpha.23 → 0.1.0-alpha.231
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/README.md +187 -191
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +1 -1
- package/changelog.json +1408 -1
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +140 -0
- package/dist/arc/episodes.js +119 -0
- package/dist/arc/intentions.js +133 -0
- package/dist/arc/json-store.js +120 -0
- package/dist/arc/obligations.js +237 -0
- package/dist/arc/presence.js +185 -0
- package/dist/heart/active-work.js +832 -0
- package/dist/heart/agent-entry.js +37 -2
- package/dist/heart/auth/auth-flow.js +447 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +111 -0
- package/dist/heart/config-registry.js +305 -0
- package/dist/heart/config.js +63 -30
- package/dist/heart/core.js +601 -145
- package/dist/heart/cross-chat-delivery.js +131 -0
- package/dist/heart/daemon/agent-config-check.js +149 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/agent-service.js +360 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/cli-defaults.js +557 -0
- package/dist/heart/daemon/cli-exec.js +1847 -0
- package/dist/heart/daemon/cli-parse.js +759 -0
- package/dist/heart/daemon/cli-render.js +316 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/daemon-cli.js +29 -1163
- package/dist/heart/daemon/daemon-entry.js +202 -4
- package/dist/heart/daemon/daemon-health.js +136 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +153 -12
- package/dist/heart/daemon/daemon-tombstone.js +123 -0
- package/dist/heart/daemon/daemon.js +489 -20
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/launchd.js +39 -8
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/ouro-bot-entry.js +1 -1
- package/dist/heart/daemon/process-manager.js +123 -0
- package/dist/heart/daemon/run-hooks.js +2 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +101 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sense-manager.js +72 -3
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +201 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/habits/habit-migration.js +181 -0
- package/dist/heart/habits/habit-parser.js +119 -0
- package/dist/heart/habits/habit-scheduler.js +369 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +29 -119
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +10 -7
- package/dist/heart/{daemon → hatch}/specialist-tools.js +49 -3
- package/dist/heart/identity.js +144 -55
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/mcp/mcp-server.js +608 -0
- package/dist/heart/migrate-config.js +127 -0
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/outlook/outlook-http.js +439 -0
- package/dist/heart/outlook/outlook-read.js +1618 -0
- package/dist/heart/outlook/outlook-render.js +1032 -0
- package/dist/heart/outlook/outlook-types.js +7 -0
- package/dist/heart/outlook/outlook-view.js +194 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +162 -0
- package/dist/heart/providers/anthropic-token.js +163 -0
- package/dist/heart/providers/anthropic.js +187 -41
- package/dist/heart/providers/azure.js +116 -10
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +51 -16
- package/dist/heart/session-activity.js +172 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/start-of-turn-packet.js +330 -0
- package/dist/heart/streaming.js +36 -27
- package/dist/heart/sync.js +217 -0
- package/dist/heart/target-resolution.js +127 -0
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/tool-activity-callbacks.js +36 -0
- package/dist/heart/tool-description.js +135 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +358 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/heart/versioning/ouro-bot-global-installer.js +128 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/{daemon → versioning}/ouro-path-installer.js +78 -35
- package/dist/heart/versioning/ouro-version-manager.js +171 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +30 -6
- package/dist/heart/{daemon → versioning}/update-checker.js +9 -1
- package/dist/heart/{daemon → versioning}/update-hooks.js +20 -2
- package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +41 -3
- package/dist/mind/associative-recall.js +126 -66
- package/dist/mind/bundle-manifest.js +8 -1
- package/dist/mind/context.js +125 -14
- package/dist/mind/{memory.js → diary.js} +73 -96
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +56 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +58 -3
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +779 -109
- package/dist/mind/scrutiny.js +173 -0
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/coverage/audit.js +1 -1
- package/dist/nerves/coverage/file-completeness.js +47 -5
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +64 -4
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/outlook-ui/assets/index-Ck8agNeO.js +61 -0
- package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
- package/dist/outlook-ui/index.html +15 -0
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +11 -5
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +330 -0
- package/dist/repertoire/coding/feedback.js +197 -30
- package/dist/repertoire/coding/manager.js +149 -2
- package/dist/repertoire/coding/spawner.js +55 -9
- package/dist/repertoire/coding/tools.js +170 -7
- package/dist/repertoire/credential-access.js +449 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +342 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +251 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +14 -23
- package/dist/repertoire/tasks/board.js +43 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +28 -10
- package/dist/repertoire/tasks/lifecycle.js +2 -2
- package/dist/repertoire/tasks/parser.js +3 -2
- package/dist/repertoire/tasks/scanner.js +194 -37
- package/dist/repertoire/tasks/transitions.js +11 -28
- package/dist/repertoire/tools-base.js +35 -771
- package/dist/repertoire/tools-bluebubbles.js +94 -0
- package/dist/repertoire/tools-bridge.js +141 -0
- package/dist/repertoire/tools-config.js +266 -0
- package/dist/repertoire/tools-continuity.js +248 -0
- package/dist/repertoire/tools-credential.js +184 -0
- package/dist/repertoire/tools-files.js +342 -0
- package/dist/repertoire/tools-flow.js +62 -0
- package/dist/repertoire/tools-github.js +1 -6
- package/dist/repertoire/tools-memory.js +347 -0
- package/dist/repertoire/tools-session.js +739 -0
- package/dist/repertoire/tools-shell.js +120 -0
- package/dist/repertoire/tools-surface.js +243 -0
- package/dist/repertoire/tools-teams.js +12 -59
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools.js +132 -124
- package/dist/repertoire/travel-api-client.js +214 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/attention-queue.js +97 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +109 -10
- package/dist/senses/bluebubbles/entry.js +13 -0
- package/dist/senses/bluebubbles/inbound-log.js +109 -0
- package/dist/senses/bluebubbles/index.js +1374 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +119 -24
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +13 -5
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +46 -6
- package/dist/senses/bluebubbles/runtime-state.js +109 -0
- package/dist/senses/bluebubbles/session-cleanup.js +72 -0
- package/dist/senses/cli/adapter.js +274 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +358 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +83 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +198 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +1 -1
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +503 -244
- package/dist/senses/commands.js +65 -1
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog-worker.js +112 -19
- package/dist/senses/inner-dialog.js +594 -85
- package/dist/senses/pipeline.js +559 -0
- package/dist/senses/shared-turn.js +196 -0
- package/dist/senses/surface-tool.js +68 -0
- package/dist/senses/teams.js +667 -120
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +12 -6
- package/skills/browser-navigation.md +110 -0
- package/skills/configure-dev-tools.md +81 -0
- package/skills/travel-planning.md +138 -0
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/dist/senses/bluebubbles-entry.js +0 -11
- package/dist/senses/bluebubbles.js +0 -421
- package/dist/senses/debug-activity.js +0 -107
- package/subagents/README.md +0 -73
- package/subagents/work-doer.md +0 -235
- package/subagents/work-merger.md +0 -618
- package/subagents/work-planner.md +0 -382
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
- /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
- /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
- /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
package/dist/senses/cli.js
CHANGED
|
@@ -33,7 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.MarkdownStreamer = exports.InputController = exports.Spinner = void 0;
|
|
36
|
+
exports.InputQueue = exports.MarkdownStreamer = exports.InputController = exports.Spinner = exports.StreamingWordWrapper = exports.wrapCliText = exports.formatEchoedInputSummary = void 0;
|
|
37
|
+
exports.formatPendingPrefix = formatPendingPrefix;
|
|
38
|
+
exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
|
|
39
|
+
exports.writeCliAsyncAssistantMessage = writeCliAsyncAssistantMessage;
|
|
40
|
+
exports.pauseActiveSpinner = pauseActiveSpinner;
|
|
41
|
+
exports.resumeActiveSpinner = resumeActiveSpinner;
|
|
42
|
+
exports.setActiveSpinner = setActiveSpinner;
|
|
37
43
|
exports.handleSigint = handleSigint;
|
|
38
44
|
exports.addHistory = addHistory;
|
|
39
45
|
exports.renderMarkdown = renderMarkdown;
|
|
@@ -51,9 +57,9 @@ const format_1 = require("../mind/format");
|
|
|
51
57
|
const config_1 = require("../heart/config");
|
|
52
58
|
const context_1 = require("../mind/context");
|
|
53
59
|
const pending_1 = require("../mind/pending");
|
|
54
|
-
const prompt_refresh_1 = require("../mind/prompt-refresh");
|
|
55
60
|
const commands_1 = require("./commands");
|
|
56
61
|
const identity_1 = require("../heart/identity");
|
|
62
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
57
63
|
const nerves_1 = require("../nerves");
|
|
58
64
|
const store_file_1 = require("../mind/friends/store-file");
|
|
59
65
|
const resolver_1 = require("../mind/friends/resolver");
|
|
@@ -61,72 +67,63 @@ const tokens_1 = require("../mind/friends/tokens");
|
|
|
61
67
|
const cli_logging_1 = require("../nerves/cli-logging");
|
|
62
68
|
const runtime_1 = require("../nerves/runtime");
|
|
63
69
|
const trust_gate_1 = require("./trust-gate");
|
|
70
|
+
const pipeline_1 = require("./pipeline");
|
|
71
|
+
const channel_1 = require("../mind/friends/channel");
|
|
64
72
|
const session_lock_1 = require("./session-lock");
|
|
65
|
-
const update_hooks_1 = require("../heart/
|
|
73
|
+
const update_hooks_1 = require("../heart/versioning/update-hooks");
|
|
66
74
|
const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
|
|
75
|
+
const agent_config_v2_1 = require("../heart/daemon/hooks/agent-config-v2");
|
|
67
76
|
const bundle_manifest_1 = require("../mind/bundle-manifest");
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
|
|
106
|
-
this.lastPhrase = next;
|
|
107
|
-
this.msg = next;
|
|
108
|
-
}
|
|
109
|
-
stop(ok) {
|
|
110
|
-
this.stopped = true;
|
|
111
|
-
if (this.iv) {
|
|
112
|
-
clearInterval(this.iv);
|
|
113
|
-
this.iv = null;
|
|
114
|
-
}
|
|
115
|
-
if (this.piv) {
|
|
116
|
-
clearInterval(this.piv);
|
|
117
|
-
this.piv = null;
|
|
118
|
-
}
|
|
119
|
-
process.stderr.write("\r\x1b[K");
|
|
120
|
-
/* v8 ignore next -- ok parameter currently unused by callers @preserve */
|
|
121
|
-
if (ok)
|
|
122
|
-
process.stderr.write(`\x1b[32m\u2713\x1b[0m ${ok}\n`);
|
|
123
|
-
}
|
|
124
|
-
fail(msg) {
|
|
125
|
-
this.stop();
|
|
126
|
-
process.stderr.write(`\x1b[31m\u2717\x1b[0m ${msg}\n`);
|
|
77
|
+
const cli_layout_1 = require("./cli-layout");
|
|
78
|
+
const spinner_imperative_1 = require("./cli/spinner-imperative");
|
|
79
|
+
const tool_display_1 = require("./cli/tool-display");
|
|
80
|
+
var cli_layout_2 = require("./cli-layout");
|
|
81
|
+
Object.defineProperty(exports, "formatEchoedInputSummary", { enumerable: true, get: function () { return cli_layout_2.formatEchoedInputSummary; } });
|
|
82
|
+
Object.defineProperty(exports, "wrapCliText", { enumerable: true, get: function () { return cli_layout_2.wrapCliText; } });
|
|
83
|
+
Object.defineProperty(exports, "StreamingWordWrapper", { enumerable: true, get: function () { return cli_layout_2.StreamingWordWrapper; } });
|
|
84
|
+
/**
|
|
85
|
+
* Format pending messages as content-prefix strings for injection into
|
|
86
|
+
* the next user message. Self-messages (from === agentName) become
|
|
87
|
+
* `[inner thought: {content}]`, inter-agent messages become
|
|
88
|
+
* `[message from {name}: {content}]`.
|
|
89
|
+
*/
|
|
90
|
+
function formatPendingPrefix(messages, agentName) {
|
|
91
|
+
return messages
|
|
92
|
+
.map((msg) => msg.from === agentName
|
|
93
|
+
? `[inner thought: ${msg.content}]`
|
|
94
|
+
: `[message from ${msg.from}: ${msg.content}]`)
|
|
95
|
+
.join("\n");
|
|
96
|
+
}
|
|
97
|
+
function getCliContinuityIngressTexts(input) {
|
|
98
|
+
const trimmed = input.trim();
|
|
99
|
+
return trimmed ? [trimmed] : [];
|
|
100
|
+
}
|
|
101
|
+
const CLI_PROMPT = "\x1b[36m) \x1b[0m";
|
|
102
|
+
function writeCliAsyncAssistantMessage(rl, message, stdout = process.stdout) {
|
|
103
|
+
const rlInt = rl;
|
|
104
|
+
const currentLine = rlInt.line ?? "";
|
|
105
|
+
const currentCursor = rlInt.cursor ?? currentLine.length;
|
|
106
|
+
stdout.write("\r\x1b[K");
|
|
107
|
+
stdout.write(`${renderMarkdown(message)}\n`);
|
|
108
|
+
stdout.write(CLI_PROMPT);
|
|
109
|
+
if (!currentLine)
|
|
110
|
+
return;
|
|
111
|
+
stdout.write(currentLine);
|
|
112
|
+
if (currentCursor < currentLine.length) {
|
|
113
|
+
readline.cursorTo(process.stdout, 2 + currentCursor);
|
|
127
114
|
}
|
|
128
115
|
}
|
|
129
|
-
|
|
116
|
+
// Module-level active spinner for log coordination.
|
|
117
|
+
// The terminal log sink calls these to avoid interleaving with spinner output.
|
|
118
|
+
let _activeSpinner = null;
|
|
119
|
+
/* v8 ignore start -- spinner coordination: exercised at runtime, not unit-testable without real terminal @preserve */
|
|
120
|
+
function pauseActiveSpinner() { _activeSpinner?.pause(); }
|
|
121
|
+
function resumeActiveSpinner() { _activeSpinner?.resume(); }
|
|
122
|
+
/* v8 ignore stop */
|
|
123
|
+
function setActiveSpinner(s) { _activeSpinner = s; }
|
|
124
|
+
// Re-export ImperativeSpinner as Spinner for backward compatibility
|
|
125
|
+
var spinner_imperative_2 = require("./cli/spinner-imperative");
|
|
126
|
+
Object.defineProperty(exports, "Spinner", { enumerable: true, get: function () { return spinner_imperative_2.ImperativeSpinner; } });
|
|
130
127
|
// Input controller: pauses readline during model/tool execution.
|
|
131
128
|
// Does NOT touch raw mode — readline with terminal:true manages raw mode
|
|
132
129
|
// internally. Touching it causes ^C to be echoed by the terminal driver.
|
|
@@ -294,59 +291,55 @@ function createCliCallbacks() {
|
|
|
294
291
|
meta: {},
|
|
295
292
|
});
|
|
296
293
|
let currentSpinner = null;
|
|
297
|
-
|
|
294
|
+
function setSpinner(s) { currentSpinner = s; setActiveSpinner(s); }
|
|
298
295
|
let hadToolRun = false;
|
|
299
296
|
let textDirty = false; // true when text/reasoning was written without a trailing newline
|
|
300
297
|
const streamer = new MarkdownStreamer();
|
|
298
|
+
const wrapper = new cli_layout_1.StreamingWordWrapper();
|
|
301
299
|
return {
|
|
302
300
|
onModelStart: () => {
|
|
303
301
|
currentSpinner?.stop();
|
|
304
|
-
|
|
305
|
-
hadReasoning = false;
|
|
302
|
+
setSpinner(null);
|
|
306
303
|
textDirty = false;
|
|
307
304
|
streamer.reset();
|
|
305
|
+
wrapper.reset();
|
|
308
306
|
const phrases = (0, phrases_1.getPhrases)();
|
|
309
307
|
const pool = hadToolRun ? phrases.followup : phrases.thinking;
|
|
310
308
|
const first = (0, phrases_1.pickPhrase)(pool);
|
|
311
|
-
|
|
309
|
+
setSpinner(new spinner_imperative_1.ImperativeSpinner(first, pool));
|
|
312
310
|
currentSpinner.start();
|
|
313
311
|
},
|
|
314
312
|
onModelStreamStart: () => {
|
|
315
313
|
// No-op: content callbacks (onTextChunk, onReasoningChunk) handle
|
|
316
314
|
// stopping the spinner. onModelStreamStart fires too early and
|
|
317
|
-
// doesn't fire at all for
|
|
315
|
+
// doesn't fire at all for settle tool streaming.
|
|
318
316
|
},
|
|
319
317
|
onClearText: () => {
|
|
320
318
|
streamer.reset();
|
|
319
|
+
wrapper.reset();
|
|
321
320
|
},
|
|
322
321
|
onTextChunk: (text) => {
|
|
323
|
-
// Stop spinner if still running —
|
|
322
|
+
// Stop spinner if still running — settle streaming and Anthropic
|
|
324
323
|
// tool-only responses bypass onModelStreamStart, so the spinner would
|
|
325
324
|
// otherwise keep running (and its \r writes overwrite response text).
|
|
326
325
|
if (currentSpinner) {
|
|
327
326
|
currentSpinner.stop();
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
if (hadReasoning) {
|
|
331
|
-
// Single newline to separate reasoning from reply — reasoning
|
|
332
|
-
// output often ends with its own trailing newline(s)
|
|
333
|
-
process.stdout.write("\n");
|
|
334
|
-
hadReasoning = false;
|
|
327
|
+
setSpinner(null);
|
|
335
328
|
}
|
|
336
329
|
const rendered = streamer.push(text);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (currentSpinner) {
|
|
343
|
-
currentSpinner.stop();
|
|
344
|
-
currentSpinner = null;
|
|
330
|
+
/* v8 ignore start -- wrapper integration: tested via cli.test.ts onTextChunk tests @preserve */
|
|
331
|
+
if (rendered) {
|
|
332
|
+
const wrapped = wrapper.push(rendered);
|
|
333
|
+
if (wrapped)
|
|
334
|
+
process.stdout.write(wrapped);
|
|
345
335
|
}
|
|
346
|
-
|
|
347
|
-
process.stdout.write(`\x1b[2m${text}\x1b[0m`);
|
|
336
|
+
/* v8 ignore stop */
|
|
348
337
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
349
338
|
},
|
|
339
|
+
onReasoningChunk: (_text) => {
|
|
340
|
+
// Keep reasoning private in the CLI surface. The spinner continues to
|
|
341
|
+
// represent active thinking until actual tool or answer output arrives.
|
|
342
|
+
},
|
|
350
343
|
onToolStart: (_name, _args) => {
|
|
351
344
|
// Stop the model-start spinner: when the model returns only tool calls
|
|
352
345
|
// (no content/reasoning), onModelStreamStart never fires, so the old
|
|
@@ -360,31 +353,29 @@ function createCliCallbacks() {
|
|
|
360
353
|
}
|
|
361
354
|
const toolPhrases = (0, phrases_1.getPhrases)().tool;
|
|
362
355
|
const first = (0, phrases_1.pickPhrase)(toolPhrases);
|
|
363
|
-
|
|
356
|
+
setSpinner(new spinner_imperative_1.ImperativeSpinner(first, toolPhrases));
|
|
364
357
|
currentSpinner.start();
|
|
365
358
|
hadToolRun = true;
|
|
366
359
|
},
|
|
367
360
|
onToolEnd: (name, argSummary, success) => {
|
|
368
361
|
currentSpinner?.stop();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const color = success ? "\x1b[32m" : "\x1b[31m";
|
|
372
|
-
process.stderr.write(`${color}${msg}\x1b[0m\n`);
|
|
362
|
+
setSpinner(null);
|
|
363
|
+
(0, tool_display_1.writeToolEnd)(name, argSummary, success);
|
|
373
364
|
},
|
|
374
365
|
onError: (error, severity) => {
|
|
375
366
|
if (severity === "transient") {
|
|
376
367
|
currentSpinner?.fail(error.message);
|
|
377
|
-
|
|
368
|
+
setSpinner(null);
|
|
378
369
|
}
|
|
379
370
|
else {
|
|
380
371
|
currentSpinner?.stop();
|
|
381
|
-
|
|
372
|
+
setSpinner(null);
|
|
382
373
|
process.stderr.write(`\x1b[31m${(0, format_1.formatError)(error)}\x1b[0m\n`);
|
|
383
374
|
}
|
|
384
375
|
},
|
|
385
376
|
onKick: () => {
|
|
386
377
|
currentSpinner?.stop();
|
|
387
|
-
|
|
378
|
+
setSpinner(null);
|
|
388
379
|
if (textDirty) {
|
|
389
380
|
process.stdout.write("\n");
|
|
390
381
|
textDirty = false;
|
|
@@ -393,10 +384,18 @@ function createCliCallbacks() {
|
|
|
393
384
|
},
|
|
394
385
|
flushMarkdown: () => {
|
|
395
386
|
currentSpinner?.stop();
|
|
396
|
-
|
|
387
|
+
setSpinner(null);
|
|
388
|
+
/* v8 ignore start -- wrapper flush: tested via cli.test.ts flushMarkdown tests @preserve */
|
|
397
389
|
const remaining = streamer.flush();
|
|
398
|
-
if (remaining)
|
|
399
|
-
|
|
390
|
+
if (remaining) {
|
|
391
|
+
const wrapped = wrapper.push(remaining);
|
|
392
|
+
if (wrapped)
|
|
393
|
+
process.stdout.write(wrapped);
|
|
394
|
+
}
|
|
395
|
+
const tail = wrapper.flush();
|
|
396
|
+
if (tail)
|
|
397
|
+
process.stdout.write(tail);
|
|
398
|
+
/* v8 ignore stop */
|
|
400
399
|
},
|
|
401
400
|
};
|
|
402
401
|
}
|
|
@@ -437,29 +436,213 @@ async function* createDebouncedLines(source, debounceMs) {
|
|
|
437
436
|
yield lines.join("\n");
|
|
438
437
|
}
|
|
439
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Async queue that bridges push-based Ink input to pull-based async iteration.
|
|
441
|
+
* Input from Ink's onSubmit callback is pushed; the business logic loop awaits via for-await.
|
|
442
|
+
*/
|
|
443
|
+
class InputQueue {
|
|
444
|
+
queue = [];
|
|
445
|
+
resolve = null;
|
|
446
|
+
done = false;
|
|
447
|
+
push(input) {
|
|
448
|
+
if (this.done)
|
|
449
|
+
return;
|
|
450
|
+
if (this.resolve) {
|
|
451
|
+
const r = this.resolve;
|
|
452
|
+
this.resolve = null;
|
|
453
|
+
r({ value: input, done: false });
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
this.queue.push(input);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
close() {
|
|
460
|
+
this.done = true;
|
|
461
|
+
if (this.resolve) {
|
|
462
|
+
const r = this.resolve;
|
|
463
|
+
this.resolve = null;
|
|
464
|
+
r({ value: undefined, done: true });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
[Symbol.asyncIterator]() {
|
|
468
|
+
return {
|
|
469
|
+
next: () => {
|
|
470
|
+
if (this.queue.length > 0) {
|
|
471
|
+
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
472
|
+
}
|
|
473
|
+
if (this.done) {
|
|
474
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
475
|
+
}
|
|
476
|
+
return new Promise((resolve) => {
|
|
477
|
+
this.resolve = resolve;
|
|
478
|
+
});
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
exports.InputQueue = InputQueue;
|
|
440
484
|
async function runCliSession(options) {
|
|
441
485
|
/* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
|
|
442
|
-
const pasteDebounceMs = options.pasteDebounceMs ?? 50;
|
|
443
486
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
444
487
|
if (!options.disableCommands) {
|
|
445
488
|
(0, commands_1.registerDefaultCommands)(registry);
|
|
446
489
|
}
|
|
447
490
|
const messages = options.messages
|
|
448
491
|
?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
|
|
449
|
-
|
|
450
|
-
const
|
|
492
|
+
// ─── Rendering: TUI (Ink + Static) for TTY, imperative for tests/pipes ───
|
|
493
|
+
const useTui = !options._testInputSource && process.stdin.isTTY === true;
|
|
451
494
|
let currentAbort = null;
|
|
452
|
-
const history = [];
|
|
453
495
|
let closed = false;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
496
|
+
let cliCallbacks;
|
|
497
|
+
let tuiStore = null;
|
|
498
|
+
let inkRef = null;
|
|
499
|
+
const inputQueue = useTui ? new InputQueue() : null;
|
|
500
|
+
let rl = null;
|
|
501
|
+
if (useTui) {
|
|
502
|
+
try {
|
|
503
|
+
const [ink, React, tuiMod, storeMod] = await Promise.all([
|
|
504
|
+
Promise.resolve().then(() => __importStar(require("ink"))),
|
|
505
|
+
Promise.resolve().then(() => __importStar(require("react"))),
|
|
506
|
+
Promise.resolve().then(() => __importStar(require("./cli/ouro-tui"))),
|
|
507
|
+
Promise.resolve().then(() => __importStar(require("./cli/tui-store"))),
|
|
508
|
+
]);
|
|
509
|
+
const { OuroTui } = tuiMod;
|
|
510
|
+
const { TuiStore, createTuiCallbacks } = storeMod;
|
|
511
|
+
tuiStore = new TuiStore();
|
|
512
|
+
cliCallbacks = createTuiCallbacks(tuiStore);
|
|
513
|
+
// Seed input history from previous session (for up/down arrows) — NOT display
|
|
514
|
+
const prevUserMsgs = messages
|
|
515
|
+
.filter((msg) => msg.role === "user" && typeof msg.content === "string")
|
|
516
|
+
.map(msg => msg.content);
|
|
517
|
+
tuiStore.seedHistory(prevUserMsgs);
|
|
518
|
+
// Ctrl-C state machine (Claude Code behavior):
|
|
519
|
+
// During generation: abort current request
|
|
520
|
+
// Idle with text: clear input
|
|
521
|
+
// Idle empty (first): warn
|
|
522
|
+
// Idle empty (second): exit
|
|
523
|
+
let ctrlCWarned = false;
|
|
524
|
+
let ctrlCTimer = null;
|
|
525
|
+
const handleCtrlC = (hasInput) => {
|
|
526
|
+
if (currentAbort) {
|
|
527
|
+
currentAbort.abort();
|
|
528
|
+
ctrlCWarned = false;
|
|
529
|
+
return "abort";
|
|
530
|
+
}
|
|
531
|
+
if (hasInput) {
|
|
532
|
+
ctrlCWarned = false;
|
|
533
|
+
return "clear";
|
|
534
|
+
}
|
|
535
|
+
if (ctrlCWarned) {
|
|
536
|
+
ctrlCWarned = false;
|
|
537
|
+
if (ctrlCTimer) {
|
|
538
|
+
clearTimeout(ctrlCTimer);
|
|
539
|
+
ctrlCTimer = null;
|
|
540
|
+
}
|
|
541
|
+
closed = true;
|
|
542
|
+
inputQueue.close();
|
|
543
|
+
return "exit";
|
|
544
|
+
}
|
|
545
|
+
ctrlCWarned = true;
|
|
546
|
+
// Reset after 2 seconds — must press twice within window
|
|
547
|
+
ctrlCTimer = setTimeout(() => { ctrlCWarned = false; }, 2000);
|
|
548
|
+
return "warn";
|
|
549
|
+
};
|
|
550
|
+
// TUI root: subscribes to store, passes props to OuroTui
|
|
551
|
+
// Elapsed timer is local React state (no store.notify overhead)
|
|
552
|
+
const storeRef = tuiStore;
|
|
553
|
+
function TuiRoot() {
|
|
554
|
+
const [, forceUpdate] = React.useState(0);
|
|
555
|
+
const [elapsed, setElapsed] = React.useState(0);
|
|
556
|
+
React.useEffect(() => storeRef.subscribe(() => {
|
|
557
|
+
forceUpdate((n) => n + 1);
|
|
558
|
+
// Reset ctrlC warning on any state change (new turn, etc.)
|
|
559
|
+
}), []);
|
|
560
|
+
React.useEffect(() => {
|
|
561
|
+
const iv = setInterval(() => setElapsed(storeRef.getElapsed()), 1000);
|
|
562
|
+
return () => clearInterval(iv);
|
|
563
|
+
}, []);
|
|
564
|
+
return React.createElement(OuroTui, {
|
|
565
|
+
agentName: options.agentName,
|
|
566
|
+
model: (0, identity_1.loadAgentConfig)().humanFacing?.model ?? "",
|
|
567
|
+
completedMessages: storeRef.completedMessages,
|
|
568
|
+
inputHistory: storeRef.inputHistory,
|
|
569
|
+
live: storeRef.live,
|
|
570
|
+
elapsedSeconds: elapsed,
|
|
571
|
+
contextPercent: 0,
|
|
572
|
+
onSubmit: (text) => { ctrlCWarned = false; inputQueue.push(text); },
|
|
573
|
+
onCtrlC: handleCtrlC,
|
|
574
|
+
headerShown: storeRef.headerShown,
|
|
575
|
+
cwd: process.cwd().replace(process.env.HOME ?? "", "~"),
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
inkRef = ink.render(React.createElement(TuiRoot), { exitOnCtrlC: false, patchConsole: false });
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
// Ink failed to load (CJS compat, missing deps, etc.) — fall through to imperative
|
|
582
|
+
(0, runtime_1.emitNervesEvent)({
|
|
583
|
+
component: "senses",
|
|
584
|
+
event: "senses.tui_fallback",
|
|
585
|
+
message: `TUI failed to load, falling back to imperative: ${err instanceof Error ? err.message : String(err)}`,
|
|
586
|
+
meta: {},
|
|
587
|
+
});
|
|
588
|
+
}
|
|
461
589
|
}
|
|
462
|
-
|
|
590
|
+
// Fallback to imperative callbacks if TUI didn't initialize
|
|
591
|
+
if (!tuiStore) {
|
|
592
|
+
cliCallbacks = createCliCallbacks();
|
|
593
|
+
if (options.banner !== false) {
|
|
594
|
+
const bannerText = typeof options.banner === "string"
|
|
595
|
+
? options.banner
|
|
596
|
+
: `${options.agentName} (type /commands for help)`;
|
|
597
|
+
// eslint-disable-next-line no-console -- terminal UX: startup banner
|
|
598
|
+
console.log(`\n${bannerText}\n`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Display helpers: route to TUI store or imperative stderr/stdout
|
|
602
|
+
const display = {
|
|
603
|
+
error: (msg) => {
|
|
604
|
+
if (tuiStore)
|
|
605
|
+
tuiStore.setError(msg);
|
|
606
|
+
else
|
|
607
|
+
process.stderr.write(`\x1b[31m${msg}\x1b[0m\n`);
|
|
608
|
+
},
|
|
609
|
+
warn: (msg) => {
|
|
610
|
+
if (tuiStore)
|
|
611
|
+
tuiStore.setError(msg); // TUI shows warnings as errors (amber color handled in component)
|
|
612
|
+
else
|
|
613
|
+
process.stderr.write(`\x1b[33m${msg}\x1b[0m\n`);
|
|
614
|
+
},
|
|
615
|
+
text: (msg) => {
|
|
616
|
+
if (tuiStore)
|
|
617
|
+
tuiStore.appendText(msg);
|
|
618
|
+
else
|
|
619
|
+
process.stdout.write(`${msg}\n`);
|
|
620
|
+
},
|
|
621
|
+
suppressInput: () => {
|
|
622
|
+
if (tuiStore)
|
|
623
|
+
tuiStore.suppressInput();
|
|
624
|
+
},
|
|
625
|
+
restoreInput: () => {
|
|
626
|
+
if (tuiStore)
|
|
627
|
+
tuiStore.restoreInput();
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
const effectiveToolContext = {
|
|
631
|
+
signin: options.toolContext?.signin ?? (async () => undefined),
|
|
632
|
+
...options.toolContext,
|
|
633
|
+
codingFeedback: {
|
|
634
|
+
send: async (message) => {
|
|
635
|
+
const assistantMessage = {
|
|
636
|
+
role: "assistant",
|
|
637
|
+
content: message,
|
|
638
|
+
};
|
|
639
|
+
messages.push(assistantMessage);
|
|
640
|
+
await options.onAsyncAssistantMessage?.(messages, assistantMessage);
|
|
641
|
+
display.text(message);
|
|
642
|
+
await options.toolContext?.codingFeedback?.send(message);
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
};
|
|
463
646
|
// exitOnToolCall machinery: wrap execTool to detect target tool
|
|
464
647
|
let exitToolResult;
|
|
465
648
|
let exitToolFired = false;
|
|
@@ -470,8 +653,6 @@ async function runCliSession(options) {
|
|
|
470
653
|
if (name === options.exitOnToolCall) {
|
|
471
654
|
exitToolResult = result;
|
|
472
655
|
exitToolFired = true;
|
|
473
|
-
// Abort immediately so the model doesn't generate more output
|
|
474
|
-
// (e.g. reasoning about calling final_answer after complete_adoption)
|
|
475
656
|
currentAbort?.abort();
|
|
476
657
|
}
|
|
477
658
|
return result;
|
|
@@ -479,28 +660,6 @@ async function runCliSession(options) {
|
|
|
479
660
|
: resolvedExecTool;
|
|
480
661
|
// Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
|
|
481
662
|
const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
|
|
482
|
-
// Ctrl-C at the input prompt: clear line or warn/exit
|
|
483
|
-
rl.on("SIGINT", () => {
|
|
484
|
-
const rlInt = rl;
|
|
485
|
-
const currentLine = rlInt.line || "";
|
|
486
|
-
const result = handleSigint(rl, currentLine);
|
|
487
|
-
if (result === "clear") {
|
|
488
|
-
rlInt.line = "";
|
|
489
|
-
rlInt.cursor = 0;
|
|
490
|
-
process.stdout.write("\r\x1b[K\x1b[36m> \x1b[0m");
|
|
491
|
-
}
|
|
492
|
-
else if (result === "warn") {
|
|
493
|
-
rlInt.line = "";
|
|
494
|
-
rlInt.cursor = 0;
|
|
495
|
-
process.stdout.write("\r\x1b[K");
|
|
496
|
-
process.stderr.write("press Ctrl-C again to exit\n");
|
|
497
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
rl.close();
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
|
|
504
663
|
(0, runtime_1.emitNervesEvent)({
|
|
505
664
|
component: "senses",
|
|
506
665
|
event: "senses.cli_session_start",
|
|
@@ -513,7 +672,7 @@ async function runCliSession(options) {
|
|
|
513
672
|
if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
|
|
514
673
|
currentAbort = new AbortController();
|
|
515
674
|
const traceId = (0, nerves_1.createTraceId)();
|
|
516
|
-
|
|
675
|
+
display.suppressInput();
|
|
517
676
|
let result;
|
|
518
677
|
try {
|
|
519
678
|
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
@@ -521,115 +680,147 @@ async function runCliSession(options) {
|
|
|
521
680
|
traceId,
|
|
522
681
|
tools: options.tools,
|
|
523
682
|
execTool: wrappedExecTool,
|
|
524
|
-
toolContext:
|
|
683
|
+
toolContext: effectiveToolContext,
|
|
525
684
|
});
|
|
526
685
|
}
|
|
527
686
|
catch (err) {
|
|
528
|
-
// AbortError (Ctrl-C) -- silently continue to prompt
|
|
529
|
-
// All other errors: show the user what happened
|
|
530
687
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
531
|
-
|
|
688
|
+
display.error(err instanceof Error ? err.message : String(err));
|
|
532
689
|
}
|
|
533
690
|
}
|
|
534
691
|
cliCallbacks.flushMarkdown();
|
|
535
|
-
|
|
692
|
+
display.restoreInput();
|
|
536
693
|
currentAbort = null;
|
|
537
694
|
if (exitToolFired) {
|
|
538
695
|
exitReason = "tool_exit";
|
|
539
|
-
|
|
696
|
+
closed = true;
|
|
540
697
|
}
|
|
541
698
|
else {
|
|
542
699
|
const lastMsg = messages[messages.length - 1];
|
|
543
700
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
544
|
-
|
|
701
|
+
display.warn("(empty response)");
|
|
545
702
|
}
|
|
546
|
-
process.stdout.write("\n\n");
|
|
547
703
|
if (options.onTurnEnd) {
|
|
548
704
|
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
549
705
|
}
|
|
550
706
|
}
|
|
551
707
|
}
|
|
552
|
-
if (!exitToolFired) {
|
|
553
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
554
|
-
}
|
|
555
708
|
try {
|
|
556
|
-
for
|
|
709
|
+
// Input source: TUI queue for Ink, test source for tests, readline fallback
|
|
710
|
+
let inputSource;
|
|
711
|
+
let inputCtrl = null;
|
|
712
|
+
if (tuiStore && inputQueue) {
|
|
713
|
+
// TUI path: input comes from Ink's onSubmit → InputQueue
|
|
714
|
+
inputSource = inputQueue;
|
|
715
|
+
}
|
|
716
|
+
else if (options._testInputSource) {
|
|
717
|
+
inputSource = options._testInputSource;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// Imperative fallback: readline
|
|
721
|
+
const isTTY = process.stdin.isTTY === true;
|
|
722
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: isTTY });
|
|
723
|
+
inputCtrl = new InputController(rl);
|
|
724
|
+
inputSource = rl;
|
|
725
|
+
process.stdout.write(CLI_PROMPT);
|
|
726
|
+
}
|
|
727
|
+
for await (const input of inputSource) {
|
|
557
728
|
if (closed)
|
|
558
729
|
break;
|
|
559
|
-
if (!input.trim())
|
|
560
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
730
|
+
if (!input.trim())
|
|
561
731
|
continue;
|
|
562
|
-
}
|
|
563
732
|
// Optional input gate (e.g. trust gate in main)
|
|
564
733
|
if (options.onInput) {
|
|
565
734
|
const gate = options.onInput(input);
|
|
566
735
|
if (!gate.allowed) {
|
|
567
736
|
if (gate.reply) {
|
|
568
|
-
|
|
737
|
+
display.text(gate.reply);
|
|
569
738
|
}
|
|
570
739
|
if (closed)
|
|
571
740
|
break;
|
|
572
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
573
741
|
continue;
|
|
574
742
|
}
|
|
575
743
|
}
|
|
576
|
-
// Check for slash commands
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (dispatchResult.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
continue;
|
|
744
|
+
// Check for slash commands (legacy path only — pipeline handles commands for runTurn path)
|
|
745
|
+
if (!options.runTurn) {
|
|
746
|
+
const parsed = (0, commands_1.parseSlashCommand)(input);
|
|
747
|
+
if (parsed) {
|
|
748
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: "cli" });
|
|
749
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
750
|
+
if (dispatchResult.result.action === "exit") {
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
else if (dispatchResult.result.action === "new") {
|
|
754
|
+
messages.length = 0;
|
|
755
|
+
messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
|
|
756
|
+
await options.onNewSession?.();
|
|
757
|
+
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
758
|
+
console.log("session cleared");
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
else if (dispatchResult.result.action === "response") {
|
|
762
|
+
display.text(dispatchResult.result.message || "");
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
598
765
|
}
|
|
599
766
|
}
|
|
600
767
|
}
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
let echoRows = 0;
|
|
605
|
-
for (const line of inputLines) {
|
|
606
|
-
echoRows += Math.ceil((2 + line.length) / cols);
|
|
607
|
-
}
|
|
608
|
-
process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
|
|
609
|
-
messages.push({ role: "user", content: input });
|
|
610
|
-
addHistory(history, input);
|
|
768
|
+
// Track user message in TUI for display
|
|
769
|
+
if (tuiStore)
|
|
770
|
+
tuiStore.addUserMessage(input);
|
|
611
771
|
currentAbort = new AbortController();
|
|
612
|
-
|
|
613
|
-
|
|
772
|
+
if (tuiStore)
|
|
773
|
+
tuiStore.suppressInput();
|
|
774
|
+
inputCtrl?.suppress(() => { currentAbort?.abort(); });
|
|
614
775
|
let result;
|
|
615
776
|
try {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
777
|
+
if (options.runTurn) {
|
|
778
|
+
// Pipeline-based turn: the runTurn callback handles user message assembly,
|
|
779
|
+
// pending drain, trust gate, runAgent, postTurn, and token accumulation.
|
|
780
|
+
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext);
|
|
781
|
+
// Handle pipeline-intercepted commands with loop-control side effects
|
|
782
|
+
if (result?.turnOutcome === "command") {
|
|
783
|
+
if (result.commandAction === "exit") {
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
else if (result.commandAction === "new") {
|
|
787
|
+
messages.length = 0;
|
|
788
|
+
messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
|
|
789
|
+
await options.onNewSession?.();
|
|
790
|
+
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
791
|
+
console.log("session cleared");
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
// For "response" commands: the pipeline already emitted the response via onTextChunk
|
|
795
|
+
cliCallbacks.flushMarkdown();
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
// Legacy path: inline runAgent (used by serpent guide and tests)
|
|
801
|
+
const prefix = options.getContentPrefix?.();
|
|
802
|
+
messages.push({ role: "user", content: prefix ? `${prefix}\n\n${input}` : input });
|
|
803
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
804
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
805
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
806
|
+
traceId,
|
|
807
|
+
tools: options.tools,
|
|
808
|
+
execTool: wrappedExecTool,
|
|
809
|
+
toolContext: effectiveToolContext,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
623
812
|
}
|
|
624
813
|
catch (err) {
|
|
625
|
-
// AbortError (Ctrl-C) -- silently return to prompt
|
|
626
|
-
// All other errors: show the user what happened
|
|
627
814
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
628
|
-
|
|
815
|
+
display.error(err instanceof Error ? err.message : String(err));
|
|
629
816
|
}
|
|
630
817
|
}
|
|
631
818
|
cliCallbacks.flushMarkdown();
|
|
632
|
-
|
|
819
|
+
if (!tuiStore)
|
|
820
|
+
process.stdout.write("\n"); // ensure response ends with newline before prompt (imperative only)
|
|
821
|
+
if (tuiStore)
|
|
822
|
+
tuiStore.restoreInput();
|
|
823
|
+
inputCtrl?.restore();
|
|
633
824
|
currentAbort = null;
|
|
634
825
|
// Check if exit tool was fired during this turn
|
|
635
826
|
if (exitToolFired) {
|
|
@@ -639,20 +830,35 @@ async function runCliSession(options) {
|
|
|
639
830
|
// Safety net: never silently swallow an empty response
|
|
640
831
|
const lastMsg = messages[messages.length - 1];
|
|
641
832
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
642
|
-
|
|
833
|
+
display.warn("(empty response)");
|
|
643
834
|
}
|
|
644
|
-
process.stdout.write("\n\n");
|
|
645
835
|
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
646
836
|
if (options.onTurnEnd) {
|
|
647
837
|
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
648
838
|
}
|
|
649
839
|
if (closed)
|
|
650
840
|
break;
|
|
651
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
652
841
|
}
|
|
653
842
|
}
|
|
654
843
|
finally {
|
|
655
|
-
rl
|
|
844
|
+
rl?.close();
|
|
845
|
+
if (inkRef) {
|
|
846
|
+
// Suppress React "state update on unmounted component" warnings during cleanup.
|
|
847
|
+
// Ink's useInput hook fires after unmount — this is harmless but noisy.
|
|
848
|
+
// This also covers the SerpentGuide exitOnToolCall path, which aborts the
|
|
849
|
+
// current request and breaks out of the loop into this finally block.
|
|
850
|
+
// eslint-disable-next-line no-console -- intentional console.warn/error override for cleanup
|
|
851
|
+
const origWarn = console.warn;
|
|
852
|
+
const origError = console.error; // eslint-disable-line no-console
|
|
853
|
+
// eslint-disable-next-line no-console -- suppress React unmount warnings
|
|
854
|
+
console.warn = (...args) => { if (typeof args[0] === "string" && args[0].includes("Can't perform a React state update"))
|
|
855
|
+
return; origWarn.apply(console, args); };
|
|
856
|
+
// eslint-disable-next-line no-console -- suppress React unmount warnings
|
|
857
|
+
console.error = (...args) => { if (typeof args[0] === "string" && args[0].includes("Can't perform a React state update"))
|
|
858
|
+
return; origError.apply(console, args); };
|
|
859
|
+
inkRef.unmount();
|
|
860
|
+
setTimeout(() => { console.warn = origWarn; console.error = origError; }, 100); // eslint-disable-line no-console
|
|
861
|
+
}
|
|
656
862
|
if (options.banner !== false) {
|
|
657
863
|
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
658
864
|
console.log("bye");
|
|
@@ -665,11 +871,14 @@ async function main(agentName, options) {
|
|
|
665
871
|
if (agentName)
|
|
666
872
|
(0, identity_1.setAgentName)(agentName);
|
|
667
873
|
const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
|
|
874
|
+
// Register spinner hooks so log output clears the spinner before printing
|
|
875
|
+
(0, nerves_1.registerSpinnerHooks)(pauseActiveSpinner, resumeActiveSpinner);
|
|
668
876
|
// Fallback: apply pending updates for daemon-less direct CLI usage
|
|
669
877
|
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
878
|
+
(0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
|
|
670
879
|
await (0, update_hooks_1.applyPendingUpdates)((0, identity_1.getAgentBundlesRoot)(), (0, bundle_manifest_1.getPackageVersion)());
|
|
671
880
|
// Fail fast if provider is misconfigured (triggers human-readable error + exit)
|
|
672
|
-
(0, core_1.getProvider)();
|
|
881
|
+
(0, core_1.getProvider)("human");
|
|
673
882
|
// Resolve context kernel (identity + channel) for CLI
|
|
674
883
|
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
675
884
|
const friendStore = new store_file_1.FileFriendStore(friendsPath);
|
|
@@ -683,13 +892,6 @@ async function main(agentName, options) {
|
|
|
683
892
|
channel: "cli",
|
|
684
893
|
});
|
|
685
894
|
const resolvedContext = await resolver.resolve();
|
|
686
|
-
const cliToolContext = {
|
|
687
|
-
/* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
|
|
688
|
-
signin: async () => undefined,
|
|
689
|
-
context: resolvedContext,
|
|
690
|
-
friendStore,
|
|
691
|
-
summarize: (0, core_1.createSummarize)(),
|
|
692
|
-
};
|
|
693
895
|
const friendId = resolvedContext.friend.id;
|
|
694
896
|
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
695
897
|
(0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
|
|
@@ -712,52 +914,108 @@ async function main(agentName, options) {
|
|
|
712
914
|
}
|
|
713
915
|
// Load existing session or start fresh
|
|
714
916
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
917
|
+
let sessionState = existing?.state;
|
|
918
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
715
919
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
716
920
|
? existing.messages
|
|
717
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli",
|
|
718
|
-
//
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
for (const msg of pending) {
|
|
725
|
-
sessionMessages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
|
|
726
|
-
sessionMessages.push({ role: "assistant", content: msg.content });
|
|
727
|
-
}
|
|
728
|
-
return pending.length;
|
|
729
|
-
};
|
|
730
|
-
// Startup drain: deliver offline messages
|
|
731
|
-
const startupCount = drainToMessages();
|
|
732
|
-
if (startupCount > 0) {
|
|
733
|
-
(0, context_1.saveSession)(sessPath, sessionMessages);
|
|
734
|
-
}
|
|
921
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", { mcpManager }, resolvedContext) }];
|
|
922
|
+
// Per-turn pipeline input: CLI capabilities and pending dir
|
|
923
|
+
const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
|
|
924
|
+
const currentAgentName = (0, identity_1.getAgentName)();
|
|
925
|
+
const pendingDir = (0, pending_1.getPendingDir)(currentAgentName, friendId, "cli", "session");
|
|
926
|
+
const summarize = (0, core_1.createSummarize)("human");
|
|
927
|
+
const cliFailoverState = { pending: null };
|
|
735
928
|
try {
|
|
736
929
|
await runCliSession({
|
|
737
|
-
agentName:
|
|
930
|
+
agentName: currentAgentName,
|
|
738
931
|
pasteDebounceMs,
|
|
739
932
|
messages: sessionMessages,
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
933
|
+
_testInputSource: options?._testInputSource,
|
|
934
|
+
onAsyncAssistantMessage: async (messages, _assistantMessage) => {
|
|
935
|
+
(0, context_1.postTurn)(messages, sessPath, undefined, undefined, sessionState);
|
|
936
|
+
},
|
|
937
|
+
runTurn: async (messages, userInput, callbacks, signal, toolContext) => {
|
|
938
|
+
// Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
|
|
939
|
+
// User message passed via input.messages so the pipeline can prepend pending messages to it.
|
|
940
|
+
const failoverState = cliFailoverState;
|
|
941
|
+
// Capture terminal errors instead of displaying immediately — the failover
|
|
942
|
+
// message replaces the raw error if failover triggers successfully.
|
|
943
|
+
let capturedTerminalError = null;
|
|
944
|
+
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
945
|
+
const failoverAwareCallbacks = {
|
|
946
|
+
...callbacks,
|
|
947
|
+
onError: (error, severity) => {
|
|
948
|
+
if (severity === "terminal" && failoverState) {
|
|
949
|
+
capturedTerminalError = error;
|
|
950
|
+
callbacks.onError(new Error(""), "transient");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
callbacks.onError(error, severity);
|
|
954
|
+
},
|
|
955
|
+
};
|
|
956
|
+
/* v8 ignore stop */
|
|
957
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
958
|
+
channel: "cli",
|
|
959
|
+
sessionKey: "session",
|
|
960
|
+
capabilities: cliCapabilities,
|
|
961
|
+
messages: [{ role: "user", content: userInput }],
|
|
962
|
+
continuityIngressTexts: getCliContinuityIngressTexts(userInput),
|
|
963
|
+
callbacks: failoverAwareCallbacks,
|
|
964
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
965
|
+
sessionLoader: {
|
|
966
|
+
loadOrCreate: () => Promise.resolve({
|
|
967
|
+
messages,
|
|
968
|
+
sessionPath: sessPath,
|
|
969
|
+
state: sessionState,
|
|
970
|
+
}),
|
|
971
|
+
},
|
|
972
|
+
pendingDir,
|
|
973
|
+
friendStore,
|
|
744
974
|
provider: "local",
|
|
745
975
|
externalId: localExternalId,
|
|
746
|
-
|
|
976
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
977
|
+
drainPending: pending_1.drainPending,
|
|
978
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(currentAgentName, deferredFriendId),
|
|
979
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
980
|
+
...opts,
|
|
981
|
+
toolContext: {
|
|
982
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
983
|
+
signin: async () => undefined,
|
|
984
|
+
...opts?.toolContext,
|
|
985
|
+
summarize,
|
|
986
|
+
},
|
|
987
|
+
}),
|
|
988
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
989
|
+
(0, context_1.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
|
|
990
|
+
sessionState = state;
|
|
991
|
+
},
|
|
992
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
993
|
+
signal,
|
|
994
|
+
runAgentOptions: {
|
|
995
|
+
toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
|
|
996
|
+
traceId: (0, nerves_1.createTraceId)(),
|
|
997
|
+
mcpManager,
|
|
998
|
+
toolContext,
|
|
999
|
+
},
|
|
1000
|
+
failoverState,
|
|
747
1001
|
});
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
};
|
|
1002
|
+
/* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
|
|
1003
|
+
if (result.failoverMessage) {
|
|
1004
|
+
// Failover handled it — show the actionable message instead of the raw error
|
|
1005
|
+
process.stdout.write(`\x1b[33m${result.failoverMessage}\x1b[0m\n`);
|
|
753
1006
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1007
|
+
else if (capturedTerminalError) {
|
|
1008
|
+
// Failover didn't trigger (no failoverState, or sequence failed) — show the raw error
|
|
1009
|
+
process.stderr.write(`\x1b[31m${(0, format_1.formatError)(capturedTerminalError)}\x1b[0m\n`);
|
|
1010
|
+
}
|
|
1011
|
+
/* v8 ignore stop */
|
|
1012
|
+
// Handle gate rejection: display auto-reply if present
|
|
1013
|
+
if (!result.gateResult.allowed) {
|
|
1014
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
1015
|
+
process.stdout.write(`${result.gateResult.autoReply}\n`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return { usage: result.usage, turnOutcome: result.turnOutcome, commandAction: result.commandAction };
|
|
761
1019
|
},
|
|
762
1020
|
onNewSession: () => {
|
|
763
1021
|
(0, context_1.deleteSession)(sessPath);
|
|
@@ -768,3 +1026,4 @@ async function main(agentName, options) {
|
|
|
768
1026
|
sessionLock?.release();
|
|
769
1027
|
}
|
|
770
1028
|
}
|
|
1029
|
+
// CI trigger
|