@ouro.bot/cli 0.1.0-alpha.25 → 0.1.0-alpha.250
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 +1507 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +140 -0
- package/dist/arc/episodes.js +117 -0
- package/dist/arc/intentions.js +133 -0
- package/dist/arc/json-store.js +117 -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 +304 -0
- package/dist/heart/config.js +63 -30
- package/dist/heart/core.js +644 -196
- 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 +145 -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 +1875 -0
- package/dist/heart/daemon/cli-parse.js +759 -0
- package/dist/heart/daemon/cli-render.js +464 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/daemon-cli.js +29 -1179
- 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 +491 -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 +202 -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 +30 -120
- 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 +157 -55
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/mcp/mcp-server.js +651 -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 +169 -46
- package/dist/heart/providers/azure.js +98 -11
- package/dist/heart/providers/error-classification.js +63 -0
- package/dist/heart/providers/github-copilot.js +136 -0
- package/dist/heart/providers/minimax.js +15 -4
- package/dist/heart/providers/openai-codex.js +33 -22
- 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 +277 -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}/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} +68 -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/resolver.js +37 -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 +762 -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 +50 -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 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +319 -0
- package/dist/repertoire/bw-installer.js +79 -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/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +515 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +369 -0
- package/dist/repertoire/mcp-client.js +255 -0
- package/dist/repertoire/mcp-manager.js +303 -0
- package/dist/repertoire/mcp-tools.js +63 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +14 -23
- package/dist/repertoire/stripe-client.js +131 -0
- 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 +43 -771
- package/dist/repertoire/tools-bluebubbles.js +94 -0
- package/dist/repertoire/tools-bridge.js +141 -0
- package/dist/repertoire/tools-config.js +185 -0
- package/dist/repertoire/tools-continuity.js +248 -0
- package/dist/repertoire/tools-credential.js +182 -0
- package/dist/repertoire/tools-files.js +342 -0
- package/dist/repertoire/tools-flight.js +224 -0
- package/dist/repertoire/tools-flow.js +62 -0
- package/dist/repertoire/tools-github.js +1 -7
- 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-stripe.js +180 -0
- package/dist/repertoire/tools-surface.js +243 -0
- package/dist/repertoire/tools-teams.js +12 -62
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-user-profile.js +144 -0
- package/dist/repertoire/tools-vault.js +110 -0
- package/dist/repertoire/tools.js +147 -130
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/user-profile.js +118 -0
- package/dist/repertoire/vault-setup.js +241 -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/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 +393 -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 +236 -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 +550 -246
- 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 +593 -85
- package/dist/senses/pipeline.js +552 -0
- package/dist/senses/shared-turn.js +199 -0
- package/dist/senses/surface-tool.js +68 -0
- package/dist/senses/teams.js +664 -165
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +14 -7
- package/skills/agent-commerce.md +106 -0
- package/skills/browser-navigation.md +110 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -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/heart/{daemon → versioning}/update-hooks.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,77 @@ 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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
/* v8 ignore start -- cosmetic time formatting @preserve */
|
|
102
|
+
function formatTimeAgo(date) {
|
|
103
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
104
|
+
if (seconds < 60)
|
|
105
|
+
return "just now";
|
|
106
|
+
const minutes = Math.floor(seconds / 60);
|
|
107
|
+
if (minutes < 60)
|
|
108
|
+
return `${minutes}m ago`;
|
|
109
|
+
const hours = Math.floor(minutes / 60);
|
|
110
|
+
if (hours < 24)
|
|
111
|
+
return `${hours}h ago`;
|
|
112
|
+
const days = Math.floor(hours / 24);
|
|
113
|
+
return `${days}d ago`;
|
|
114
|
+
}
|
|
115
|
+
const CLI_PROMPT = "\x1b[36m) \x1b[0m";
|
|
116
|
+
function writeCliAsyncAssistantMessage(rl, message, stdout = process.stdout) {
|
|
117
|
+
const rlInt = rl;
|
|
118
|
+
const currentLine = rlInt.line ?? "";
|
|
119
|
+
const currentCursor = rlInt.cursor ?? currentLine.length;
|
|
120
|
+
stdout.write("\r\x1b[K");
|
|
121
|
+
stdout.write(`${renderMarkdown(message)}\n`);
|
|
122
|
+
stdout.write(CLI_PROMPT);
|
|
123
|
+
if (!currentLine)
|
|
124
|
+
return;
|
|
125
|
+
stdout.write(currentLine);
|
|
126
|
+
if (currentCursor < currentLine.length) {
|
|
127
|
+
readline.cursorTo(process.stdout, 2 + currentCursor);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
|
-
|
|
130
|
+
// Module-level active spinner for log coordination.
|
|
131
|
+
// The terminal log sink calls these to avoid interleaving with spinner output.
|
|
132
|
+
let _activeSpinner = null;
|
|
133
|
+
/* v8 ignore start -- spinner coordination: exercised at runtime, not unit-testable without real terminal @preserve */
|
|
134
|
+
function pauseActiveSpinner() { _activeSpinner?.pause(); }
|
|
135
|
+
function resumeActiveSpinner() { _activeSpinner?.resume(); }
|
|
136
|
+
/* v8 ignore stop */
|
|
137
|
+
function setActiveSpinner(s) { _activeSpinner = s; }
|
|
138
|
+
// Re-export ImperativeSpinner as Spinner for backward compatibility
|
|
139
|
+
var spinner_imperative_2 = require("./cli/spinner-imperative");
|
|
140
|
+
Object.defineProperty(exports, "Spinner", { enumerable: true, get: function () { return spinner_imperative_2.ImperativeSpinner; } });
|
|
130
141
|
// Input controller: pauses readline during model/tool execution.
|
|
131
142
|
// Does NOT touch raw mode — readline with terminal:true manages raw mode
|
|
132
143
|
// internally. Touching it causes ^C to be echoed by the terminal driver.
|
|
@@ -294,59 +305,55 @@ function createCliCallbacks() {
|
|
|
294
305
|
meta: {},
|
|
295
306
|
});
|
|
296
307
|
let currentSpinner = null;
|
|
297
|
-
|
|
308
|
+
function setSpinner(s) { currentSpinner = s; setActiveSpinner(s); }
|
|
298
309
|
let hadToolRun = false;
|
|
299
310
|
let textDirty = false; // true when text/reasoning was written without a trailing newline
|
|
300
311
|
const streamer = new MarkdownStreamer();
|
|
312
|
+
const wrapper = new cli_layout_1.StreamingWordWrapper();
|
|
301
313
|
return {
|
|
302
314
|
onModelStart: () => {
|
|
303
315
|
currentSpinner?.stop();
|
|
304
|
-
|
|
305
|
-
hadReasoning = false;
|
|
316
|
+
setSpinner(null);
|
|
306
317
|
textDirty = false;
|
|
307
318
|
streamer.reset();
|
|
319
|
+
wrapper.reset();
|
|
308
320
|
const phrases = (0, phrases_1.getPhrases)();
|
|
309
321
|
const pool = hadToolRun ? phrases.followup : phrases.thinking;
|
|
310
322
|
const first = (0, phrases_1.pickPhrase)(pool);
|
|
311
|
-
|
|
323
|
+
setSpinner(new spinner_imperative_1.ImperativeSpinner(first, pool));
|
|
312
324
|
currentSpinner.start();
|
|
313
325
|
},
|
|
314
326
|
onModelStreamStart: () => {
|
|
315
327
|
// No-op: content callbacks (onTextChunk, onReasoningChunk) handle
|
|
316
328
|
// stopping the spinner. onModelStreamStart fires too early and
|
|
317
|
-
// doesn't fire at all for
|
|
329
|
+
// doesn't fire at all for settle tool streaming.
|
|
318
330
|
},
|
|
319
331
|
onClearText: () => {
|
|
320
332
|
streamer.reset();
|
|
333
|
+
wrapper.reset();
|
|
321
334
|
},
|
|
322
335
|
onTextChunk: (text) => {
|
|
323
|
-
// Stop spinner if still running —
|
|
336
|
+
// Stop spinner if still running — settle streaming and Anthropic
|
|
324
337
|
// tool-only responses bypass onModelStreamStart, so the spinner would
|
|
325
338
|
// otherwise keep running (and its \r writes overwrite response text).
|
|
326
339
|
if (currentSpinner) {
|
|
327
340
|
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;
|
|
341
|
+
setSpinner(null);
|
|
335
342
|
}
|
|
336
343
|
const rendered = streamer.push(text);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (currentSpinner) {
|
|
343
|
-
currentSpinner.stop();
|
|
344
|
-
currentSpinner = null;
|
|
344
|
+
/* v8 ignore start -- wrapper integration: tested via cli.test.ts onTextChunk tests @preserve */
|
|
345
|
+
if (rendered) {
|
|
346
|
+
const wrapped = wrapper.push(rendered);
|
|
347
|
+
if (wrapped)
|
|
348
|
+
process.stdout.write(wrapped);
|
|
345
349
|
}
|
|
346
|
-
|
|
347
|
-
process.stdout.write(`\x1b[2m${text}\x1b[0m`);
|
|
350
|
+
/* v8 ignore stop */
|
|
348
351
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
349
352
|
},
|
|
353
|
+
onReasoningChunk: (_text) => {
|
|
354
|
+
// Keep reasoning private in the CLI surface. The spinner continues to
|
|
355
|
+
// represent active thinking until actual tool or answer output arrives.
|
|
356
|
+
},
|
|
350
357
|
onToolStart: (_name, _args) => {
|
|
351
358
|
// Stop the model-start spinner: when the model returns only tool calls
|
|
352
359
|
// (no content/reasoning), onModelStreamStart never fires, so the old
|
|
@@ -360,31 +367,29 @@ function createCliCallbacks() {
|
|
|
360
367
|
}
|
|
361
368
|
const toolPhrases = (0, phrases_1.getPhrases)().tool;
|
|
362
369
|
const first = (0, phrases_1.pickPhrase)(toolPhrases);
|
|
363
|
-
|
|
370
|
+
setSpinner(new spinner_imperative_1.ImperativeSpinner(first, toolPhrases));
|
|
364
371
|
currentSpinner.start();
|
|
365
372
|
hadToolRun = true;
|
|
366
373
|
},
|
|
367
374
|
onToolEnd: (name, argSummary, success) => {
|
|
368
375
|
currentSpinner?.stop();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const color = success ? "\x1b[32m" : "\x1b[31m";
|
|
372
|
-
process.stderr.write(`${color}${msg}\x1b[0m\n`);
|
|
376
|
+
setSpinner(null);
|
|
377
|
+
(0, tool_display_1.writeToolEnd)(name, argSummary, success);
|
|
373
378
|
},
|
|
374
379
|
onError: (error, severity) => {
|
|
375
380
|
if (severity === "transient") {
|
|
376
381
|
currentSpinner?.fail(error.message);
|
|
377
|
-
|
|
382
|
+
setSpinner(null);
|
|
378
383
|
}
|
|
379
384
|
else {
|
|
380
385
|
currentSpinner?.stop();
|
|
381
|
-
|
|
386
|
+
setSpinner(null);
|
|
382
387
|
process.stderr.write(`\x1b[31m${(0, format_1.formatError)(error)}\x1b[0m\n`);
|
|
383
388
|
}
|
|
384
389
|
},
|
|
385
390
|
onKick: () => {
|
|
386
391
|
currentSpinner?.stop();
|
|
387
|
-
|
|
392
|
+
setSpinner(null);
|
|
388
393
|
if (textDirty) {
|
|
389
394
|
process.stdout.write("\n");
|
|
390
395
|
textDirty = false;
|
|
@@ -393,10 +398,18 @@ function createCliCallbacks() {
|
|
|
393
398
|
},
|
|
394
399
|
flushMarkdown: () => {
|
|
395
400
|
currentSpinner?.stop();
|
|
396
|
-
|
|
401
|
+
setSpinner(null);
|
|
402
|
+
/* v8 ignore start -- wrapper flush: tested via cli.test.ts flushMarkdown tests @preserve */
|
|
397
403
|
const remaining = streamer.flush();
|
|
398
|
-
if (remaining)
|
|
399
|
-
|
|
404
|
+
if (remaining) {
|
|
405
|
+
const wrapped = wrapper.push(remaining);
|
|
406
|
+
if (wrapped)
|
|
407
|
+
process.stdout.write(wrapped);
|
|
408
|
+
}
|
|
409
|
+
const tail = wrapper.flush();
|
|
410
|
+
if (tail)
|
|
411
|
+
process.stdout.write(tail);
|
|
412
|
+
/* v8 ignore stop */
|
|
400
413
|
},
|
|
401
414
|
};
|
|
402
415
|
}
|
|
@@ -437,29 +450,236 @@ async function* createDebouncedLines(source, debounceMs) {
|
|
|
437
450
|
yield lines.join("\n");
|
|
438
451
|
}
|
|
439
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Async queue that bridges push-based Ink input to pull-based async iteration.
|
|
455
|
+
* Input from Ink's onSubmit callback is pushed; the business logic loop awaits via for-await.
|
|
456
|
+
*/
|
|
457
|
+
class InputQueue {
|
|
458
|
+
queue = [];
|
|
459
|
+
resolve = null;
|
|
460
|
+
done = false;
|
|
461
|
+
push(input) {
|
|
462
|
+
if (this.done)
|
|
463
|
+
return;
|
|
464
|
+
if (this.resolve) {
|
|
465
|
+
const r = this.resolve;
|
|
466
|
+
this.resolve = null;
|
|
467
|
+
r({ value: input, done: false });
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
this.queue.push(input);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
close() {
|
|
474
|
+
this.done = true;
|
|
475
|
+
if (this.resolve) {
|
|
476
|
+
const r = this.resolve;
|
|
477
|
+
this.resolve = null;
|
|
478
|
+
r({ value: undefined, done: true });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/** Drain all buffered items, leaving any pending async awaiter untouched. */
|
|
482
|
+
drainAll() {
|
|
483
|
+
const items = [...this.queue];
|
|
484
|
+
this.queue = [];
|
|
485
|
+
return items;
|
|
486
|
+
}
|
|
487
|
+
[Symbol.asyncIterator]() {
|
|
488
|
+
return {
|
|
489
|
+
next: () => {
|
|
490
|
+
if (this.queue.length > 0) {
|
|
491
|
+
return Promise.resolve({ value: this.queue.shift(), done: false });
|
|
492
|
+
}
|
|
493
|
+
if (this.done) {
|
|
494
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
495
|
+
}
|
|
496
|
+
return new Promise((resolve) => {
|
|
497
|
+
this.resolve = resolve;
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
exports.InputQueue = InputQueue;
|
|
440
504
|
async function runCliSession(options) {
|
|
441
505
|
/* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
|
|
442
|
-
const pasteDebounceMs = options.pasteDebounceMs ?? 50;
|
|
443
506
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
444
507
|
if (!options.disableCommands) {
|
|
445
508
|
(0, commands_1.registerDefaultCommands)(registry);
|
|
446
509
|
}
|
|
447
510
|
const messages = options.messages
|
|
448
511
|
?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
|
|
449
|
-
|
|
450
|
-
const
|
|
512
|
+
// ─── Rendering: TUI (Ink + Static) for TTY, imperative for tests/pipes ───
|
|
513
|
+
const useTui = !options._testInputSource && process.stdin.isTTY === true;
|
|
451
514
|
let currentAbort = null;
|
|
452
|
-
const history = [];
|
|
453
515
|
let closed = false;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
516
|
+
let cliCallbacks;
|
|
517
|
+
let tuiStore = null;
|
|
518
|
+
let inkRef = null;
|
|
519
|
+
const inputQueue = useTui ? new InputQueue() : null;
|
|
520
|
+
let rl = null;
|
|
521
|
+
if (useTui) {
|
|
522
|
+
try {
|
|
523
|
+
const [ink, React, tuiMod, storeMod] = await Promise.all([
|
|
524
|
+
Promise.resolve().then(() => __importStar(require("ink"))),
|
|
525
|
+
Promise.resolve().then(() => __importStar(require("react"))),
|
|
526
|
+
Promise.resolve().then(() => __importStar(require("./cli/ouro-tui"))),
|
|
527
|
+
Promise.resolve().then(() => __importStar(require("./cli/tui-store"))),
|
|
528
|
+
]);
|
|
529
|
+
const { OuroTui } = tuiMod;
|
|
530
|
+
const { TuiStore, createTuiCallbacks } = storeMod;
|
|
531
|
+
tuiStore = new TuiStore();
|
|
532
|
+
cliCallbacks = createTuiCallbacks(tuiStore);
|
|
533
|
+
// Seed input history from previous session (for up/down arrows) — NOT display
|
|
534
|
+
const prevUserMsgs = messages
|
|
535
|
+
.filter((msg) => msg.role === "user" && typeof msg.content === "string")
|
|
536
|
+
.map(msg => msg.content);
|
|
537
|
+
tuiStore.seedHistory(prevUserMsgs);
|
|
538
|
+
// Show session resume context: summary + last few exchanges (dimmed)
|
|
539
|
+
/* v8 ignore start -- session resume display: integration-only, tested visually @preserve */
|
|
540
|
+
if (messages.length > 1) {
|
|
541
|
+
const userAssistantMsgs = messages.filter((m) => (m.role === "user" || m.role === "assistant") && typeof m.content === "string" && m.content.trim().length > 0);
|
|
542
|
+
// Extract last 2 exchanges (up to 4 messages)
|
|
543
|
+
const lastExchanges = [];
|
|
544
|
+
for (let i = userAssistantMsgs.length - 1; i >= 0 && lastExchanges.length < 4; i--) {
|
|
545
|
+
lastExchanges.unshift({ role: userAssistantMsgs[i].role, content: userAssistantMsgs[i].content });
|
|
546
|
+
}
|
|
547
|
+
const msgCount = messages.filter(m => m.role === "user" || m.role === "assistant").length;
|
|
548
|
+
const timeAgo = options.lastActivityAt ? formatTimeAgo(new Date(options.lastActivityAt)) : null;
|
|
549
|
+
const summary = ` resuming session (${msgCount} messages${timeAgo ? ` · last active ${timeAgo}` : ""})`;
|
|
550
|
+
tuiStore.addSessionHistory(summary, lastExchanges);
|
|
551
|
+
}
|
|
552
|
+
/* v8 ignore stop */
|
|
553
|
+
// Ctrl-C state machine (Claude Code behavior):
|
|
554
|
+
// During generation: abort current request
|
|
555
|
+
// Idle with text: clear input
|
|
556
|
+
// Idle empty (first): warn
|
|
557
|
+
// Idle empty (second): exit
|
|
558
|
+
let ctrlCWarned = false;
|
|
559
|
+
let ctrlCTimer = null;
|
|
560
|
+
const handleCtrlC = (hasInput) => {
|
|
561
|
+
if (currentAbort) {
|
|
562
|
+
currentAbort.abort();
|
|
563
|
+
ctrlCWarned = false;
|
|
564
|
+
return "abort";
|
|
565
|
+
}
|
|
566
|
+
if (hasInput) {
|
|
567
|
+
ctrlCWarned = false;
|
|
568
|
+
return "clear";
|
|
569
|
+
}
|
|
570
|
+
if (ctrlCWarned) {
|
|
571
|
+
ctrlCWarned = false;
|
|
572
|
+
if (ctrlCTimer) {
|
|
573
|
+
clearTimeout(ctrlCTimer);
|
|
574
|
+
ctrlCTimer = null;
|
|
575
|
+
}
|
|
576
|
+
closed = true;
|
|
577
|
+
inputQueue.close();
|
|
578
|
+
return "exit";
|
|
579
|
+
}
|
|
580
|
+
ctrlCWarned = true;
|
|
581
|
+
// Reset after 2 seconds — must press twice within window
|
|
582
|
+
ctrlCTimer = setTimeout(() => { ctrlCWarned = false; }, 2000);
|
|
583
|
+
return "warn";
|
|
584
|
+
};
|
|
585
|
+
// TUI root: subscribes to store, passes props to OuroTui
|
|
586
|
+
// Elapsed timer is local React state (no store.notify overhead)
|
|
587
|
+
const storeRef = tuiStore;
|
|
588
|
+
function TuiRoot() {
|
|
589
|
+
const [, forceUpdate] = React.useState(0);
|
|
590
|
+
const [elapsed, setElapsed] = React.useState(0);
|
|
591
|
+
React.useEffect(() => storeRef.subscribe(() => {
|
|
592
|
+
forceUpdate((n) => n + 1);
|
|
593
|
+
// Reset ctrlC warning on any state change (new turn, etc.)
|
|
594
|
+
}), []);
|
|
595
|
+
React.useEffect(() => {
|
|
596
|
+
const iv = setInterval(() => setElapsed(storeRef.getElapsed()), 1000);
|
|
597
|
+
return () => clearInterval(iv);
|
|
598
|
+
}, []);
|
|
599
|
+
return React.createElement(OuroTui, {
|
|
600
|
+
agentName: options.agentName,
|
|
601
|
+
model: (0, identity_1.loadAgentConfig)().humanFacing?.model ?? "",
|
|
602
|
+
completedMessages: storeRef.completedMessages,
|
|
603
|
+
inputHistory: storeRef.inputHistory,
|
|
604
|
+
queuedInputs: storeRef.queuedInputs,
|
|
605
|
+
live: storeRef.live,
|
|
606
|
+
elapsedSeconds: elapsed,
|
|
607
|
+
contextPercent: 0,
|
|
608
|
+
onSubmit: (text) => { ctrlCWarned = false; inputQueue.push(text); storeRef.enqueueInput(text); },
|
|
609
|
+
onCtrlC: handleCtrlC,
|
|
610
|
+
onPopQueue: () => { const items = storeRef.popAllQueuedForEditing(); inputQueue.drainAll(); return items; },
|
|
611
|
+
headerShown: storeRef.headerShown,
|
|
612
|
+
cwd: process.cwd().replace(process.env.HOME ?? "", "~"),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
inkRef = ink.render(React.createElement(TuiRoot), { exitOnCtrlC: false, patchConsole: false });
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
// Ink failed to load (CJS compat, missing deps, etc.) — fall through to imperative
|
|
619
|
+
(0, runtime_1.emitNervesEvent)({
|
|
620
|
+
component: "senses",
|
|
621
|
+
event: "senses.tui_fallback",
|
|
622
|
+
message: `TUI failed to load, falling back to imperative: ${err instanceof Error ? err.message : String(err)}`,
|
|
623
|
+
meta: {},
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Fallback to imperative callbacks if TUI didn't initialize
|
|
628
|
+
if (!tuiStore) {
|
|
629
|
+
cliCallbacks = createCliCallbacks();
|
|
630
|
+
if (options.banner !== false) {
|
|
631
|
+
const bannerText = typeof options.banner === "string"
|
|
632
|
+
? options.banner
|
|
633
|
+
: `${options.agentName} (type /commands for help)`;
|
|
634
|
+
// eslint-disable-next-line no-console -- terminal UX: startup banner
|
|
635
|
+
console.log(`\n${bannerText}\n`);
|
|
636
|
+
}
|
|
461
637
|
}
|
|
462
|
-
|
|
638
|
+
// Display helpers: route to TUI store or imperative stderr/stdout
|
|
639
|
+
const display = {
|
|
640
|
+
error: (msg) => {
|
|
641
|
+
if (tuiStore)
|
|
642
|
+
tuiStore.setError(msg);
|
|
643
|
+
else
|
|
644
|
+
process.stderr.write(`\x1b[31m${msg}\x1b[0m\n`);
|
|
645
|
+
},
|
|
646
|
+
warn: (msg) => {
|
|
647
|
+
if (tuiStore)
|
|
648
|
+
tuiStore.setError(msg); // TUI shows warnings as errors (amber color handled in component)
|
|
649
|
+
else
|
|
650
|
+
process.stderr.write(`\x1b[33m${msg}\x1b[0m\n`);
|
|
651
|
+
},
|
|
652
|
+
text: (msg) => {
|
|
653
|
+
if (tuiStore)
|
|
654
|
+
tuiStore.appendText(msg);
|
|
655
|
+
else
|
|
656
|
+
process.stdout.write(`${msg}\n`);
|
|
657
|
+
},
|
|
658
|
+
suppressInput: () => {
|
|
659
|
+
if (tuiStore)
|
|
660
|
+
tuiStore.suppressInput();
|
|
661
|
+
},
|
|
662
|
+
restoreInput: () => {
|
|
663
|
+
if (tuiStore)
|
|
664
|
+
tuiStore.restoreInput();
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
const effectiveToolContext = {
|
|
668
|
+
signin: options.toolContext?.signin ?? (async () => undefined),
|
|
669
|
+
...options.toolContext,
|
|
670
|
+
codingFeedback: {
|
|
671
|
+
send: async (message) => {
|
|
672
|
+
const assistantMessage = {
|
|
673
|
+
role: "assistant",
|
|
674
|
+
content: message,
|
|
675
|
+
};
|
|
676
|
+
messages.push(assistantMessage);
|
|
677
|
+
await options.onAsyncAssistantMessage?.(messages, assistantMessage);
|
|
678
|
+
display.text(message);
|
|
679
|
+
await options.toolContext?.codingFeedback?.send(message);
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
};
|
|
463
683
|
// exitOnToolCall machinery: wrap execTool to detect target tool
|
|
464
684
|
let exitToolResult;
|
|
465
685
|
let exitToolFired = false;
|
|
@@ -470,8 +690,6 @@ async function runCliSession(options) {
|
|
|
470
690
|
if (name === options.exitOnToolCall) {
|
|
471
691
|
exitToolResult = result;
|
|
472
692
|
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
693
|
currentAbort?.abort();
|
|
476
694
|
}
|
|
477
695
|
return result;
|
|
@@ -479,28 +697,6 @@ async function runCliSession(options) {
|
|
|
479
697
|
: resolvedExecTool;
|
|
480
698
|
// Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
|
|
481
699
|
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
700
|
(0, runtime_1.emitNervesEvent)({
|
|
505
701
|
component: "senses",
|
|
506
702
|
event: "senses.cli_session_start",
|
|
@@ -513,7 +709,7 @@ async function runCliSession(options) {
|
|
|
513
709
|
if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
|
|
514
710
|
currentAbort = new AbortController();
|
|
515
711
|
const traceId = (0, nerves_1.createTraceId)();
|
|
516
|
-
|
|
712
|
+
display.suppressInput();
|
|
517
713
|
let result;
|
|
518
714
|
try {
|
|
519
715
|
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
@@ -521,115 +717,149 @@ async function runCliSession(options) {
|
|
|
521
717
|
traceId,
|
|
522
718
|
tools: options.tools,
|
|
523
719
|
execTool: wrappedExecTool,
|
|
524
|
-
toolContext:
|
|
720
|
+
toolContext: effectiveToolContext,
|
|
525
721
|
});
|
|
526
722
|
}
|
|
527
723
|
catch (err) {
|
|
528
|
-
// AbortError (Ctrl-C) -- silently continue to prompt
|
|
529
|
-
// All other errors: show the user what happened
|
|
530
724
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
531
|
-
|
|
725
|
+
display.error(err instanceof Error ? err.message : String(err));
|
|
532
726
|
}
|
|
533
727
|
}
|
|
534
728
|
cliCallbacks.flushMarkdown();
|
|
535
|
-
|
|
729
|
+
display.restoreInput();
|
|
536
730
|
currentAbort = null;
|
|
537
731
|
if (exitToolFired) {
|
|
538
732
|
exitReason = "tool_exit";
|
|
539
|
-
|
|
733
|
+
closed = true;
|
|
540
734
|
}
|
|
541
735
|
else {
|
|
542
736
|
const lastMsg = messages[messages.length - 1];
|
|
543
737
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
544
|
-
|
|
738
|
+
display.warn("(empty response)");
|
|
545
739
|
}
|
|
546
|
-
process.stdout.write("\n\n");
|
|
547
740
|
if (options.onTurnEnd) {
|
|
548
741
|
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
549
742
|
}
|
|
550
743
|
}
|
|
551
744
|
}
|
|
552
|
-
if (!exitToolFired) {
|
|
553
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
554
|
-
}
|
|
555
745
|
try {
|
|
556
|
-
for
|
|
746
|
+
// Input source: TUI queue for Ink, test source for tests, readline fallback
|
|
747
|
+
let inputSource;
|
|
748
|
+
let inputCtrl = null;
|
|
749
|
+
if (tuiStore && inputQueue) {
|
|
750
|
+
// TUI path: input comes from Ink's onSubmit → InputQueue
|
|
751
|
+
inputSource = inputQueue;
|
|
752
|
+
}
|
|
753
|
+
else if (options._testInputSource) {
|
|
754
|
+
inputSource = options._testInputSource;
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
// Imperative fallback: readline
|
|
758
|
+
const isTTY = process.stdin.isTTY === true;
|
|
759
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: isTTY });
|
|
760
|
+
inputCtrl = new InputController(rl);
|
|
761
|
+
inputSource = rl;
|
|
762
|
+
process.stdout.write(CLI_PROMPT);
|
|
763
|
+
}
|
|
764
|
+
for await (const input of inputSource) {
|
|
557
765
|
if (closed)
|
|
558
766
|
break;
|
|
559
|
-
if (!input.trim())
|
|
560
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
767
|
+
if (!input.trim())
|
|
561
768
|
continue;
|
|
562
|
-
|
|
769
|
+
// Remove from TUI queue display as the agent picks up this message
|
|
770
|
+
tuiStore?.dequeueInput(input);
|
|
563
771
|
// Optional input gate (e.g. trust gate in main)
|
|
564
772
|
if (options.onInput) {
|
|
565
773
|
const gate = options.onInput(input);
|
|
566
774
|
if (!gate.allowed) {
|
|
567
775
|
if (gate.reply) {
|
|
568
|
-
|
|
776
|
+
display.text(gate.reply);
|
|
569
777
|
}
|
|
570
778
|
if (closed)
|
|
571
779
|
break;
|
|
572
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
573
780
|
continue;
|
|
574
781
|
}
|
|
575
782
|
}
|
|
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;
|
|
783
|
+
// Check for slash commands (legacy path only — pipeline handles commands for runTurn path)
|
|
784
|
+
if (!options.runTurn) {
|
|
785
|
+
const parsed = (0, commands_1.parseSlashCommand)(input);
|
|
786
|
+
if (parsed) {
|
|
787
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: "cli" });
|
|
788
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
789
|
+
if (dispatchResult.result.action === "exit") {
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
else if (dispatchResult.result.action === "new") {
|
|
793
|
+
messages.length = 0;
|
|
794
|
+
messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
|
|
795
|
+
await options.onNewSession?.();
|
|
796
|
+
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
797
|
+
console.log("session cleared");
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
else if (dispatchResult.result.action === "response") {
|
|
801
|
+
display.text(dispatchResult.result.message || "");
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
598
804
|
}
|
|
599
805
|
}
|
|
600
806
|
}
|
|
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);
|
|
807
|
+
// Track user message in TUI for display
|
|
808
|
+
if (tuiStore)
|
|
809
|
+
tuiStore.addUserMessage(input);
|
|
611
810
|
currentAbort = new AbortController();
|
|
612
|
-
|
|
613
|
-
|
|
811
|
+
if (tuiStore)
|
|
812
|
+
tuiStore.suppressInput();
|
|
813
|
+
inputCtrl?.suppress(() => { currentAbort?.abort(); });
|
|
614
814
|
let result;
|
|
615
815
|
try {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
816
|
+
if (options.runTurn) {
|
|
817
|
+
// Pipeline-based turn: the runTurn callback handles user message assembly,
|
|
818
|
+
// pending drain, trust gate, runAgent, postTurn, and token accumulation.
|
|
819
|
+
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext);
|
|
820
|
+
// Handle pipeline-intercepted commands with loop-control side effects
|
|
821
|
+
if (result?.turnOutcome === "command") {
|
|
822
|
+
if (result.commandAction === "exit") {
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
else if (result.commandAction === "new") {
|
|
826
|
+
messages.length = 0;
|
|
827
|
+
messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
|
|
828
|
+
await options.onNewSession?.();
|
|
829
|
+
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
830
|
+
console.log("session cleared");
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
// For "response" commands: the pipeline already emitted the response via onTextChunk
|
|
834
|
+
cliCallbacks.flushMarkdown();
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
// Legacy path: inline runAgent (used by serpent guide and tests)
|
|
840
|
+
const prefix = options.getContentPrefix?.();
|
|
841
|
+
messages.push({ role: "user", content: prefix ? `${prefix}\n\n${input}` : input });
|
|
842
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
843
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
844
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
845
|
+
traceId,
|
|
846
|
+
tools: options.tools,
|
|
847
|
+
execTool: wrappedExecTool,
|
|
848
|
+
toolContext: effectiveToolContext,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
623
851
|
}
|
|
624
852
|
catch (err) {
|
|
625
|
-
// AbortError (Ctrl-C) -- silently return to prompt
|
|
626
|
-
// All other errors: show the user what happened
|
|
627
853
|
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
628
|
-
|
|
854
|
+
display.error(err instanceof Error ? err.message : String(err));
|
|
629
855
|
}
|
|
630
856
|
}
|
|
631
857
|
cliCallbacks.flushMarkdown();
|
|
632
|
-
|
|
858
|
+
if (!tuiStore)
|
|
859
|
+
process.stdout.write("\n"); // ensure response ends with newline before prompt (imperative only)
|
|
860
|
+
if (tuiStore)
|
|
861
|
+
tuiStore.restoreInput();
|
|
862
|
+
inputCtrl?.restore();
|
|
633
863
|
currentAbort = null;
|
|
634
864
|
// Check if exit tool was fired during this turn
|
|
635
865
|
if (exitToolFired) {
|
|
@@ -639,20 +869,35 @@ async function runCliSession(options) {
|
|
|
639
869
|
// Safety net: never silently swallow an empty response
|
|
640
870
|
const lastMsg = messages[messages.length - 1];
|
|
641
871
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
642
|
-
|
|
872
|
+
display.warn("(empty response)");
|
|
643
873
|
}
|
|
644
|
-
process.stdout.write("\n\n");
|
|
645
874
|
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
646
875
|
if (options.onTurnEnd) {
|
|
647
876
|
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
648
877
|
}
|
|
649
878
|
if (closed)
|
|
650
879
|
break;
|
|
651
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
652
880
|
}
|
|
653
881
|
}
|
|
654
882
|
finally {
|
|
655
|
-
rl
|
|
883
|
+
rl?.close();
|
|
884
|
+
if (inkRef) {
|
|
885
|
+
// Suppress React "state update on unmounted component" warnings during cleanup.
|
|
886
|
+
// Ink's useInput hook fires after unmount — this is harmless but noisy.
|
|
887
|
+
// This also covers the SerpentGuide exitOnToolCall path, which aborts the
|
|
888
|
+
// current request and breaks out of the loop into this finally block.
|
|
889
|
+
// eslint-disable-next-line no-console -- intentional console.warn/error override for cleanup
|
|
890
|
+
const origWarn = console.warn;
|
|
891
|
+
const origError = console.error; // eslint-disable-line no-console
|
|
892
|
+
// eslint-disable-next-line no-console -- suppress React unmount warnings
|
|
893
|
+
console.warn = (...args) => { if (typeof args[0] === "string" && args[0].includes("Can't perform a React state update"))
|
|
894
|
+
return; origWarn.apply(console, args); };
|
|
895
|
+
// eslint-disable-next-line no-console -- suppress React unmount warnings
|
|
896
|
+
console.error = (...args) => { if (typeof args[0] === "string" && args[0].includes("Can't perform a React state update"))
|
|
897
|
+
return; origError.apply(console, args); };
|
|
898
|
+
inkRef.unmount();
|
|
899
|
+
setTimeout(() => { console.warn = origWarn; console.error = origError; }, 100); // eslint-disable-line no-console
|
|
900
|
+
}
|
|
656
901
|
if (options.banner !== false) {
|
|
657
902
|
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
658
903
|
console.log("bye");
|
|
@@ -665,17 +910,19 @@ async function main(agentName, options) {
|
|
|
665
910
|
if (agentName)
|
|
666
911
|
(0, identity_1.setAgentName)(agentName);
|
|
667
912
|
const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
|
|
913
|
+
// Register spinner hooks so log output clears the spinner before printing
|
|
914
|
+
(0, nerves_1.registerSpinnerHooks)(pauseActiveSpinner, resumeActiveSpinner);
|
|
668
915
|
// Fallback: apply pending updates for daemon-less direct CLI usage
|
|
669
916
|
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
917
|
+
(0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
|
|
670
918
|
await (0, update_hooks_1.applyPendingUpdates)((0, identity_1.getAgentBundlesRoot)(), (0, bundle_manifest_1.getPackageVersion)());
|
|
671
919
|
// Fail fast if provider is misconfigured (triggers human-readable error + exit)
|
|
672
|
-
(0, core_1.getProvider)();
|
|
920
|
+
(0, core_1.getProvider)("human");
|
|
673
921
|
// Resolve context kernel (identity + channel) for CLI
|
|
674
922
|
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
675
923
|
const friendStore = new store_file_1.FileFriendStore(friendsPath);
|
|
676
924
|
const username = os.userInfo().username;
|
|
677
|
-
const
|
|
678
|
-
const localExternalId = `${username}@${hostname}`;
|
|
925
|
+
const localExternalId = username;
|
|
679
926
|
const resolver = new resolver_1.FriendResolver(friendStore, {
|
|
680
927
|
provider: "local",
|
|
681
928
|
externalId: localExternalId,
|
|
@@ -683,13 +930,6 @@ async function main(agentName, options) {
|
|
|
683
930
|
channel: "cli",
|
|
684
931
|
});
|
|
685
932
|
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
933
|
const friendId = resolvedContext.friend.id;
|
|
694
934
|
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
695
935
|
(0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
|
|
@@ -712,52 +952,109 @@ async function main(agentName, options) {
|
|
|
712
952
|
}
|
|
713
953
|
// Load existing session or start fresh
|
|
714
954
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
955
|
+
let sessionState = existing?.state;
|
|
956
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
715
957
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
716
958
|
? 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
|
-
}
|
|
959
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", {}, resolvedContext) }];
|
|
960
|
+
// Per-turn pipeline input: CLI capabilities and pending dir
|
|
961
|
+
const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
|
|
962
|
+
const currentAgentName = (0, identity_1.getAgentName)();
|
|
963
|
+
const pendingDir = (0, pending_1.getPendingDir)(currentAgentName, friendId, "cli", "session");
|
|
964
|
+
const summarize = (0, core_1.createSummarize)("human");
|
|
965
|
+
const cliFailoverState = { pending: null };
|
|
735
966
|
try {
|
|
736
967
|
await runCliSession({
|
|
737
|
-
agentName:
|
|
968
|
+
agentName: currentAgentName,
|
|
738
969
|
pasteDebounceMs,
|
|
739
970
|
messages: sessionMessages,
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
971
|
+
lastActivityAt: sessionState?.lastFriendActivityAt,
|
|
972
|
+
_testInputSource: options?._testInputSource,
|
|
973
|
+
onAsyncAssistantMessage: async (messages, _assistantMessage) => {
|
|
974
|
+
(0, context_1.postTurn)(messages, sessPath, undefined, undefined, sessionState);
|
|
975
|
+
},
|
|
976
|
+
runTurn: async (messages, userInput, callbacks, signal, toolContext) => {
|
|
977
|
+
// Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
|
|
978
|
+
// User message passed via input.messages so the pipeline can prepend pending messages to it.
|
|
979
|
+
const failoverState = cliFailoverState;
|
|
980
|
+
// Capture terminal errors instead of displaying immediately — the failover
|
|
981
|
+
// message replaces the raw error if failover triggers successfully.
|
|
982
|
+
let capturedTerminalError = null;
|
|
983
|
+
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
984
|
+
const failoverAwareCallbacks = {
|
|
985
|
+
...callbacks,
|
|
986
|
+
onError: (error, severity) => {
|
|
987
|
+
if (severity === "terminal" && failoverState) {
|
|
988
|
+
capturedTerminalError = error;
|
|
989
|
+
callbacks.onError(new Error(""), "transient");
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
callbacks.onError(error, severity);
|
|
993
|
+
},
|
|
994
|
+
};
|
|
995
|
+
/* v8 ignore stop */
|
|
996
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
997
|
+
channel: "cli",
|
|
998
|
+
sessionKey: "session",
|
|
999
|
+
capabilities: cliCapabilities,
|
|
1000
|
+
messages: [{ role: "user", content: userInput }],
|
|
1001
|
+
continuityIngressTexts: getCliContinuityIngressTexts(userInput),
|
|
1002
|
+
callbacks: failoverAwareCallbacks,
|
|
1003
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
1004
|
+
sessionLoader: {
|
|
1005
|
+
loadOrCreate: () => Promise.resolve({
|
|
1006
|
+
messages,
|
|
1007
|
+
sessionPath: sessPath,
|
|
1008
|
+
state: sessionState,
|
|
1009
|
+
}),
|
|
1010
|
+
},
|
|
1011
|
+
pendingDir,
|
|
1012
|
+
friendStore,
|
|
744
1013
|
provider: "local",
|
|
745
1014
|
externalId: localExternalId,
|
|
746
|
-
|
|
1015
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
1016
|
+
drainPending: pending_1.drainPending,
|
|
1017
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(currentAgentName, deferredFriendId),
|
|
1018
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
1019
|
+
...opts,
|
|
1020
|
+
toolContext: {
|
|
1021
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
1022
|
+
signin: async () => undefined,
|
|
1023
|
+
...opts?.toolContext,
|
|
1024
|
+
summarize,
|
|
1025
|
+
},
|
|
1026
|
+
}),
|
|
1027
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
1028
|
+
(0, context_1.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
|
|
1029
|
+
sessionState = state;
|
|
1030
|
+
},
|
|
1031
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
1032
|
+
signal,
|
|
1033
|
+
runAgentOptions: {
|
|
1034
|
+
toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
|
|
1035
|
+
traceId: (0, nerves_1.createTraceId)(),
|
|
1036
|
+
mcpManager,
|
|
1037
|
+
toolContext,
|
|
1038
|
+
},
|
|
1039
|
+
failoverState,
|
|
747
1040
|
});
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
};
|
|
1041
|
+
/* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
|
|
1042
|
+
if (result.failoverMessage) {
|
|
1043
|
+
// Failover handled it — show the actionable message instead of the raw error
|
|
1044
|
+
process.stdout.write(`\x1b[33m${result.failoverMessage}\x1b[0m\n`);
|
|
753
1045
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1046
|
+
else if (capturedTerminalError) {
|
|
1047
|
+
// Failover didn't trigger (no failoverState, or sequence failed) — show the raw error
|
|
1048
|
+
process.stderr.write(`\x1b[31m${(0, format_1.formatError)(capturedTerminalError)}\x1b[0m\n`);
|
|
1049
|
+
}
|
|
1050
|
+
/* v8 ignore stop */
|
|
1051
|
+
// Handle gate rejection: display auto-reply if present
|
|
1052
|
+
if (!result.gateResult.allowed) {
|
|
1053
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
1054
|
+
process.stdout.write(`${result.gateResult.autoReply}\n`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return { usage: result.usage, turnOutcome: result.turnOutcome, commandAction: result.commandAction };
|
|
761
1058
|
},
|
|
762
1059
|
onNewSession: () => {
|
|
763
1060
|
(0, context_1.deleteSession)(sessPath);
|
|
@@ -767,4 +1064,11 @@ async function main(agentName, options) {
|
|
|
767
1064
|
finally {
|
|
768
1065
|
sessionLock?.release();
|
|
769
1066
|
}
|
|
1067
|
+
// Force exit: lingering handles (Ink cleanup timers, MCP connections) keep the
|
|
1068
|
+
// event loop alive after the interactive session ends. This is safe because all
|
|
1069
|
+
// session persistence has already completed in the finally block above.
|
|
1070
|
+
/* v8 ignore next -- process.exit not callable in vitest @preserve */
|
|
1071
|
+
if (!options?._testInputSource)
|
|
1072
|
+
process.exit(0);
|
|
770
1073
|
}
|
|
1074
|
+
// CI trigger
|