openhorse 0.1.22 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +76 -427
  2. package/dist/cli.d.ts +1 -3
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +125 -1028
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/index.d.ts.map +1 -1
  7. package/dist/commands/index.js +91 -19
  8. package/dist/commands/index.js.map +1 -1
  9. package/dist/commands/types.d.ts +1 -0
  10. package/dist/commands/types.d.ts.map +1 -1
  11. package/dist/commands/types.js.map +1 -1
  12. package/dist/framework/index.d.ts +2 -2
  13. package/dist/framework/index.d.ts.map +1 -1
  14. package/dist/framework/index.js +6 -1
  15. package/dist/framework/index.js.map +1 -1
  16. package/dist/framework/query.d.ts +2 -0
  17. package/dist/framework/query.d.ts.map +1 -1
  18. package/dist/framework/query.js +5 -2
  19. package/dist/framework/query.js.map +1 -1
  20. package/dist/harness/assembler.d.ts +14 -2
  21. package/dist/harness/assembler.d.ts.map +1 -1
  22. package/dist/harness/assembler.js +167 -32
  23. package/dist/harness/assembler.js.map +1 -1
  24. package/dist/harness/capsule.d.ts +2 -1
  25. package/dist/harness/capsule.d.ts.map +1 -1
  26. package/dist/harness/capsule.js +64 -0
  27. package/dist/harness/capsule.js.map +1 -1
  28. package/dist/harness/context-harness.d.ts +23 -3
  29. package/dist/harness/context-harness.d.ts.map +1 -1
  30. package/dist/harness/context-harness.js +97 -7
  31. package/dist/harness/context-harness.js.map +1 -1
  32. package/dist/harness/contract.d.ts +2 -2
  33. package/dist/harness/contract.d.ts.map +1 -1
  34. package/dist/harness/contract.js +7 -3
  35. package/dist/harness/contract.js.map +1 -1
  36. package/dist/harness/evidence.d.ts +15 -0
  37. package/dist/harness/evidence.d.ts.map +1 -0
  38. package/dist/harness/evidence.js +171 -0
  39. package/dist/harness/evidence.js.map +1 -0
  40. package/dist/harness/index.d.ts +9 -3
  41. package/dist/harness/index.d.ts.map +1 -1
  42. package/dist/harness/index.js +15 -1
  43. package/dist/harness/index.js.map +1 -1
  44. package/dist/harness/intent.d.ts +4 -0
  45. package/dist/harness/intent.d.ts.map +1 -0
  46. package/dist/harness/intent.js +139 -0
  47. package/dist/harness/intent.js.map +1 -0
  48. package/dist/harness/state.d.ts +12 -0
  49. package/dist/harness/state.d.ts.map +1 -0
  50. package/dist/harness/state.js +84 -0
  51. package/dist/harness/state.js.map +1 -0
  52. package/dist/harness/turn-summary.d.ts +25 -0
  53. package/dist/harness/turn-summary.d.ts.map +1 -0
  54. package/dist/harness/turn-summary.js +120 -0
  55. package/dist/harness/turn-summary.js.map +1 -0
  56. package/dist/harness/types.d.ts +103 -0
  57. package/dist/harness/types.d.ts.map +1 -1
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +6 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/ink-ui/App.d.ts +6 -0
  63. package/dist/ink-ui/App.d.ts.map +1 -0
  64. package/dist/ink-ui/App.js +12 -0
  65. package/dist/ink-ui/App.js.map +1 -0
  66. package/dist/ink-ui/components/Markdown.d.ts +9 -0
  67. package/dist/ink-ui/components/Markdown.d.ts.map +1 -0
  68. package/dist/ink-ui/components/Markdown.js +188 -0
  69. package/dist/ink-ui/components/Markdown.js.map +1 -0
  70. package/dist/ink-ui/components/PixelHorseBanner.d.ts +8 -0
  71. package/dist/ink-ui/components/PixelHorseBanner.d.ts.map +1 -0
  72. package/dist/ink-ui/components/PixelHorseBanner.js +97 -0
  73. package/dist/ink-ui/components/PixelHorseBanner.js.map +1 -0
  74. package/dist/ink-ui/components/PromptInput.d.ts +9 -0
  75. package/dist/ink-ui/components/PromptInput.d.ts.map +1 -0
  76. package/dist/ink-ui/components/PromptInput.js +24 -0
  77. package/dist/ink-ui/components/PromptInput.js.map +1 -0
  78. package/dist/ink-ui/components/SelectList.d.ts +15 -0
  79. package/dist/ink-ui/components/SelectList.d.ts.map +1 -0
  80. package/dist/ink-ui/components/SelectList.js +39 -0
  81. package/dist/ink-ui/components/SelectList.js.map +1 -0
  82. package/dist/ink-ui/components/StatusLine.d.ts +9 -0
  83. package/dist/ink-ui/components/StatusLine.d.ts.map +1 -0
  84. package/dist/ink-ui/components/StatusLine.js +71 -0
  85. package/dist/ink-ui/components/StatusLine.js.map +1 -0
  86. package/dist/ink-ui/components/TerminalCursor.d.ts +13 -0
  87. package/dist/ink-ui/components/TerminalCursor.d.ts.map +1 -0
  88. package/dist/ink-ui/components/TerminalCursor.js +74 -0
  89. package/dist/ink-ui/components/TerminalCursor.js.map +1 -0
  90. package/dist/ink-ui/components/Transcript.d.ts +8 -0
  91. package/dist/ink-ui/components/Transcript.d.ts.map +1 -0
  92. package/dist/ink-ui/components/Transcript.js +29 -0
  93. package/dist/ink-ui/components/Transcript.js.map +1 -0
  94. package/dist/ink-ui/controllers/chat-controller.d.ts +15 -0
  95. package/dist/ink-ui/controllers/chat-controller.d.ts.map +1 -0
  96. package/dist/ink-ui/controllers/chat-controller.js +410 -0
  97. package/dist/ink-ui/controllers/chat-controller.js.map +1 -0
  98. package/dist/ink-ui/launch.d.ts +3 -0
  99. package/dist/ink-ui/launch.d.ts.map +1 -0
  100. package/dist/ink-ui/launch.js +14 -0
  101. package/dist/ink-ui/launch.js.map +1 -0
  102. package/dist/ink-ui/runtime/prompt-layout.d.ts +11 -0
  103. package/dist/ink-ui/runtime/prompt-layout.d.ts.map +1 -0
  104. package/dist/ink-ui/runtime/prompt-layout.js +51 -0
  105. package/dist/ink-ui/runtime/prompt-layout.js.map +1 -0
  106. package/dist/ink-ui/screens/ReplScreen.d.ts +14 -0
  107. package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -0
  108. package/dist/ink-ui/screens/ReplScreen.js +470 -0
  109. package/dist/ink-ui/screens/ReplScreen.js.map +1 -0
  110. package/dist/ink-ui/types.d.ts +43 -0
  111. package/dist/ink-ui/types.d.ts.map +1 -0
  112. package/dist/ink-ui/types.js +3 -0
  113. package/dist/ink-ui/types.js.map +1 -0
  114. package/dist/services/compact/auto-compact.d.ts +3 -1
  115. package/dist/services/compact/auto-compact.d.ts.map +1 -1
  116. package/dist/services/compact/auto-compact.js +10 -0
  117. package/dist/services/compact/auto-compact.js.map +1 -1
  118. package/dist/services/compact/compact.d.ts +5 -1
  119. package/dist/services/compact/compact.d.ts.map +1 -1
  120. package/dist/services/compact/compact.js +11 -1
  121. package/dist/services/compact/compact.js.map +1 -1
  122. package/dist/services/config-dir.d.ts +2 -0
  123. package/dist/services/config-dir.d.ts.map +1 -1
  124. package/dist/services/config-dir.js +5 -0
  125. package/dist/services/config-dir.js.map +1 -1
  126. package/dist/services/config.js +2 -2
  127. package/dist/services/config.js.map +1 -1
  128. package/dist/services/global-config.d.ts +1 -1
  129. package/dist/services/global-config.d.ts.map +1 -1
  130. package/dist/services/session-storage.d.ts +4 -1
  131. package/dist/services/session-storage.d.ts.map +1 -1
  132. package/dist/services/session-storage.js +76 -6
  133. package/dist/services/session-storage.js.map +1 -1
  134. package/dist/tools/mcp.js +1 -1
  135. package/dist/ui-v2/components/session-picker.d.ts +2 -0
  136. package/dist/ui-v2/components/session-picker.d.ts.map +1 -1
  137. package/dist/ui-v2/components/session-picker.js +34 -5
  138. package/dist/ui-v2/components/session-picker.js.map +1 -1
  139. package/dist/ui-v2/state/sessions.d.ts +1 -0
  140. package/dist/ui-v2/state/sessions.d.ts.map +1 -1
  141. package/dist/ui-v2/state/sessions.js +18 -0
  142. package/dist/ui-v2/state/sessions.js.map +1 -1
  143. package/package.json +5 -1
package/dist/cli.js CHANGED
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * openhorse - 命令行交互入口
4
- *
5
- * 简洁版 REPL,使用 readline + keypress 事件处理
3
+ * openhorse - Ink/React CLI entry
6
4
  */
7
5
  var __importDefault = (this && this.__importDefault) || function (mod) {
8
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -10,79 +8,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
8
  Object.defineProperty(exports, "__esModule", { value: true });
11
9
  require("dotenv/config");
12
10
  const chalk_1 = __importDefault(require("chalk"));
13
- const readline_1 = __importDefault(require("readline"));
14
11
  const fs_1 = require("fs");
15
12
  const path_1 = require("path");
16
13
  const init_1 = require("./init");
17
14
  const llm_1 = require("./services/llm");
18
- const tools_1 = require("./tools");
19
- const mcp_1 = require("./tools/mcp");
20
15
  const config_1 = require("./services/config");
21
16
  const config_dir_1 = require("./services/config-dir");
22
17
  const global_config_1 = require("./services/global-config");
23
- const model_context_1 = require("./services/model-context");
24
18
  const session_storage_1 = require("./services/session-storage");
25
19
  const storage_1 = require("./memory/storage");
26
20
  const skills_1 = require("./skills");
27
21
  const framework_1 = require("./framework");
28
- const commands_1 = require("./commands");
29
- const parser_1 = require("./commands/parser");
30
- const types_1 = require("./commands/types");
31
- const box_1 = require("./ui/box");
32
- const command_panel_1 = require("./ui/command-panel");
33
- const multiline_input_1 = require("./ui/multiline-input");
34
- const file_completion_1 = require("./ui/file-completion");
35
- const status_bar_1 = require("./ui/status-bar");
36
- const user_input_1 = require("./ui/user-input");
37
- const ui_v2_1 = require("./ui-v2");
38
- const turn_controller_1 = require("./runtime/turn-controller");
39
- // Get version from package.json
22
+ const tools_1 = require("./tools");
23
+ const mcp_1 = require("./tools/mcp");
24
+ const model_context_1 = require("./services/model-context");
25
+ const launch_1 = require("./ink-ui/launch");
26
+ const BRAND = chalk_1.default.hex('#FF6B35');
27
+ const ACCENT = chalk_1.default.hex('#00D4AA');
28
+ const DIM = chalk_1.default.dim;
29
+ const ERROR = chalk_1.default.red;
30
+ const WARN = chalk_1.default.yellow;
40
31
  const VERSION = (() => {
41
32
  try {
42
33
  const pkgPath = (0, path_1.join)(__dirname, '..', 'package.json');
43
34
  const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
44
- return pkg.version || '0.1.14';
35
+ return pkg.version || '0.2.0';
45
36
  }
46
37
  catch {
47
- return '0.1.14';
38
+ return '0.2.0';
48
39
  }
49
40
  })();
50
- // ============================================================================
51
- // 颜色常量
52
- // ============================================================================
53
- const BRAND = chalk_1.default.hex('#FF6B35');
54
- const ACCENT = chalk_1.default.hex('#00D4AA');
55
- const DIM = chalk_1.default.dim;
56
- const ERROR = chalk_1.default.red;
57
- const WARN = chalk_1.default.yellow;
58
- const SUCCESS = chalk_1.default.green;
59
- // ============================================================================
60
- // CLI Help
61
- // ============================================================================
62
41
  function showCliHelp() {
63
42
  console.log();
64
43
  console.log(BRAND('openhorse') + DIM(` v${VERSION}`));
65
44
  console.log(DIM(' Universal Agent Harness Framework'));
66
45
  console.log();
67
46
  console.log(ACCENT('Usage:'));
68
- console.log(' openhorse Start interactive REPL');
69
- console.log(' openhorse --help Show this help message');
70
- console.log(' openhorse --version Show version');
71
- console.log(' openhorse --ui legacy Use the legacy terminal renderer');
47
+ console.log(' openhorse Start the Ink/React interactive UI');
48
+ console.log(' openhorse --help Show this help message');
49
+ console.log(' openhorse --version Show version');
50
+ console.log(' openhorse --ui ink Start the Ink UI explicitly');
72
51
  console.log();
73
52
  console.log(ACCENT('Options:'));
74
53
  console.log(' -h, --help Show help');
75
54
  console.log(' -v, --version Show version');
76
- console.log(' --ui <mode> UI renderer: legacy | v2');
55
+ console.log(' --ui <mode> UI renderer: ink');
77
56
  console.log();
78
- console.log(ACCENT('Interactive Commands:'));
79
- console.log(' /help Show available slash commands');
80
- console.log(' /status Show system status');
81
- console.log(' /model [name] Show or change model');
82
- console.log(' /chat <msg> Send message to LLM');
83
- console.log(' /exit Exit the REPL');
84
- console.log();
85
- console.log(DIM('Type /help in REPL for full command list.'));
57
+ console.log(DIM('Legacy readline/v2 renderers were removed in v0.2.0.'));
86
58
  console.log();
87
59
  }
88
60
  function parseCliUIRenderer(args) {
@@ -95,1023 +67,148 @@ function parseCliUIRenderer(args) {
95
67
  : undefined;
96
68
  if (value === undefined)
97
69
  continue;
98
- if (value === 'legacy' || value === 'v2')
99
- return value;
70
+ if (value === 'ink')
71
+ return 'ink';
72
+ if (value === 'legacy' || value === 'v2') {
73
+ console.log(WARN(`Renderer "${value}" was removed in v0.2.0; starting Ink UI instead.`));
74
+ return 'ink';
75
+ }
100
76
  console.error(ERROR(`Invalid --ui value: ${value}`));
101
- console.error(DIM('Expected one of: legacy, v2'));
77
+ console.error(DIM('Expected: ink'));
102
78
  process.exit(1);
103
79
  }
104
- return undefined;
105
- }
106
- // ============================================================================
107
- // 全局状态
108
- // ============================================================================
109
- let llm = null;
110
- let store;
111
- let currentSession = null;
112
- let runtime;
113
- const turnController = new turn_controller_1.TurnController();
114
- let isShuttingDown = false;
115
- // 输入状态
116
- let currentInput = '';
117
- let inputHistory = [];
118
- let historyIndex = -1;
119
- let historyMode = 'none';
120
- let searchQuery = '';
121
- let resumePickerState = null;
122
- let resumePickerLines = [];
123
- let resumePickerReservedHeight = 0;
124
- function ensureCurrentSession() {
125
- if (!currentSession) {
126
- currentSession = (0, session_storage_1.createSession)(process.cwd(), store.getSnapshot().currentModel || store.getSnapshot().config.model);
127
- (0, global_config_1.incrementSessionCount)();
128
- }
129
- return currentSession;
130
- }
131
- function setCurrentSession(session) {
132
- currentSession = session;
133
- }
134
- function getCurrentSession() {
135
- return currentSession;
136
- }
137
- function echoSubmittedInput(input) {
138
- (0, command_panel_1.clearRenderedInput)();
139
- const rendered = isV2UI()
140
- ? (0, user_input_1.renderUserInputEchoFrame)(input)
141
- : (0, user_input_1.renderUserInputEcho)(input);
142
- (0, command_panel_1.writeLinePreservingInput)(rendered);
143
- if (isV2UI()) {
144
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
145
- }
146
- }
147
- function isExitCommandInput(input) {
148
- const parsed = (0, parser_1.parseInput)(input.trim());
149
- if (!parsed.isCommand)
150
- return false;
151
- const cmd = (0, commands_1.findCommand)(parsed.name);
152
- return cmd?.name === 'exit';
153
- }
154
- function submitInput(input) {
155
- const submittedInput = input;
156
- currentInput = '';
157
- echoSubmittedInput(submittedInput);
158
- (0, global_config_1.addToInputHistory)(submittedInput);
159
- inputHistory = (0, global_config_1.getInputHistory)();
160
- if (turnController.hasActiveTurn()) {
161
- const text = submittedInput.trim();
162
- const parsed = (0, parser_1.parseInput)(text);
163
- if (isExitCommandInput(text)) {
164
- void shutdownCli();
165
- return;
166
- }
167
- if (parsed.isCommand) {
168
- console.log(DIM('Command ignored while agent is running. Press Ctrl+C to interrupt first.'));
169
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
170
- return;
171
- }
172
- if (text) {
173
- turnController.clearExitIntent();
174
- turnController.requestRevision(submittedInput);
175
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
176
- }
177
- return;
178
- }
179
- void runInputTurn(submittedInput);
180
- }
181
- async function runInputTurn(input) {
182
- let nextInput = input;
183
- while (nextInput) {
184
- const turn = turnController.beginTurn(nextInput);
185
- store.setProcessing(true);
186
- try {
187
- await handleInput(nextInput, turn.abortSignal, { redrawPrompt: false, updateStatus: false });
188
- }
189
- catch (err) {
190
- console.log(ERROR(`Input error: ${err.message || String(err)}`));
191
- }
192
- finally {
193
- const revision = turnController.finishTurn(turn.id);
194
- store.setProcessing(false);
195
- if (revision && revision.trim()) {
196
- (0, command_panel_1.writeLinePreservingInput)(DIM('Interrupted. Restarting with latest instruction...'));
197
- nextInput = revision;
198
- }
199
- else {
200
- nextInput = undefined;
201
- }
202
- }
203
- }
204
- updateStatusBar();
205
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
206
- }
207
- function parseKey(char, key) {
208
- const sequence = key?.sequence || char || '';
209
- if (sequence === '\x1b[13;2u') {
210
- return { name: 'enter', ctrl: false, shift: true, meta: false, sequence };
211
- }
212
- if (sequence === '\x1b[13;3u') {
213
- return { name: 'enter', ctrl: false, shift: false, meta: true, sequence };
214
- }
215
- if (!key) {
216
- // 手动解析
217
- if (char === '\r' || char === '\n')
218
- return { name: 'enter', ctrl: false, shift: false, meta: false, sequence: char || '' };
219
- if (char === '\x1b')
220
- return { name: 'escape', ctrl: false, shift: false, meta: false, sequence: char };
221
- if (char === '\t')
222
- return { name: 'tab', ctrl: false, shift: false, meta: false, sequence: char };
223
- if (char === '\x7f' || char === '\b')
224
- return { name: 'backspace', ctrl: false, shift: false, meta: false, sequence: char };
225
- return { name: char || '', ctrl: false, shift: false, meta: false, sequence: char || '' };
226
- }
227
- // 统一 "return" 和 "enter"
228
- if (key.name === 'return') {
229
- key.name = 'enter';
230
- }
231
- if (key.name === 'enter' && (key.shift || key.meta)) {
232
- return key;
233
- }
234
- // 如果 key.name 为空,使用 char 作为 name
235
- if (!key.name && char) {
236
- key.name = char;
237
- }
238
- return key;
239
- }
240
- function handleKeypress(char, key) {
241
- const k = parseKey(char, key);
242
- if (k.ctrl && k.name === 'l') {
243
- clearTerminalView();
244
- return;
245
- }
246
- if (k.ctrl && k.name === 'c') {
247
- void handleCtrlC();
248
- return;
249
- }
250
- // 命令面板模式
251
- if ((0, command_panel_1.isPanelVisible)()) {
252
- handlePanelKeypress(k, char);
253
- return;
254
- }
255
- // 文件补全模式
256
- if ((0, file_completion_1.isFileCompletionVisible)()) {
257
- handleFileCompletionKeypress(k, char);
258
- return;
259
- }
260
- // Resume session picker mode
261
- if (resumePickerState?.visible) {
262
- handleResumePickerKeypress(k, char);
263
- return;
264
- }
265
- // 历史导航模式
266
- if (historyMode !== 'none') {
267
- handleHistoryKeypress(k, char);
268
- return;
269
- }
270
- // 正常输入模式
271
- handleNormalKeypress(k, char);
80
+ return 'ink';
272
81
  }
273
- function handlePanelKeypress(k, char) {
274
- switch (k.name) {
275
- case 'up':
276
- (0, command_panel_1.navigatePanel)('up');
277
- break;
278
- case 'down':
279
- (0, command_panel_1.navigatePanel)('down');
280
- break;
281
- case 'tab':
282
- const completedCmd = (0, command_panel_1.completeSelectedCommand)();
283
- if (completedCmd) {
284
- currentInput = completedCmd;
285
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
286
- }
287
- break;
288
- case 'enter':
289
- const cmd = (0, command_panel_1.selectCommand)();
290
- if (cmd) {
291
- submitInput(cmd);
292
- (0, command_panel_1.clearPendingCommand)();
293
- }
294
- break;
295
- case 'escape':
296
- (0, command_panel_1.hideCommandPanel)();
297
- currentInput = '';
298
- (0, command_panel_1.redrawInputWithPrompt)('');
299
- break;
300
- case 'backspace':
301
- currentInput = currentInput.slice(0, -1);
302
- if (currentInput.length <= 1) {
303
- (0, command_panel_1.hideCommandPanel)();
304
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
305
- }
306
- else {
307
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
308
- (0, command_panel_1.updatePanelFilter)(currentInput.slice(1));
309
- }
310
- break;
311
- default:
312
- // 添加字符到过滤
313
- if (char && char.length === 1 && !k.ctrl) {
314
- currentInput += char;
315
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
316
- (0, command_panel_1.updatePanelFilter)(currentInput.slice(1));
317
- }
318
- }
319
- }
320
- function handleHistoryKeypress(k, char) {
321
- if (historyMode === 'search') {
322
- // 搜索模式:输入搜索词
323
- switch (k.name) {
324
- case 'enter':
325
- if (inputHistory.length > 0 && historyIndex >= 0) {
326
- currentInput = inputHistory[historyIndex]?.content || '';
327
- }
328
- historyMode = 'none';
329
- searchQuery = '';
330
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
331
- break;
332
- case 'escape':
333
- historyMode = 'none';
334
- searchQuery = '';
335
- currentInput = '';
336
- (0, command_panel_1.redrawInputWithPrompt)('');
337
- break;
338
- case 'backspace':
339
- searchQuery = searchQuery.slice(0, -1);
340
- updateHistorySearch();
341
- break;
342
- default:
343
- if (char && char.length === 1 && !k.ctrl) {
344
- searchQuery += char;
345
- updateHistorySearch();
346
- }
347
- }
348
- }
349
- else {
350
- // 导航模式
351
- switch (k.name) {
352
- case 'up':
353
- historyIndex = Math.min(inputHistory.length - 1, historyIndex + 1);
354
- currentInput = inputHistory[historyIndex]?.content || '';
355
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
356
- break;
357
- case 'down':
358
- historyIndex = Math.max(-1, historyIndex - 1);
359
- if (historyIndex < 0) {
360
- currentInput = '';
361
- historyMode = 'none';
362
- }
363
- else {
364
- currentInput = inputHistory[historyIndex]?.content || '';
365
- }
366
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
367
- break;
368
- case 'enter':
369
- historyMode = 'none';
370
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
371
- break;
372
- case 'escape':
373
- historyMode = 'none';
374
- currentInput = '';
375
- historyIndex = -1;
376
- (0, command_panel_1.redrawInputWithPrompt)('');
377
- break;
378
- }
379
- }
380
- }
381
- function handleFileCompletionKeypress(k, char) {
382
- switch (k.name) {
383
- case 'up':
384
- (0, file_completion_1.navigateFiles)('up');
385
- break;
386
- case 'down':
387
- (0, file_completion_1.navigateFiles)('down');
388
- break;
389
- case 'tab':
390
- const completedPath = (0, file_completion_1.completeFile)();
391
- if (completedPath) {
392
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + completedPath;
393
- (0, file_completion_1.hideFileCompletion)();
394
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
395
- }
396
- else {
397
- // 目录:继续显示面板,更新 query
398
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
399
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
400
- }
401
- break;
402
- case 'enter':
403
- const selectedPath = (0, file_completion_1.selectFile)();
404
- if (selectedPath) {
405
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + selectedPath;
406
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
407
- }
408
- break;
409
- case 'escape':
410
- (0, file_completion_1.hideFileCompletion)();
411
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
412
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
413
- break;
414
- case 'backspace':
415
- const query = (0, file_completion_1.getFileQuery)();
416
- if (query.length > 0) {
417
- (0, file_completion_1.updateFileQuery)(query.slice(0, -1));
418
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
419
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
420
- }
421
- else {
422
- (0, file_completion_1.hideFileCompletion)();
423
- currentInput = (0, file_completion_1.getBaseInput)();
424
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
425
- }
426
- break;
427
- default:
428
- // 添加字符到路径查询
429
- if (char && char.length === 1 && !k.ctrl) {
430
- const newQuery = (0, file_completion_1.getFileQuery)() + char;
431
- (0, file_completion_1.updateFileQuery)(newQuery);
432
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + newQuery;
433
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
434
- }
435
- }
436
- }
437
- function handleResumePickerKeypress(k, char) {
438
- if (!resumePickerState?.visible)
439
- return;
440
- switch (k.name) {
441
- case 'up':
442
- resumePickerState.selectedIndex = Math.max(0, resumePickerState.selectedIndex - 1);
443
- renderResumePicker();
444
- break;
445
- case 'down':
446
- resumePickerState.selectedIndex = Math.min(resumePickerState.sessions.length - 1, resumePickerState.selectedIndex + 1);
447
- renderResumePicker();
448
- break;
449
- case 'enter':
450
- restoreSelectedResumeSession();
451
- break;
452
- case 'escape':
453
- hideResumePicker();
454
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
455
- break;
456
- default:
457
- if (char && /^[1-9]$/.test(char)) {
458
- const index = Number(char) - 1;
459
- if (index >= 0 && index < resumePickerState.sessions.length) {
460
- resumePickerState.selectedIndex = index;
461
- restoreSelectedResumeSession();
462
- }
463
- }
464
- }
465
- }
466
- function showResumePicker(options) {
467
- resumePickerState = {
468
- visible: true,
469
- sessions: options.sessions,
470
- selectedIndex: 0,
471
- title: options.title,
472
- showProject: options.showProject,
473
- moreCount: options.moreCount,
474
- allProjects: options.allProjects,
475
- };
476
- renderResumePicker();
477
- }
478
- function renderResumePicker() {
479
- if (!resumePickerState?.visible)
480
- return;
481
- clearResumePicker({ release: false });
482
- const lines = (0, ui_v2_1.renderSessionPicker)({
483
- title: resumePickerState.title,
484
- sessions: resumePickerState.sessions,
485
- selectedIndex: resumePickerState.selectedIndex,
486
- width: process.stdout.columns || 80,
487
- showProject: resumePickerState.showProject,
488
- moreCount: resumePickerState.moreCount,
489
- footer: ' ↑↓ Select Enter Resume 1-9 Quick Esc Cancel',
490
- theme: {
491
- accent: ACCENT,
492
- dim: DIM,
493
- selected: text => chalk_1.default.bgHex('#1E293B').hex('#E2E8F0')(text),
494
- },
495
- });
496
- const offset = getResumePickerOffsetRows();
497
- reserveResumePickerSpace(lines.length + offset);
498
- resumePickerLines = lines;
499
- process.stdout.write('\x1b7');
500
- if (offset > 0) {
501
- process.stdout.write(`\x1b[${offset}B\r`);
502
- }
503
- process.stdout.write('\x1b[J');
504
- for (let index = 0; index < lines.length; index++) {
505
- if (index > 0 || offset === 0) {
506
- process.stdout.write('\n');
507
- }
508
- process.stdout.write('\r' + lines[index]);
509
- }
510
- process.stdout.write('\x1b8');
511
- }
512
- function hideResumePicker() {
513
- clearResumePicker({ release: true });
514
- resumePickerState = null;
515
- }
516
- function clearResumePicker(options = {}) {
517
- const height = Math.max(resumePickerReservedHeight, resumePickerLines.length);
518
- if (height <= 0)
519
- return;
520
- process.stdout.write('\x1b7');
521
- const offset = getResumePickerOffsetRows();
522
- if (offset > 0) {
523
- process.stdout.write(`\x1b[${offset}B\r`);
524
- }
525
- process.stdout.write('\x1b[J');
526
- process.stdout.write('\x1b8');
527
- resumePickerLines = [];
528
- if (options.release) {
529
- resumePickerReservedHeight = 0;
530
- }
531
- }
532
- function reserveResumePickerSpace(requiredHeight) {
533
- if (requiredHeight <= resumePickerReservedHeight)
534
- return;
535
- const extraLines = requiredHeight - resumePickerReservedHeight;
536
- process.stdout.write('\n'.repeat(extraLines));
537
- process.stdout.write(`\x1b[${extraLines}A`);
538
- resumePickerReservedHeight = requiredHeight;
539
- }
540
- function getResumePickerOffsetRows() {
541
- return isV2UI() ? 2 : 1;
542
- }
543
- function restoreSelectedResumeSession() {
544
- if (!resumePickerState?.visible)
545
- return;
546
- const session = resumePickerState.sessions[resumePickerState.selectedIndex];
547
- const suffix = resumePickerState.allProjects ? ' --all' : '';
548
- hideResumePicker();
549
- currentInput = '';
550
- (0, command_panel_1.clearRenderedInput)();
551
- runInputTurn(`/resume ${session.id}${suffix}`).catch(err => {
552
- console.log(ERROR(`Resume error: ${err.message || String(err)}`));
553
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
554
- });
555
- }
556
- function handleNormalKeypress(k, char) {
557
- switch (k.name) {
558
- case 'enter':
559
- if (k.shift || k.meta) {
560
- if (isV2UI()) {
561
- insertInputNewline();
562
- }
563
- return;
564
- }
565
- // 多行模式:添加行
566
- if ((0, multiline_input_1.isMultilineActive)()) {
567
- if ((0, multiline_input_1.shouldEnterMultiline)(currentInput)) {
568
- (0, multiline_input_1.addMultilineLine)(currentInput);
569
- currentInput = '';
570
- if (isV2UI()) {
571
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
572
- }
573
- else {
574
- process.stdout.write('\r\x1b[2K');
575
- process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
576
- }
577
- }
578
- else {
579
- // 结束多行,发送完整输入
580
- (0, multiline_input_1.addMultilineLine)(currentInput);
581
- const fullInput = (0, multiline_input_1.getMultilineInput)();
582
- (0, multiline_input_1.resetMultiline)();
583
- if (fullInput.trim()) {
584
- submitInput(fullInput);
585
- }
586
- currentInput = '';
587
- }
588
- return;
589
- }
590
- // 检查是否进入多行模式
591
- if ((0, multiline_input_1.shouldEnterMultiline)(currentInput)) {
592
- (0, multiline_input_1.enterMultiline)(currentInput);
593
- currentInput = '';
594
- if (isV2UI()) {
595
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
596
- }
597
- else {
598
- process.stdout.write('\r\x1b[2K');
599
- process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
600
- }
601
- return;
602
- }
603
- // 正常发送输入
604
- if (currentInput.trim()) {
605
- submitInput(currentInput);
606
- }
607
- break;
608
- case 'backspace':
609
- currentInput = currentInput.slice(0, -1);
610
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
611
- break;
612
- case 'up':
613
- // 进入历史导航
614
- if (currentInput === '' && inputHistory.length > 0) {
615
- historyMode = 'navigate';
616
- historyIndex = 0;
617
- currentInput = inputHistory[0]?.content || '';
618
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
619
- }
620
- break;
621
- case 'r':
622
- if (k.ctrl) {
623
- // Ctrl+R:搜索历史
624
- historyMode = 'search';
625
- searchQuery = '';
626
- showHistorySearchPrompt();
627
- }
628
- else if (char) {
629
- currentInput += char;
630
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
631
- }
632
- break;
633
- case 'c':
634
- if (k.ctrl) {
635
- // Ctrl+C:取消多行或退出
636
- if ((0, multiline_input_1.isMultilineActive)()) {
637
- (0, multiline_input_1.resetMultiline)();
638
- currentInput = '';
639
- (0, command_panel_1.redrawInputWithPrompt)('');
640
- console.log(DIM('(cancelled)'));
641
- }
642
- else {
643
- handleCtrlC();
644
- }
645
- }
646
- else if (char) {
647
- currentInput += char;
648
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
649
- }
650
- break;
651
- case 'd':
652
- if (k.ctrl && currentInput === '') {
653
- // Ctrl+D:退出(空输入时)
654
- void shutdownCli();
655
- }
656
- else if (char) {
657
- currentInput += char;
658
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
659
- }
660
- break;
661
- case '/':
662
- // Issue #30 fix: 只在输入为空时触发命令面板(即 `/` 是第一个字符)
663
- // 避免在 URL(http://)、路径(src/)、正则等场景误触发
664
- if (currentInput === '' && !turnController.hasActiveTurn()) {
665
- currentInput = '/';
666
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
667
- (0, command_panel_1.showCommandPanel)('');
668
- }
669
- else {
670
- // 正常添加 `/` 到输入
671
- currentInput += '/';
672
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
673
- }
674
- break;
675
- case '@':
676
- // 显示文件补全
677
- if (turnController.hasActiveTurn()) {
678
- currentInput += '@';
679
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
680
- }
681
- else {
682
- const baseInput = currentInput;
683
- currentInput += '@';
684
- (0, file_completion_1.showFileCompletion)('', baseInput);
685
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
686
- }
687
- break;
688
- case '?':
689
- if (currentInput === '' && store.getSnapshot().config.ui?.renderer === 'v2' && !turnController.hasActiveTurn()) {
690
- showV2Shortcuts();
691
- }
692
- else {
693
- currentInput += '?';
694
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
695
- }
696
- break;
697
- default:
698
- // 普通字符
699
- if (char && char.length === 1 && !k.ctrl && !k.meta) {
700
- currentInput += char;
701
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
702
- }
703
- }
704
- }
705
- function insertInputNewline() {
706
- currentInput += '\n';
707
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
708
- }
709
- function updateHistorySearch() {
710
- if (searchQuery) {
711
- const allHistory = (0, global_config_1.getInputHistory)();
712
- const matches = allHistory.filter(h => h.content.toLowerCase().includes(searchQuery.toLowerCase()));
713
- historyIndex = 0;
714
- currentInput = matches[0]?.content || '';
715
- // Don't replace inputHistory — just display filtered results
716
- }
717
- else {
718
- historyIndex = -1;
719
- currentInput = '';
720
- }
721
- (0, command_panel_1.redrawInputWithPrompt)(currentInput, `[Search: ${searchQuery}]`);
722
- }
723
- function showHistorySearchPrompt() {
724
- (0, command_panel_1.clearRenderedInput)();
725
- (0, command_panel_1.redrawInputWithPrompt)('', '[Search: ]');
726
- }
727
- function isV2UI() {
728
- return store.getSnapshot().config.ui?.renderer === 'v2';
729
- }
730
- function showV2Shortcuts() {
731
- console.log();
732
- console.log((0, ui_v2_1.renderV2Shortcuts)(process.stdout.columns || 80));
733
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
734
- }
735
- function clearTerminalView() {
736
- (0, command_panel_1.hideCommandPanel)();
737
- (0, file_completion_1.hideFileCompletion)();
738
- (0, command_panel_1.resetRenderLength)();
739
- process.stdout.write('\x1Bc');
740
- showBanner();
741
- if (isV2UI()) {
742
- console.log(DIM(' View cleared. Conversation context is preserved.'));
743
- console.log((0, ui_v2_1.renderV2FooterHint)(process.stdout.columns || 80));
744
- }
745
- else {
746
- console.log(DIM(' View cleared. Conversation context is preserved.'));
747
- }
748
- console.log();
749
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
750
- }
751
- function cancelActiveInputMode() {
752
- if ((0, multiline_input_1.isMultilineActive)()) {
753
- (0, multiline_input_1.resetMultiline)();
754
- currentInput = '';
755
- console.log(DIM('(cancelled)'));
756
- (0, command_panel_1.redrawInputWithPrompt)('');
757
- return true;
758
- }
759
- if ((0, command_panel_1.isPanelVisible)()) {
760
- (0, command_panel_1.hideCommandPanel)();
761
- (0, command_panel_1.clearPendingCommand)();
762
- currentInput = '';
763
- (0, command_panel_1.redrawInputWithPrompt)('');
764
- return true;
765
- }
766
- if ((0, file_completion_1.isFileCompletionVisible)()) {
767
- (0, file_completion_1.hideFileCompletion)();
768
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
769
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
770
- return true;
771
- }
772
- if (resumePickerState?.visible) {
773
- hideResumePicker();
774
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
775
- return true;
776
- }
777
- if (historyMode !== 'none') {
778
- historyMode = 'none';
779
- searchQuery = '';
780
- historyIndex = -1;
781
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
782
- return true;
783
- }
784
- return false;
785
- }
786
- async function shutdownCli() {
787
- if (isShuttingDown)
788
- return;
789
- isShuttingDown = true;
790
- // 保存会话摘要后退出
791
- if (currentSession) {
792
- const messages = (0, session_storage_1.readSessionMessages)(currentSession.id);
793
- if (messages.length > 0) {
794
- (0, session_storage_1.updateSessionSummary)(currentSession.id, messages);
795
- }
796
- (0, session_storage_1.endSession)(currentSession.id);
797
- }
798
- // 关闭 stdin raw mode 并退出
799
- if (process.stdin.isTTY) {
800
- process.stdin.setRawMode(false);
801
- }
802
- process.stdin.pause();
803
- console.log();
804
- console.log(DIM('Shutting down...'));
805
- mcp_1.mcpManager.disconnectAll();
806
- await runtime.shutdown();
807
- console.log(SUCCESS('Goodbye! 🐴'));
808
- process.exit(0);
809
- }
810
- async function handleCtrlC() {
811
- if (cancelActiveInputMode()) {
812
- turnController.clearExitIntent();
813
- return;
814
- }
815
- if (turnController.hasActiveTurn()) {
816
- const shouldExit = turnController.registerExitIntent();
817
- turnController.interruptActiveTurn();
818
- if (shouldExit) {
819
- await shutdownCli();
820
- return;
821
- }
822
- (0, command_panel_1.writeLinePreservingInput)(DIM('Interrupted. Press Ctrl+C again to exit.'));
823
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
824
- return;
825
- }
826
- if (turnController.registerExitIntent()) {
827
- await shutdownCli();
828
- return;
829
- }
830
- (0, command_panel_1.writeLinePreservingInput)(DIM('Press Ctrl+C again to exit.'));
831
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
832
- }
833
- // ============================================================================
834
- // Banner
835
- // ============================================================================
836
- function showBanner() {
837
- const config = store.getSnapshot().config;
838
- const baseUrl = config.apiBaseUrl || '';
839
- const provider = baseUrl.includes('anthropic') ? 'Anthropic'
840
- : baseUrl.includes('openai') ? 'OpenAI'
841
- : baseUrl.includes('dashscope') ? 'Alibaba Cloud'
842
- : 'Custom';
843
- if (config.ui?.renderer === 'v2') {
844
- console.log();
845
- console.log((0, ui_v2_1.renderV2ShellHeader)({
846
- provider,
847
- model: config.model,
848
- projectPath: process.cwd(),
849
- status: llm ? 'ready' : 'loading',
850
- statusText: llm ? 'ready' : 'Set OPENHORSE_API_KEY',
851
- version: VERSION,
852
- }));
853
- return;
854
- }
855
- console.log();
856
- console.log((0, box_1.renderHeaderBox)({
857
- provider,
858
- model: config.model,
859
- endpoint: baseUrl,
860
- status: llm ? 'ready' : 'loading',
861
- statusText: llm ? undefined : 'Set OPENHORSE_API_KEY in .env',
862
- version: VERSION,
863
- }));
864
- }
865
- // ============================================================================
866
- // 输入处理
867
- // ============================================================================
868
- function getPrompt() {
869
- const mode = store.getSnapshot().permissionMode;
870
- const modeText = (0, types_1.getModeDisplayText)(mode);
871
- const modeIndicator = modeText ? `[${modeText}] ` : '';
872
- return ACCENT('❯ ') + DIM(modeIndicator);
873
- }
874
- async function handleInput(input, abortSignal, options = {}) {
875
- const redrawPrompt = options.redrawPrompt !== false;
876
- const shouldUpdateStatus = options.updateStatus !== false;
877
- const text = input.trim();
878
- if (!text)
879
- return;
880
- // 不在这里打印输入,readline 已经显示了
881
- // 直接交给 executeChat 处理,它有自己的 spinner 和流式输出
882
- const ctx = {
883
- cwd: process.cwd(),
884
- config: store.getSnapshot().config,
885
- store,
886
- llm,
887
- runtime,
888
- sessionId: currentSession?.id,
889
- ensureSession: ensureCurrentSession,
890
- setSession: setCurrentSession,
891
- getSession: getCurrentSession,
892
- abortSignal,
893
- writeOutput: command_panel_1.writeOutputPreservingInput,
894
- writeLine: command_panel_1.writeLinePreservingInput,
895
- };
896
- let pendingSessionPicker;
897
- try {
898
- const parsed = (0, parser_1.parseInput)(text);
899
- if (parsed.isCommand) {
900
- const cmd = (0, commands_1.findCommand)(parsed.name);
901
- if (cmd) {
902
- const result = await cmd.execute(ctx, parsed.args);
903
- pendingSessionPicker = result.sessionPicker;
904
- if (result.continueAsChat) {
905
- await (0, commands_1.executeChat)(ctx, result.chatInput ?? parsed.args);
906
- }
907
- }
908
- else {
909
- if ((0, skills_1.hasMatchingSkill)(text)) {
910
- await (0, commands_1.executeChat)(ctx, text);
911
- }
912
- else {
913
- console.log();
914
- console.log(ERROR(`Unknown command: /${parsed.name}`));
915
- const suggestions = (0, parser_1.buildCommandSuggestions)(parsed.name);
916
- if (suggestions.length > 0) {
917
- console.log(DIM(`Did you mean: ${suggestions.map(s => `/${s}`).join(', ')}?`));
918
- }
919
- console.log();
920
- }
921
- }
922
- }
923
- else {
924
- // 直接 chat - executeChat 有自己的 spinner 和流式输出
925
- await (0, commands_1.executeChat)(ctx, text);
926
- }
927
- }
928
- catch (err) {
929
- console.log(ERROR(`Error: ${err.message || String(err)}`));
930
- }
931
- // 显示状态栏
932
- if (shouldUpdateStatus) {
933
- updateStatusBar();
934
- }
935
- // 重新显示 prompt
936
- if (redrawPrompt) {
937
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
938
- }
939
- if (pendingSessionPicker) {
940
- showResumePicker(pendingSessionPicker);
941
- }
942
- }
943
- /**
944
- * 更新状态栏显示
945
- */
946
- function updateStatusBar() {
947
- const snapshot = store.getSnapshot();
948
- const usage = snapshot.tokenUsage;
949
- const costStats = snapshot.costTracker.getSessionStats();
950
- const mcpStatus = mcp_1.mcpManager.getStatus();
951
- const stats = {
952
- model: snapshot.currentModel,
953
- tokens: usage ? usage.promptTokens + usage.completionTokens : 0,
954
- promptTokens: usage?.promptTokens || 0,
955
- completionTokens: usage?.completionTokens || 0,
956
- cost: costStats.totalCost,
957
- ctxPercent: (0, model_context_1.calculateCtxPercent)(usage ? usage.promptTokens + usage.completionTokens : 0, snapshot.currentModel || 'gpt-4o'),
958
- mcpConnected: mcpStatus.filter(s => s.connected).length,
959
- mcpTotal: mcpStatus.length,
960
- };
961
- // 在 prompt 上一行显示状态栏
962
- if (snapshot.config.ui?.renderer === 'v2') {
963
- (0, command_panel_1.setInputStatusText)((0, ui_v2_1.renderV2StatusBadge)({
964
- ...stats,
965
- sessionId: currentSession?.id,
966
- modeText: (0, types_1.getModeDisplayText)(snapshot.permissionMode) || undefined,
967
- width: process.stdout.columns || 80,
968
- }));
969
- }
970
- else {
971
- console.log((0, status_bar_1.renderStatusBar)(stats));
972
- }
973
- }
974
- // 接口变量(用于兼容性,主要逻辑通过 keypress 处理)
975
- let rl = null;
976
- // ============================================================================
977
- // 主入口
978
- // ============================================================================
979
- async function main() {
980
- // Parse command line arguments
981
- const args = process.argv.slice(2);
982
- if (args.includes('--help') || args.includes('-h')) {
983
- showCliHelp();
984
- process.exit(0);
985
- }
986
- if (args.includes('--version') || args.includes('-v')) {
987
- console.log(`openhorse v${VERSION}`);
988
- process.exit(0);
989
- }
82
+ async function bootstrapRuntime(uiRenderer) {
990
83
  (0, config_dir_1.ensureConfigDir)();
991
84
  (0, global_config_1.recordFirstStartTime)();
992
- const projectPath = process.cwd();
993
- const uiRenderer = parseCliUIRenderer(args);
994
- const cliConfig = (0, config_1.loadConfig)(uiRenderer ? { ui: { renderer: uiRenderer } } : {});
995
- (0, command_panel_1.setInputPromptRenderer)(cliConfig.ui?.renderer === 'v2' ? 'v2' : 'legacy');
996
- (0, command_panel_1.setInputRenderContextProvider)(() => (cliConfig.ui?.renderer === 'v2' && (0, multiline_input_1.isMultilineActive)()
997
- ? { prefixLines: (0, multiline_input_1.getMultilineLines)() }
998
- : {}));
999
- (0, file_completion_1.setFileCompletionPromptRenderer)(cliConfig.ui?.renderer === 'v2' ? 'v2' : 'legacy');
1000
- // Load project memory
1001
- const memories = (0, storage_1.loadAllMemories)(projectPath);
85
+ const cwd = process.cwd();
86
+ const config = (0, config_1.loadConfig)({ ui: { renderer: uiRenderer } });
87
+ const memories = (0, storage_1.loadAllMemories)(cwd);
1002
88
  const memoryContent = memories.length > 0
1003
- ? memories.map(m => `## ${m.name} (${m.type})\n${m.content}`).join('\n\n')
89
+ ? memories.map(memory => `## ${memory.name} (${memory.type})\n${memory.content}`).join('\n\n')
1004
90
  : '';
1005
- // Load skills (builtin + user + project) and render the prompt section
1006
91
  let skillsContent = '';
1007
92
  try {
1008
93
  const registry = (0, skills_1.getSkillsRegistry)();
1009
94
  skillsContent = registry.generateSystemPromptInjection();
1010
95
  }
1011
- catch (err) {
1012
- console.error(WARN(`⚠ Skills load error: ${err.message}`));
96
+ catch {
97
+ skillsContent = '';
1013
98
  }
1014
- store = new framework_1.Store({
1015
- config: cliConfig,
99
+ const store = new framework_1.Store({
100
+ config,
1016
101
  tools: (0, tools_1.getRuntimeTools)(),
1017
- currentModel: cliConfig.model,
102
+ currentModel: config.model,
1018
103
  memoryContent,
1019
104
  skillsContent,
1020
105
  });
1021
- // Mirror tool-state (todos/plan) into Store so the UI can observe it
1022
106
  (0, framework_1.resetToolState)();
1023
- (0, framework_1.subscribeToolState)((s) => {
107
+ (0, framework_1.subscribeToolState)(state => {
1024
108
  store.setState({
1025
- todos: s.todos,
1026
- planMode: s.planMode,
1027
- currentPlan: s.currentPlan,
109
+ todos: state.todos,
110
+ planMode: state.planMode,
111
+ currentPlan: state.currentPlan,
1028
112
  });
1029
113
  });
1030
- if ((0, config_1.isConfigured)(cliConfig)) {
1031
- try {
1032
- llm = new llm_1.LLMService({
1033
- apiKey: cliConfig.apiKey,
1034
- baseUrl: cliConfig.apiBaseUrl,
1035
- model: cliConfig.model,
1036
- fallbackModel: cliConfig.fallbackModel,
1037
- });
1038
- // 动态发现模型上下文窗口(非阻塞)
1039
- if (cliConfig.apiBaseUrl) {
1040
- (0, model_context_1.discoverModelContexts)(cliConfig.apiBaseUrl, cliConfig.apiKey)
1041
- .then(models => {
1042
- if (models.length > 0) {
1043
- console.log(` Discovered ${models.length} models from API`);
1044
- }
1045
- })
1046
- .catch(() => { }); // 静默失败,回退到内置数据库
1047
- }
1048
- }
1049
- catch (err) {
1050
- console.log(WARN(`⚠ LLM initialization warning: ${err.message}`));
114
+ let llm = null;
115
+ if ((0, config_1.isConfigured)(config)) {
116
+ llm = new llm_1.LLMService({
117
+ apiKey: config.apiKey,
118
+ baseUrl: config.apiBaseUrl,
119
+ model: config.model,
120
+ fallbackModel: config.fallbackModel,
121
+ });
122
+ if (config.apiBaseUrl) {
123
+ (0, model_context_1.discoverModelContexts)(config.apiBaseUrl, config.apiKey).catch(() => undefined);
1051
124
  }
1052
125
  }
1053
- const config = store.getSnapshot().config;
1054
- runtime = await (0, init_1.init)({
126
+ const runtime = await (0, init_1.init)({
1055
127
  name: config.name,
1056
128
  mode: config.mode,
1057
129
  logLevel: config.logLevel,
1058
130
  });
1059
131
  await runtime.start();
1060
- // Auto-connect MCP servers from ~/.openhorse/mcp.json (non-blocking)
1061
- mcp_1.mcpManager.connectAll()
1062
- .then(() => {
1063
- store.setState({ tools: (0, tools_1.getRuntimeTools)() });
1064
- })
1065
- .catch(err => {
1066
- console.error(WARN(`⚠ MCP startup error: ${err.message}`));
1067
- });
1068
- // Banner
1069
- showBanner();
1070
- // 提示
1071
- console.log(SUCCESS('✔ System initialized'));
1072
- console.log(DIM(' Type /help for commands, /exit to quit'));
1073
- if (cliConfig.ui?.renderer === 'v2') {
1074
- console.log(DIM(' UI v2 enabled: command palette and session picker'));
1075
- console.log((0, ui_v2_1.renderV2FooterHint)(process.stdout.columns || 80));
1076
- }
1077
- if (!(0, config_1.isConfigured)(cliConfig)) {
1078
- console.log(WARN(' ⚠ LLM not configured — set OPENHORSE_API_KEY'));
1079
- }
1080
- console.log();
1081
- // 加载输入历史
1082
- inputHistory = (0, global_config_1.getInputHistory)();
1083
- // 启用 keypress 事件处理
1084
- // 使用 emitKeypressEvents + setRawMode 实现交互式功能
1085
- readline_1.default.emitKeypressEvents(process.stdin);
1086
- if (process.stdin.isTTY) {
132
+ void (async () => {
133
+ const originalLog = console.log;
134
+ const originalError = console.error;
1087
135
  try {
1088
- process.stdin.setRawMode(true);
1089
- }
1090
- catch (err) {
1091
- console.error(ERROR(`setRawMode failed: ${err.message}`));
136
+ console.log = () => undefined;
137
+ console.error = () => undefined;
138
+ await mcp_1.mcpManager.connectAll();
139
+ store.setState({ tools: (0, tools_1.getRuntimeTools)() });
1092
140
  }
1093
- }
1094
- else {
1095
- console.log(WARN('⚠ stdin is not TTY - interactive features disabled'));
1096
- }
1097
- process.stdin.resume();
1098
- // 监听 keypress 事件
1099
- process.stdin.on('keypress', (char, key) => {
1100
- try {
1101
- handleKeypress(char, key);
141
+ catch {
142
+ // MCP failures are surfaced through /mcp and tool errors.
1102
143
  }
1103
- catch (err) {
1104
- console.error(ERROR(`Keypress error: ${err.message}`));
144
+ finally {
145
+ console.log = originalLog;
146
+ console.error = originalError;
147
+ }
148
+ })();
149
+ let currentSession = null;
150
+ let shuttingDown = false;
151
+ const ensureSession = () => {
152
+ if (!currentSession) {
153
+ currentSession = (0, session_storage_1.createSession)(cwd, store.getSnapshot().currentModel || store.getSnapshot().config.model);
154
+ (0, global_config_1.incrementSessionCount)();
155
+ }
156
+ return currentSession;
157
+ };
158
+ const setSession = (session) => {
159
+ currentSession = session;
160
+ };
161
+ const getSession = () => currentSession;
162
+ const shutdown = async () => {
163
+ if (shuttingDown)
164
+ return;
165
+ shuttingDown = true;
166
+ if (currentSession) {
167
+ const messages = (0, session_storage_1.readSessionMessages)(currentSession.id);
168
+ if (messages.length > 0) {
169
+ (0, session_storage_1.updateSessionSummary)(currentSession.id, messages);
170
+ }
171
+ (0, session_storage_1.endSession)(currentSession.id);
1105
172
  }
1106
- });
1107
- // 初始 prompt
1108
- if (cliConfig.ui?.renderer === 'v2') {
1109
- updateStatusBar();
173
+ await mcp_1.mcpManager.disconnectAll();
174
+ await runtime.shutdown();
175
+ };
176
+ return {
177
+ cwd,
178
+ version: VERSION,
179
+ config,
180
+ store,
181
+ llm,
182
+ runtime,
183
+ isConfigured: (0, config_1.isConfigured)(config),
184
+ ensureSession,
185
+ setSession,
186
+ getSession,
187
+ shutdown,
188
+ };
189
+ }
190
+ async function main() {
191
+ const args = process.argv.slice(2);
192
+ if (args.includes('--help') || args.includes('-h')) {
193
+ showCliHelp();
194
+ process.exit(0);
1110
195
  }
1111
- (0, command_panel_1.redrawInputWithPrompt)('');
196
+ if (args.includes('--version') || args.includes('-v')) {
197
+ console.log(`openhorse v${VERSION}`);
198
+ process.exit(0);
199
+ }
200
+ const uiRenderer = parseCliUIRenderer(args);
201
+ const runtime = await bootstrapRuntime(uiRenderer);
202
+ await (0, launch_1.launchInkUI)(runtime);
1112
203
  }
1113
- main().catch(err => {
1114
- console.error(ERROR('[OpenHorse] Fatal error:'), err);
204
+ main().catch(async (error) => {
205
+ console.error(ERROR('[OpenHorse] Fatal error:'), error);
206
+ try {
207
+ await mcp_1.mcpManager.disconnectAll();
208
+ }
209
+ catch {
210
+ // ignore shutdown errors
211
+ }
1115
212
  process.exit(1);
1116
213
  });
1117
214
  //# sourceMappingURL=cli.js.map