fixo-cli 1.0.4 → 2.0.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.
Potentially problematic release.
This version of fixo-cli might be problematic. Click here for more details.
- package/CHANGELOG.md +62 -0
- package/README.md +18 -14
- package/dist/agent/agent-client.d.ts +28 -6
- package/dist/agent/agent-client.d.ts.map +1 -1
- package/dist/agent/agent-client.js +118 -39
- package/dist/agent/agent-client.js.map +1 -1
- package/dist/agent/agent-pool.d.ts +55 -6
- package/dist/agent/agent-pool.d.ts.map +1 -1
- package/dist/agent/agent-pool.js +120 -20
- package/dist/agent/agent-pool.js.map +1 -1
- package/dist/agent/auto-verifier.d.ts +55 -0
- package/dist/agent/auto-verifier.d.ts.map +1 -0
- package/dist/agent/auto-verifier.js +50 -0
- package/dist/agent/auto-verifier.js.map +1 -0
- package/dist/agent/command-parser.d.ts.map +1 -1
- package/dist/agent/command-parser.js +176 -0
- package/dist/agent/command-parser.js.map +1 -1
- package/dist/agent/context-builder.d.ts +24 -0
- package/dist/agent/context-builder.d.ts.map +1 -0
- package/dist/agent/context-builder.js +197 -0
- package/dist/agent/context-builder.js.map +1 -0
- package/dist/agent/conversation.d.ts +14 -1
- package/dist/agent/conversation.d.ts.map +1 -1
- package/dist/agent/conversation.js +53 -7
- package/dist/agent/conversation.js.map +1 -1
- package/dist/agent/mcp-bridge.js +1 -1
- package/dist/agent/mcp-bridge.js.map +1 -1
- package/dist/agent/orchestrator.d.ts +45 -0
- package/dist/agent/orchestrator.d.ts.map +1 -1
- package/dist/agent/orchestrator.js +140 -3
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/parser-adapter.d.ts +17 -0
- package/dist/agent/parser-adapter.d.ts.map +1 -1
- package/dist/agent/parser-adapter.js +254 -2
- package/dist/agent/parser-adapter.js.map +1 -1
- package/dist/agent/predictive-gate.d.ts.map +1 -1
- package/dist/agent/predictive-gate.js +4 -1
- package/dist/agent/predictive-gate.js.map +1 -1
- package/dist/agent/providers-manager.d.ts +5 -0
- package/dist/agent/providers-manager.d.ts.map +1 -1
- package/dist/agent/providers-manager.js +119 -8
- package/dist/agent/providers-manager.js.map +1 -1
- package/dist/agent/repo-map.d.ts +18 -1
- package/dist/agent/repo-map.d.ts.map +1 -1
- package/dist/agent/repo-map.js +144 -54
- package/dist/agent/repo-map.js.map +1 -1
- package/dist/agent/retry.js +1 -2
- package/dist/agent/retry.js.map +1 -1
- package/dist/agent/single-agent.d.ts.map +1 -1
- package/dist/agent/single-agent.js +129 -22
- package/dist/agent/single-agent.js.map +1 -1
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +2 -1
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/task-router.d.ts +46 -0
- package/dist/agent/task-router.d.ts.map +1 -0
- package/dist/agent/task-router.js +352 -0
- package/dist/agent/task-router.js.map +1 -0
- package/dist/agent/telemetry.d.ts +29 -1
- package/dist/agent/telemetry.d.ts.map +1 -1
- package/dist/agent/telemetry.js +25 -10
- package/dist/agent/telemetry.js.map +1 -1
- package/dist/agent/tool-definitions.d.ts +3 -0
- package/dist/agent/tool-definitions.d.ts.map +1 -0
- package/dist/agent/tool-definitions.js +519 -0
- package/dist/agent/tool-definitions.js.map +1 -0
- package/dist/agent/tool-executor.d.ts +6 -1
- package/dist/agent/tool-executor.d.ts.map +1 -1
- package/dist/agent/tool-executor.js +99 -553
- package/dist/agent/tool-executor.js.map +1 -1
- package/dist/agent/tools/command-tools.d.ts +6 -0
- package/dist/agent/tools/command-tools.d.ts.map +1 -0
- package/dist/agent/tools/command-tools.js +104 -0
- package/dist/agent/tools/command-tools.js.map +1 -0
- package/dist/agent/tools/file-tools.d.ts +15 -0
- package/dist/agent/tools/file-tools.d.ts.map +1 -0
- package/dist/agent/tools/file-tools.js +551 -0
- package/dist/agent/tools/file-tools.js.map +1 -0
- package/dist/agent/tools/todo-tools.d.ts +3 -0
- package/dist/agent/tools/todo-tools.d.ts.map +1 -0
- package/dist/agent/tools/todo-tools.js +70 -0
- package/dist/agent/tools/todo-tools.js.map +1 -0
- package/dist/agent/web-impl.d.ts.map +1 -1
- package/dist/agent/web-impl.js +45 -0
- package/dist/agent/web-impl.js.map +1 -1
- package/dist/agent/worker-agent.d.ts +3 -1
- package/dist/agent/worker-agent.d.ts.map +1 -1
- package/dist/agent/worker-agent.js +51 -14
- package/dist/agent/worker-agent.js.map +1 -1
- package/dist/config.d.ts +242 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -1
- package/dist/git/git-manager.d.ts +33 -2
- package/dist/git/git-manager.d.ts.map +1 -1
- package/dist/git/git-manager.js +111 -15
- package/dist/git/git-manager.js.map +1 -1
- package/dist/git/git-ops.d.ts.map +1 -1
- package/dist/git/git-ops.js +2 -1
- package/dist/git/git-ops.js.map +1 -1
- package/dist/index.js +85 -8
- package/dist/index.js.map +1 -1
- package/dist/lsp/lsp-manager.js +1 -1
- package/dist/lsp/lsp-manager.js.map +1 -1
- package/dist/model-outcomes.d.ts.map +1 -1
- package/dist/model-outcomes.js +2 -1
- package/dist/model-outcomes.js.map +1 -1
- package/dist/planner.d.ts +0 -9
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +0 -9
- package/dist/planner.js.map +1 -1
- package/dist/project-memory.d.ts +12 -1
- package/dist/project-memory.d.ts.map +1 -1
- package/dist/project-memory.js +8 -6
- package/dist/project-memory.js.map +1 -1
- package/dist/runtime/loop-mitigation.d.ts +78 -7
- package/dist/runtime/loop-mitigation.d.ts.map +1 -1
- package/dist/runtime/loop-mitigation.js +122 -9
- package/dist/runtime/loop-mitigation.js.map +1 -1
- package/dist/runtime/os-sandbox.d.ts +100 -0
- package/dist/runtime/os-sandbox.d.ts.map +1 -0
- package/dist/runtime/os-sandbox.js +246 -0
- package/dist/runtime/os-sandbox.js.map +1 -0
- package/dist/runtime/run-inventory.d.ts +17 -0
- package/dist/runtime/run-inventory.d.ts.map +1 -0
- package/dist/runtime/run-inventory.js +49 -0
- package/dist/runtime/run-inventory.js.map +1 -0
- package/dist/runtime/staging.d.ts.map +1 -1
- package/dist/runtime/staging.js +4 -1
- package/dist/runtime/staging.js.map +1 -1
- package/dist/runtime/task-session.d.ts +14 -0
- package/dist/runtime/task-session.d.ts.map +1 -1
- package/dist/runtime/task-session.js +26 -0
- package/dist/runtime/task-session.js.map +1 -1
- package/dist/setup-wizard.d.ts +11 -3
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +113 -15
- package/dist/setup-wizard.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/commands/context-commands.d.ts +7 -0
- package/dist/ui/commands/context-commands.d.ts.map +1 -0
- package/dist/ui/commands/context-commands.js +241 -0
- package/dist/ui/commands/context-commands.js.map +1 -0
- package/dist/ui/commands/index.d.ts +3 -0
- package/dist/ui/commands/index.d.ts.map +1 -0
- package/dist/ui/commands/index.js +46 -0
- package/dist/ui/commands/index.js.map +1 -0
- package/dist/ui/commands/info-commands.d.ts +15 -0
- package/dist/ui/commands/info-commands.d.ts.map +1 -0
- package/dist/ui/commands/info-commands.js +122 -0
- package/dist/ui/commands/info-commands.js.map +1 -0
- package/dist/ui/commands/model-commands.d.ts +5 -0
- package/dist/ui/commands/model-commands.d.ts.map +1 -0
- package/dist/ui/commands/model-commands.js +417 -0
- package/dist/ui/commands/model-commands.js.map +1 -0
- package/dist/ui/commands/session-commands.d.ts +5 -0
- package/dist/ui/commands/session-commands.d.ts.map +1 -0
- package/dist/ui/commands/session-commands.js +154 -0
- package/dist/ui/commands/session-commands.js.map +1 -0
- package/dist/ui/commands/task-commands.d.ts +8 -0
- package/dist/ui/commands/task-commands.d.ts.map +1 -0
- package/dist/ui/commands/task-commands.js +152 -0
- package/dist/ui/commands/task-commands.js.map +1 -0
- package/dist/ui/commands/types.d.ts +46 -0
- package/dist/ui/commands/types.d.ts.map +1 -0
- package/dist/ui/commands/types.js +2 -0
- package/dist/ui/commands/types.js.map +1 -0
- package/dist/ui/commands/workspace-commands.d.ts +8 -0
- package/dist/ui/commands/workspace-commands.d.ts.map +1 -0
- package/dist/ui/commands/workspace-commands.js +131 -0
- package/dist/ui/commands/workspace-commands.js.map +1 -0
- package/dist/ui/loading-animation.d.ts +24 -0
- package/dist/ui/loading-animation.d.ts.map +1 -0
- package/dist/ui/loading-animation.js +123 -0
- package/dist/ui/loading-animation.js.map +1 -0
- package/dist/ui/markdown-stream.js +2 -2
- package/dist/ui/markdown-stream.js.map +1 -1
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.d.ts.map +1 -1
- package/dist/ui/prompt.js +435 -1214
- package/dist/ui/prompt.js.map +1 -1
- package/dist/ui/render-primitives.d.ts +6 -0
- package/dist/ui/render-primitives.d.ts.map +1 -1
- package/dist/ui/render-primitives.js +30 -13
- package/dist/ui/render-primitives.js.map +1 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +2 -0
- package/dist/ui/render.js.map +1 -1
- package/package.json +17 -3
- package/scripts/check-vendor-wasm.js +11 -0
- package/vendor/tree-sitter-go.wasm +0 -0
- package/vendor/tree-sitter-javascript.wasm +0 -0
- package/vendor/tree-sitter-python.wasm +0 -0
- package/vendor/tree-sitter-rust.wasm +0 -0
- package/vendor/tree-sitter-tsx.wasm +0 -0
- package/vendor/tree-sitter-typescript.wasm +0 -0
package/dist/ui/prompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandRegistry } from './commands/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Interactive REPL shell for FixO CLI.
|
|
3
4
|
* Provides command handling, file pinning, model selection,
|
|
@@ -11,26 +12,17 @@ import * as p from '@clack/prompts';
|
|
|
11
12
|
import { SingleAgent } from '../agent/single-agent.js';
|
|
12
13
|
import { ConversationManager } from '../agent/conversation.js';
|
|
13
14
|
import { GitManager } from '../git/git-manager.js';
|
|
14
|
-
import { loadImageAsBlock } from './image-attach.js';
|
|
15
|
-
import { saveConfig } from '../config.js';
|
|
16
15
|
import { WorkspaceGuard } from '../workspace-guard.js';
|
|
17
|
-
import { listRuns, showRun
|
|
16
|
+
import { listRuns, showRun } from '../runtime/task-session.js';
|
|
18
17
|
import { checkPermission } from '../agent/permissions.js';
|
|
19
18
|
import { redactedEnv, redactSecrets } from '../runtime/redaction.js';
|
|
20
|
-
import { appendMemory, doctor, forgetMemory, readMemory } from '../project-memory.js';
|
|
21
19
|
import { buildIndex, explainIndexedTarget, findInIndex } from '../indexer.js';
|
|
22
|
-
import { reviewWorkspace } from '../review.js';
|
|
23
|
-
import { runProjectTests } from '../test-runner.js';
|
|
24
|
-
import { loadPlan, renderPlan, savePlan, classifyComplexityHeuristic } from '../planner.js';
|
|
25
20
|
import { mcpManager, mcpBridgeManager } from '../agent/tool-executor.js';
|
|
26
|
-
import { ProvidersManager
|
|
21
|
+
import { ProvidersManager } from '../agent/providers-manager.js';
|
|
27
22
|
import { C, colors } from './colors.js';
|
|
28
23
|
import { COMMANDS_WITH_DESC, printHelp, formatInputPaths } from './render.js';
|
|
29
|
-
import { addItem, loadTodoList, removeItem, renderTodoList, saveTodoList, setItemStatus, summariseTodoList, } from '../context/todo.js';
|
|
30
24
|
import { renderStatusBar } from './render-primitives.js';
|
|
31
|
-
const c =
|
|
32
|
-
...colors,
|
|
33
|
-
};
|
|
25
|
+
const c = colors;
|
|
34
26
|
export async function startREPL(options) {
|
|
35
27
|
const { config, projectConfig, cwd, verbose, resume } = options;
|
|
36
28
|
// ──── Initialize components ────
|
|
@@ -107,13 +99,21 @@ export async function startREPL(options) {
|
|
|
107
99
|
}
|
|
108
100
|
let lastPromptRow = 0;
|
|
109
101
|
let mouseReportingEnabled = false;
|
|
110
|
-
|
|
102
|
+
let stats = {
|
|
111
103
|
totalPromptTokens: 0,
|
|
112
104
|
totalCompletionTokens: 0,
|
|
113
105
|
totalToolCalls: 0,
|
|
114
106
|
totalTasks: 0,
|
|
115
107
|
totalDurationMs: 0,
|
|
116
108
|
};
|
|
109
|
+
let pendingPastes = [];
|
|
110
|
+
let pasteIdCounter = 1;
|
|
111
|
+
let isPasting = false;
|
|
112
|
+
let pasteBuffer = '';
|
|
113
|
+
/** Builds the inline token string that goes INTO the rl line buffer. */
|
|
114
|
+
function pasteToken(id, lineCount) {
|
|
115
|
+
return `[Paste #${id} +${lineCount} lines]`;
|
|
116
|
+
}
|
|
117
117
|
// The welcome screen (lava logo + command grid) is printed by
|
|
118
118
|
// `src/index.ts` before the REPL starts; the startREPL entry
|
|
119
119
|
// point jumps straight into the prompt loop.
|
|
@@ -129,7 +129,8 @@ export async function startREPL(options) {
|
|
|
129
129
|
}
|
|
130
130
|
catch (error) {
|
|
131
131
|
if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
|
|
132
|
-
|
|
132
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
133
|
+
console.warn(`[Debug Warning] Failed to read command history from ${historyFile}: ${msg}`);
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
// ──── Create readline interface ────
|
|
@@ -160,9 +161,7 @@ export async function startREPL(options) {
|
|
|
160
161
|
// existing /mode command semantics intact while still letting
|
|
161
162
|
// the new bar visualise the live mode.
|
|
162
163
|
const buildLavaStatusState = () => {
|
|
163
|
-
const modeForState = currentMode === 'PLAN' ? 'PLAN' :
|
|
164
|
-
currentMode === 'BUILD' ? 'BUILD' :
|
|
165
|
-
'BUILD';
|
|
164
|
+
const modeForState = currentMode === 'PLAN' ? 'PLAN' : 'BUILD';
|
|
166
165
|
let contextPercent = 0;
|
|
167
166
|
try {
|
|
168
167
|
const used = conversation.getTotalTokens();
|
|
@@ -192,7 +191,7 @@ export async function startREPL(options) {
|
|
|
192
191
|
branch: currentBranch || '(detached HEAD)',
|
|
193
192
|
contextPercent,
|
|
194
193
|
providersCount,
|
|
195
|
-
transport: 'freellmapi',
|
|
194
|
+
transport: config.provider_mode === 'direct' ? 'direct' : 'freellmapi',
|
|
196
195
|
};
|
|
197
196
|
};
|
|
198
197
|
const drawLavaStatusBar = () => {
|
|
@@ -203,6 +202,10 @@ export async function startREPL(options) {
|
|
|
203
202
|
// returns.
|
|
204
203
|
renderStatusBar(buildLavaStatusState());
|
|
205
204
|
process.stdout.write('\n');
|
|
205
|
+
if (pendingPastes.length > 0) {
|
|
206
|
+
const tokens = pendingPastes.map(p => pasteToken(p.id, p.lines)).join(' ');
|
|
207
|
+
process.stdout.write(`${c.dim} ${tokens}${c.reset}\n`);
|
|
208
|
+
}
|
|
206
209
|
};
|
|
207
210
|
// Surface the result of a live model fetch as a one-line status.
|
|
208
211
|
// Invoked from /providers add and /providers test so the user
|
|
@@ -254,6 +257,16 @@ export async function startREPL(options) {
|
|
|
254
257
|
}
|
|
255
258
|
// Register synchronous exit cleanups
|
|
256
259
|
const exitCleanup = () => {
|
|
260
|
+
try {
|
|
261
|
+
if (process.stdout.isTTY) {
|
|
262
|
+
fs.writeSync(1, '\x1b[?2004l');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
if (process.env.DEBUG || process.env.VERBOSE) {
|
|
267
|
+
console.warn('[exit] writeSync failed:', e);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
257
270
|
try {
|
|
258
271
|
const hist = rl.history;
|
|
259
272
|
if (Array.isArray(hist)) {
|
|
@@ -262,7 +275,8 @@ export async function startREPL(options) {
|
|
|
262
275
|
}
|
|
263
276
|
catch (error) {
|
|
264
277
|
if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
|
|
265
|
-
|
|
278
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
279
|
+
console.warn(`[Debug Warning] Failed to write history file on exit: ${msg}`);
|
|
266
280
|
}
|
|
267
281
|
}
|
|
268
282
|
disableMouseReportingSync();
|
|
@@ -286,39 +300,50 @@ export async function startREPL(options) {
|
|
|
286
300
|
let sigintCount = 0;
|
|
287
301
|
let lastSigintTime = 0;
|
|
288
302
|
let sigintResetTimer = null;
|
|
303
|
+
// Dedup guard: prevents double-firing when both `rl` and `process` SIGINT listeners fire.
|
|
304
|
+
let sigintHandling = false;
|
|
289
305
|
const sigintHandler = () => {
|
|
290
|
-
if (
|
|
291
|
-
// A task is running — cancel it instead of exiting
|
|
292
|
-
currentRunningAgent.abort();
|
|
306
|
+
if (sigintHandling)
|
|
293
307
|
return;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
308
|
+
sigintHandling = true;
|
|
309
|
+
try {
|
|
310
|
+
if (isTaskRunning && currentRunningAgent) {
|
|
311
|
+
// A task is running — cancel it instead of exiting
|
|
312
|
+
currentRunningAgent.abort();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
if (now - lastSigintTime > SIGINT_RESET_MS) {
|
|
317
|
+
// First press (or after reset window)
|
|
318
|
+
sigintCount = 1;
|
|
319
|
+
lastSigintTime = now;
|
|
320
|
+
// Write hint and redraw the prompt
|
|
321
|
+
const promptStr = `> `;
|
|
322
|
+
process.stdout.write(`\n${c.yellow}⚠ Press Ctrl+C again to exit${c.reset}\n`);
|
|
323
|
+
drawLavaStatusBar();
|
|
324
|
+
process.stdout.write(`${c.dim}─────────────────────────────────────────────────────────────────${c.reset}\n`);
|
|
325
|
+
process.stdout.write(promptStr);
|
|
326
|
+
// Auto-reset after the window expires
|
|
327
|
+
if (sigintResetTimer)
|
|
328
|
+
clearTimeout(sigintResetTimer);
|
|
329
|
+
sigintResetTimer = setTimeout(() => {
|
|
330
|
+
sigintCount = 0;
|
|
331
|
+
sigintResetTimer = null;
|
|
332
|
+
}, SIGINT_RESET_MS);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Second press within the window — exit
|
|
306
336
|
if (sigintResetTimer)
|
|
307
337
|
clearTimeout(sigintResetTimer);
|
|
308
|
-
sigintResetTimer =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
338
|
+
sigintResetTimer = null;
|
|
339
|
+
sigintCount = 0;
|
|
340
|
+
exitCleanup();
|
|
341
|
+
console.log('\n\n👋 FixO CLI session ended safely. Core engine offline.');
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
sigintHandling = false;
|
|
313
346
|
}
|
|
314
|
-
// Second press within the window — exit
|
|
315
|
-
if (sigintResetTimer)
|
|
316
|
-
clearTimeout(sigintResetTimer);
|
|
317
|
-
sigintResetTimer = null;
|
|
318
|
-
sigintCount = 0;
|
|
319
|
-
exitCleanup();
|
|
320
|
-
console.log('\n\n👋 FixO CLI session ended safely. Core engine offline.');
|
|
321
|
-
process.exit(0);
|
|
322
347
|
};
|
|
323
348
|
// Listen on both the readline interface (catches Ctrl+C during rl.question())
|
|
324
349
|
// and the process (fallback for non-readline scenarios).
|
|
@@ -355,7 +380,6 @@ export async function startREPL(options) {
|
|
|
355
380
|
enableMouseReporting();
|
|
356
381
|
const currentCursor = rl.cursor;
|
|
357
382
|
let output = '\n';
|
|
358
|
-
const width = 60;
|
|
359
383
|
const borderTop = `${c.snow}┌────────────────────────────────────────────────────────┐${c.reset}\n`;
|
|
360
384
|
const borderBottom = `${c.snow}└────────────────────────────────────────────────────────┘${c.reset}`;
|
|
361
385
|
output += borderTop;
|
|
@@ -468,7 +492,8 @@ export async function startREPL(options) {
|
|
|
468
492
|
}
|
|
469
493
|
catch (error) {
|
|
470
494
|
if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
|
|
471
|
-
|
|
495
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
496
|
+
console.warn(`[Debug Warning] Failed to load skills list: ${msg}`);
|
|
472
497
|
}
|
|
473
498
|
}
|
|
474
499
|
const matchingFiles = workspaceFiles.filter(f => f.toLowerCase().includes(q) || path.basename(f).toLowerCase().startsWith(q));
|
|
@@ -498,6 +523,7 @@ export async function startREPL(options) {
|
|
|
498
523
|
readline.emitKeypressEvents(process.stdin);
|
|
499
524
|
if (process.stdin.isTTY) {
|
|
500
525
|
process.stdin.setRawMode(true);
|
|
526
|
+
process.stdout.write('\x1b[?2004h');
|
|
501
527
|
}
|
|
502
528
|
const keypressHandler = (_char, key) => {
|
|
503
529
|
if (!isPrompting)
|
|
@@ -535,6 +561,30 @@ export async function startREPL(options) {
|
|
|
535
561
|
};
|
|
536
562
|
process.stdin.on('keypress', keypressHandler);
|
|
537
563
|
let mouseBuffer = '';
|
|
564
|
+
function getPasteTokenAtCursorForBackspace(line, cursor) {
|
|
565
|
+
const regex = /\[Paste #(\d+) \+\d+ lines\]/g;
|
|
566
|
+
let match;
|
|
567
|
+
while ((match = regex.exec(line)) !== null) {
|
|
568
|
+
const start = match.index;
|
|
569
|
+
const end = regex.lastIndex;
|
|
570
|
+
if (cursor > start && cursor <= end) {
|
|
571
|
+
return { id: parseInt(match[1], 10), start, end };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
function getPasteTokenAtCursorForDelete(line, cursor) {
|
|
577
|
+
const regex = /\[Paste #(\d+) \+\d+ lines\]/g;
|
|
578
|
+
let match;
|
|
579
|
+
while ((match = regex.exec(line)) !== null) {
|
|
580
|
+
const start = match.index;
|
|
581
|
+
const end = regex.lastIndex;
|
|
582
|
+
if (cursor >= start && cursor < end) {
|
|
583
|
+
return { id: parseInt(match[1], 10), start, end };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
538
588
|
// Monkey-patch process.stdin.emit to intercept keypress and mouse events
|
|
539
589
|
const originalEmit = process.stdin.emit;
|
|
540
590
|
process.stdin.emit = function (event, ...args) {
|
|
@@ -543,6 +593,78 @@ export async function startREPL(options) {
|
|
|
543
593
|
if (rawData) {
|
|
544
594
|
let str = mouseBuffer + rawData.toString();
|
|
545
595
|
mouseBuffer = '';
|
|
596
|
+
// ── Bracketed Paste Interception ──────────────────────────────
|
|
597
|
+
// This fires when the terminal supports bracketed paste mode
|
|
598
|
+
// (\x1b[?2004h is enabled in promptForInput on every render).
|
|
599
|
+
if (str.includes('\x1b[200~')) {
|
|
600
|
+
const parts = str.split('\x1b[200~');
|
|
601
|
+
// Any characters before the paste-start marker are real keystrokes
|
|
602
|
+
if (parts[0]) {
|
|
603
|
+
originalEmit.apply(this, ['data', Buffer.from(parts[0])]);
|
|
604
|
+
}
|
|
605
|
+
isPasting = true;
|
|
606
|
+
pasteBuffer = '';
|
|
607
|
+
str = parts.slice(1).join('\x1b[200~');
|
|
608
|
+
}
|
|
609
|
+
if (isPasting) {
|
|
610
|
+
if (str.includes('\x1b[201~')) {
|
|
611
|
+
const parts = str.split('\x1b[201~');
|
|
612
|
+
pasteBuffer += parts[0];
|
|
613
|
+
isPasting = false;
|
|
614
|
+
const rawLines = pasteBuffer.split(/\r\n|\r|\n/);
|
|
615
|
+
// Trim a single trailing empty line that terminals often append
|
|
616
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
|
|
617
|
+
rawLines.pop();
|
|
618
|
+
}
|
|
619
|
+
if (rawLines.length > 1) {
|
|
620
|
+
// Multi-line paste → attachment
|
|
621
|
+
const id = pasteIdCounter++;
|
|
622
|
+
pendingPastes.push({ id, content: pasteBuffer.replace(/\r\n/g, '\n'), lines: rawLines.length });
|
|
623
|
+
injectTokenIntoPrompt(pasteToken(id, rawLines.length));
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
// Single line → let it flow into rl normally
|
|
627
|
+
rl.write(pasteBuffer);
|
|
628
|
+
}
|
|
629
|
+
pasteBuffer = '';
|
|
630
|
+
str = parts.slice(1).join('\x1b[201~');
|
|
631
|
+
if (str.length === 0)
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// Still accumulating paste data
|
|
636
|
+
pasteBuffer += str;
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// ── Heuristic Paste Fallback ──────────────────────────────────
|
|
641
|
+
// For terminals that strip bracketed paste codes, multi-line
|
|
642
|
+
// pastes arrive as a single large data chunk containing \n chars.
|
|
643
|
+
// Humans cannot produce this pattern; only paste events do.
|
|
644
|
+
//
|
|
645
|
+
// Guards:
|
|
646
|
+
// 1. Not already in isPasting mode (handled above).
|
|
647
|
+
// 2. The chunk is NOT a bare Enter keypress.
|
|
648
|
+
// 3. At least 3 non-empty lines AND total length > 80 chars.
|
|
649
|
+
// (Prevents firing on "2\n" or any short accidental newline.)
|
|
650
|
+
if (!isPasting && str.includes('\n')) {
|
|
651
|
+
const isJustEnter = (str === '\r' || str === '\n' || str === '\r\n');
|
|
652
|
+
if (!isJustEnter && str.length > 80) {
|
|
653
|
+
const rawLines = str.split(/\r\n|\r|\n/);
|
|
654
|
+
// Remove a single trailing empty line
|
|
655
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
|
|
656
|
+
rawLines.pop();
|
|
657
|
+
}
|
|
658
|
+
const nonEmptyLines = rawLines.filter(l => l.trim().length > 0);
|
|
659
|
+
if (nonEmptyLines.length >= 3) {
|
|
660
|
+
const id = pasteIdCounter++;
|
|
661
|
+
pendingPastes.push({ id, content: str.replace(/\r\n/g, '\n'), lines: rawLines.length });
|
|
662
|
+
injectTokenIntoPrompt(pasteToken(id, rawLines.length));
|
|
663
|
+
// Swallow the chunk so readline never sees the \n characters
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
546
668
|
// Intercept cursor position response
|
|
547
669
|
if (str.startsWith('\x1b[') && str.endsWith('R')) {
|
|
548
670
|
const match = str.match(/\x1b\[(\d+);(\d+)R/);
|
|
@@ -634,6 +756,38 @@ export async function startREPL(options) {
|
|
|
634
756
|
}
|
|
635
757
|
if (event === 'keypress') {
|
|
636
758
|
const [char, key] = args;
|
|
759
|
+
if (isPrompting && key) {
|
|
760
|
+
if (key.name === 'backspace') {
|
|
761
|
+
const line = rl.line;
|
|
762
|
+
const cursor = rl.cursor;
|
|
763
|
+
const tokenMatch = getPasteTokenAtCursorForBackspace(line, cursor);
|
|
764
|
+
if (tokenMatch) {
|
|
765
|
+
const { id, start, end } = tokenMatch;
|
|
766
|
+
pendingPastes = pendingPastes.filter(p => p.id !== id);
|
|
767
|
+
const newLine = line.slice(0, start) + line.slice(end);
|
|
768
|
+
const newCursor = start;
|
|
769
|
+
rl.line = newLine;
|
|
770
|
+
rl.cursor = newCursor;
|
|
771
|
+
rl._refreshLine();
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
else if (key.name === 'delete') {
|
|
776
|
+
const line = rl.line;
|
|
777
|
+
const cursor = rl.cursor;
|
|
778
|
+
const tokenMatch = getPasteTokenAtCursorForDelete(line, cursor);
|
|
779
|
+
if (tokenMatch) {
|
|
780
|
+
const { id, start, end } = tokenMatch;
|
|
781
|
+
pendingPastes = pendingPastes.filter(p => p.id !== id);
|
|
782
|
+
const newLine = line.slice(0, start) + line.slice(end);
|
|
783
|
+
const newCursor = start;
|
|
784
|
+
rl.line = newLine;
|
|
785
|
+
rl.cursor = newCursor;
|
|
786
|
+
rl._refreshLine();
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
637
791
|
// Intercept Escape or Ctrl+C to cancel a running task (when not prompting)
|
|
638
792
|
if (key && key.name === 'escape' && isTaskRunning && currentRunningAgent) {
|
|
639
793
|
currentRunningAgent.abort();
|
|
@@ -657,17 +811,42 @@ export async function startREPL(options) {
|
|
|
657
811
|
// legacy dirLabel/branchLabel/modelLabel/modeLabel row
|
|
658
812
|
// is gone — the new bar carries all of that information.
|
|
659
813
|
drawLavaStatusBar();
|
|
660
|
-
process.stdout.write(`${
|
|
814
|
+
process.stdout.write(`${c.dim}─────────────────────────────────────────────────────────────────${c.reset}\n> `);
|
|
661
815
|
return true; // swallow keypress
|
|
662
816
|
}
|
|
663
817
|
}
|
|
664
818
|
return originalEmit.apply(this, [event, ...args]);
|
|
665
819
|
};
|
|
820
|
+
/**
|
|
821
|
+
* Injects `token` into the readline line buffer and redraws the prompt line.
|
|
822
|
+
* Any text already in rl.line is preserved and appended after the token
|
|
823
|
+
* with a space separator.
|
|
824
|
+
*
|
|
825
|
+
* This produces the Claude Code / Antigravity pattern:
|
|
826
|
+
* > [Paste #1 +45 lines] <any pre-paste text the user was typing>
|
|
827
|
+
*/
|
|
828
|
+
function injectTokenIntoPrompt(token) {
|
|
829
|
+
// 1. Capture whatever the user had typed before pasting
|
|
830
|
+
const preTyped = (rl.line ?? '').trimEnd();
|
|
831
|
+
// 2. Clear the entire current line visually
|
|
832
|
+
readline.clearLine(process.stdout, 0);
|
|
833
|
+
readline.cursorTo(process.stdout, 0);
|
|
834
|
+
// 3. Wipe rl's internal buffer with Ctrl-U so readline tracks zero length
|
|
835
|
+
rl.write(null, { ctrl: true, name: 'u' });
|
|
836
|
+
// 4. Write the token (+ pre-typed text if any) back into rl
|
|
837
|
+
const newLine = preTyped.length > 0 ? `${token} ${preTyped}` : token;
|
|
838
|
+
rl.write(newLine);
|
|
839
|
+
// rl.write() both updates rl.line and echoes the characters to stdout,
|
|
840
|
+
// so the user sees: > [Paste #1 +45 lines] Refactor th
|
|
841
|
+
// with the cursor positioned after the last character.
|
|
842
|
+
}
|
|
666
843
|
// ──── REPL loop ────
|
|
667
844
|
const promptForInput = () => {
|
|
668
845
|
// Restore raw mode and resume streams to recover from any clack/spinner interactions
|
|
669
846
|
if (process.stdin.isTTY) {
|
|
670
847
|
process.stdin.setRawMode(true);
|
|
848
|
+
// Explicitly re-enable Bracketed Paste Mode just in case a spinner disabled it
|
|
849
|
+
process.stdout.write('\x1b[?2004h');
|
|
671
850
|
}
|
|
672
851
|
process.stdin.resume();
|
|
673
852
|
rl.resume();
|
|
@@ -677,7 +856,8 @@ export async function startREPL(options) {
|
|
|
677
856
|
// visible in the bar; the prompt itself is the lava `›` glyph.
|
|
678
857
|
drawLavaStatusBar();
|
|
679
858
|
isPrompting = true;
|
|
680
|
-
|
|
859
|
+
const promptPrefix = `\n${C.SNOW4}╭─${C.RESET} 👤 ${C.LAVA}${C.BOLD}User${C.RESET}\n${C.SNOW4}╰─❯${C.RESET} `;
|
|
860
|
+
rl.question(promptPrefix, async (input) => {
|
|
681
861
|
isPrompting = false;
|
|
682
862
|
disableMouseReporting();
|
|
683
863
|
clearSuggestions();
|
|
@@ -702,12 +882,45 @@ export async function startREPL(options) {
|
|
|
702
882
|
else if (msg.includes('429')) {
|
|
703
883
|
console.log(`${c.dim} → Rate limited. Wait a moment or add more API keys.${c.reset}`);
|
|
704
884
|
}
|
|
885
|
+
else if (msg.includes('404') || msg.toLowerCase().includes('model not found')) {
|
|
886
|
+
rl.pause();
|
|
887
|
+
const fallback = await p.confirm({
|
|
888
|
+
message: `Model '${currentModel}' not found or unavailable. Switch to default 'auto' model and retry?`,
|
|
889
|
+
initialValue: true,
|
|
890
|
+
});
|
|
891
|
+
rl.resume();
|
|
892
|
+
if (fallback && !p.isCancel(fallback)) {
|
|
893
|
+
console.log(`\n${c.dim}Switching to 'auto' and retrying...${c.reset}`);
|
|
894
|
+
currentModel = 'auto';
|
|
895
|
+
try {
|
|
896
|
+
await handleInput(trimmed);
|
|
897
|
+
}
|
|
898
|
+
catch (retryError) {
|
|
899
|
+
console.log(`\n${c.red}✗ Retry failed: ${retryError instanceof Error ? retryError.message : String(retryError)}${c.reset}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
705
903
|
}
|
|
706
904
|
promptForInput();
|
|
707
905
|
});
|
|
708
906
|
};
|
|
709
907
|
// ──── Input handler ────
|
|
710
|
-
async function handleInput(
|
|
908
|
+
async function handleInput(rawInput) {
|
|
909
|
+
// ── Payload assembly ─────────────────────────────────────────────
|
|
910
|
+
// Strip paste tokens from the user's typed text so the LLM only
|
|
911
|
+
// sees the clean question, not "[Paste #1 +45 lines]" literally.
|
|
912
|
+
const tokenPattern = /\[Paste #\d+ \+\d+ lines\]\s*/g;
|
|
913
|
+
const cleanRawInput = rawInput.replace(tokenPattern, '').trim();
|
|
914
|
+
// Build final LLM payload: question first, context blocks after.
|
|
915
|
+
let input = cleanRawInput;
|
|
916
|
+
if (pendingPastes.length > 0) {
|
|
917
|
+
const contextBlocks = pendingPastes
|
|
918
|
+
.map(p => `<pasted_context id="${p.id}">\n${p.content}\n</pasted_context>`)
|
|
919
|
+
.join('\n\n');
|
|
920
|
+
input = cleanRawInput.length > 0
|
|
921
|
+
? `${cleanRawInput}\n\n${contextBlocks}`
|
|
922
|
+
: contextBlocks;
|
|
923
|
+
}
|
|
711
924
|
// ─── Slash commands ───
|
|
712
925
|
if (input.startsWith('/')) {
|
|
713
926
|
const parts = input.split(/\s+/).filter(Boolean);
|
|
@@ -729,1099 +942,150 @@ export async function startREPL(options) {
|
|
|
729
942
|
case '/help':
|
|
730
943
|
printHelp();
|
|
731
944
|
return;
|
|
732
|
-
case '/
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
console.log(`\n${c.bold}${c.cyan}Available Models by Provider${c.reset}`);
|
|
738
|
-
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
739
|
-
for (const def of PROVIDER_REGISTRY) {
|
|
740
|
-
const hasKey = ProvidersManager.has(def.name);
|
|
741
|
-
const keyStatus = hasKey ? `${c.green}[key ✓]${c.reset}` : `${c.dim}[no key]${c.reset}`;
|
|
742
|
-
const cached = ProvidersManager.getCachedModels(def.name);
|
|
743
|
-
const modelList = cached?.models?.length ? cached.models : def.models;
|
|
744
|
-
const sourceTag = cached?.source === 'live'
|
|
745
|
-
? ''
|
|
746
|
-
: ` ${c.dim}[unverified]${c.reset}`;
|
|
747
|
-
console.log(`\n ${c.snow}${c.bold}${def.displayName}${c.reset} ${keyStatus}${sourceTag}`);
|
|
748
|
-
for (const model of modelList) {
|
|
749
|
-
console.log(` ${c.cyan}•${c.reset} ${model}`);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
console.log(`\n${c.dim} Use /providers add <name> to connect a provider with your API key.${c.reset}`);
|
|
753
|
-
console.log(`${c.dim} Or set model directly: /model <model-id>${c.reset}\n`);
|
|
945
|
+
case '/view': {
|
|
946
|
+
const id = parseInt(args[0] ?? '', 10);
|
|
947
|
+
if (isNaN(id)) {
|
|
948
|
+
console.log(`\n${c.yellow}⚠ Usage: /view <paste-id>${c.reset}`);
|
|
949
|
+
promptForInput();
|
|
754
950
|
return;
|
|
755
951
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
{ value: 'all', label: 'Show all models (flat list)', hint: 'classic view' },
|
|
763
|
-
...PROVIDER_REGISTRY.map(def => ({
|
|
764
|
-
value: def.name,
|
|
765
|
-
label: def.displayName,
|
|
766
|
-
hint: ProvidersManager.has(def.name) ? ' [key ✓]' : ' [no key]'
|
|
767
|
-
})),
|
|
768
|
-
{ value: '__manual__', label: 'Enter model ID manually…', hint: '' },
|
|
769
|
-
],
|
|
770
|
-
initialValue: PROVIDER_REGISTRY.find(def => def.models.includes(currentModel))?.name || 'all',
|
|
771
|
-
});
|
|
772
|
-
rl.resume();
|
|
773
|
-
if (p.isCancel(pickedProvider)) {
|
|
774
|
-
console.log(`\n${c.dim}Model unchanged: ${c.cyan}${currentModel}${c.reset}`);
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
if (pickedProvider === '__manual__') {
|
|
778
|
-
rl.pause();
|
|
779
|
-
const manual = await p.text({
|
|
780
|
-
message: 'Enter model ID:',
|
|
781
|
-
placeholder: 'e.g. gpt-4o, claude-opus-4-5, gemini-2.5-pro',
|
|
782
|
-
validate: v => !v.trim() ? 'Model ID is required' : undefined,
|
|
783
|
-
});
|
|
784
|
-
rl.resume();
|
|
785
|
-
if (!p.isCancel(manual) && manual) {
|
|
786
|
-
currentModel = manual.trim();
|
|
787
|
-
conversation.setContextLimit(currentModel);
|
|
788
|
-
console.log(`\n${c.green}✓ Model set to: ${c.bold}${currentModel}${c.reset}`);
|
|
789
|
-
}
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
if (pickedProvider === 'all') {
|
|
793
|
-
rl.pause();
|
|
794
|
-
const allOptions = PROVIDER_REGISTRY.flatMap(def => def.models.map(m => ({
|
|
795
|
-
value: m,
|
|
796
|
-
label: `${m}`,
|
|
797
|
-
hint: def.displayName + (ProvidersManager.has(def.name) ? ' [key ✓]' : ''),
|
|
798
|
-
})));
|
|
799
|
-
const picked = await p.select({
|
|
800
|
-
message: 'Select a model from the flat list:',
|
|
801
|
-
options: [
|
|
802
|
-
{ value: currentModel, label: `Keep current: ${currentModel}`, hint: 'no change' },
|
|
803
|
-
...allOptions,
|
|
804
|
-
],
|
|
805
|
-
initialValue: currentModel,
|
|
806
|
-
});
|
|
807
|
-
rl.resume();
|
|
808
|
-
if (p.isCancel(picked)) {
|
|
809
|
-
console.log(`\n${c.dim}Model unchanged: ${c.cyan}${currentModel}${c.reset}`);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
currentModel = picked;
|
|
813
|
-
// Store hint — find which provider this model belongs to
|
|
814
|
-
const owningDef = PROVIDER_REGISTRY.find(d => d.models.includes(currentModel)
|
|
815
|
-
|| ProvidersManager.getCachedModels(d.name)?.models?.includes(currentModel));
|
|
816
|
-
if (owningDef)
|
|
817
|
-
ProvidersManager.setModelProviderHint(currentModel, owningDef.name);
|
|
818
|
-
conversation.setContextLimit(currentModel);
|
|
819
|
-
console.log(`\n${c.green}✓ Model set to: ${c.bold}${currentModel}${c.reset}`);
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
const def = PROVIDER_REGISTRY.find(p => p.name === pickedProvider);
|
|
823
|
-
const hasKey = ProvidersManager.has(def.name);
|
|
824
|
-
const keyStatus = hasKey ? `${c.green}[key ✓]${c.reset}` : `${c.red}[no key]${c.reset}`;
|
|
825
|
-
// Prefer the cached live model list; fall back to the
|
|
826
|
-
// registry list (tagged `[unverified]`) when no fresh
|
|
827
|
-
// cache exists. Drops the synthetic "(free)" suffix
|
|
828
|
-
// since we no longer know that without provider
|
|
829
|
-
// metadata.
|
|
830
|
-
const cached = ProvidersManager.getCachedModels(def.name);
|
|
831
|
-
const modelList = cached?.models?.length ? cached.models : def.models;
|
|
832
|
-
const sourceSuffix = cached?.source === 'live'
|
|
833
|
-
? ''
|
|
834
|
-
: ` ${c.dim}[unverified]${c.reset}`;
|
|
835
|
-
rl.pause();
|
|
836
|
-
const picked = await p.select({
|
|
837
|
-
message: `Select a model from ${c.bold}${def.displayName}${c.reset} ${keyStatus}${sourceSuffix}:`,
|
|
838
|
-
options: modelList.map(m => {
|
|
839
|
-
return {
|
|
840
|
-
value: m,
|
|
841
|
-
label: m,
|
|
842
|
-
hint: m === currentModel ? 'currently selected' : ''
|
|
843
|
-
};
|
|
844
|
-
}),
|
|
845
|
-
initialValue: modelList.includes(currentModel) ? currentModel : undefined,
|
|
846
|
-
});
|
|
847
|
-
rl.resume();
|
|
848
|
-
if (p.isCancel(picked)) {
|
|
849
|
-
console.log(`\n${c.dim}Model unchanged: ${c.cyan}${currentModel}${c.reset}`);
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
currentModel = picked;
|
|
853
|
-
// Store explicit model-provider association so
|
|
854
|
-
// resolveDirectConfig can route this model directly
|
|
855
|
-
// to this provider (critical for live-fetched models
|
|
856
|
-
// that don't appear in the static registry).
|
|
857
|
-
ProvidersManager.setModelProviderHint(currentModel, def.name);
|
|
858
|
-
conversation.setContextLimit(currentModel);
|
|
859
|
-
console.log(`\n${c.green}✓ Model set to: ${c.bold}${currentModel}${c.reset}`);
|
|
952
|
+
const paste = pendingPastes.find(p => p.id === id);
|
|
953
|
+
if (!paste) {
|
|
954
|
+
console.log(`\n${c.yellow}⚠ Paste #${id} not found. Active pastes: ${pendingPastes.length > 0
|
|
955
|
+
? pendingPastes.map(p => `#${p.id}`).join(', ')
|
|
956
|
+
: 'none'}${c.reset}`);
|
|
957
|
+
promptForInput();
|
|
860
958
|
return;
|
|
861
959
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
console.log(
|
|
960
|
+
const border = `${c.dim}${'─'.repeat(60)}${c.reset}`;
|
|
961
|
+
console.log(`\n${border}`);
|
|
962
|
+
console.log(`${c.cyan}Paste #${paste.id} — ${paste.lines} lines${c.reset}`);
|
|
963
|
+
console.log(border);
|
|
964
|
+
console.log(paste.content);
|
|
965
|
+
console.log(border);
|
|
966
|
+
promptForInput();
|
|
865
967
|
return;
|
|
866
968
|
}
|
|
867
|
-
case '/
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
else {
|
|
873
|
-
console.log(`\n${c.dim}Selected files:${c.reset}`);
|
|
874
|
-
for (const f of selectedFiles) {
|
|
875
|
-
console.log(` ${c.cyan}${path.basename(f)}${c.reset} ${c.dim}(${f})${c.reset}`);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
969
|
+
case '/edit': {
|
|
970
|
+
const id = parseInt(args[0] ?? '', 10);
|
|
971
|
+
if (isNaN(id)) {
|
|
972
|
+
console.log(`\n${c.yellow}⚠ Usage: /edit <paste-id>${c.reset}`);
|
|
973
|
+
promptForInput();
|
|
878
974
|
return;
|
|
879
975
|
}
|
|
880
|
-
|
|
881
|
-
if (
|
|
882
|
-
(
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
let filePath;
|
|
886
|
-
try {
|
|
887
|
-
filePath = guard.ensureFile(rawPath);
|
|
888
|
-
}
|
|
889
|
-
catch (error) {
|
|
890
|
-
console.log(`\n${c.red}✗ ${error instanceof Error ? error.message : String(error)}${c.reset}`);
|
|
976
|
+
const paste = pendingPastes.find(p => p.id === id);
|
|
977
|
+
if (!paste) {
|
|
978
|
+
console.log(`\n${c.yellow}⚠ Paste #${id} not found.${c.reset}`);
|
|
979
|
+
promptForInput();
|
|
891
980
|
return;
|
|
892
981
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
case '/diff':
|
|
908
|
-
console.log(`\n${git.getDiff()}`);
|
|
909
|
-
return;
|
|
910
|
-
case '/undo': {
|
|
911
|
-
if (args[0]) {
|
|
912
|
-
console.log(`\n${undoRun(cwd, args[0])}`);
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
rl.pause();
|
|
916
|
-
const confirmed = await p.confirm({
|
|
917
|
-
message: 'Are you sure you want to completely discard the last automated agent commit and restore all files?',
|
|
918
|
-
initialValue: false,
|
|
919
|
-
});
|
|
920
|
-
rl.resume();
|
|
921
|
-
if (p.isCancel(confirmed) || !confirmed) {
|
|
922
|
-
console.log(`\n${c.yellow} ⚠ Undo cancelled.${c.reset}`);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
git.undoLastCommit();
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
case '/clear':
|
|
929
|
-
conversation.clear();
|
|
930
|
-
pendingAttachments = [];
|
|
931
|
-
console.log(`\n${c.green}✓ Conversation cleared${c.reset}`);
|
|
932
|
-
return;
|
|
933
|
-
case '/image': {
|
|
934
|
-
// `/image <path>` — queue a local image for the next turn.
|
|
935
|
-
// `/image clear` — drop the queue.
|
|
936
|
-
// `/image list` — show what's queued.
|
|
937
|
-
const sub = args[0];
|
|
938
|
-
if (sub === 'clear') {
|
|
939
|
-
const n = pendingAttachments.length;
|
|
940
|
-
pendingAttachments = [];
|
|
941
|
-
console.log(`\n${c.green}✓ Cleared ${n} pending image(s)${c.reset}`);
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
if (sub === 'list') {
|
|
945
|
-
if (pendingAttachments.length === 0) {
|
|
946
|
-
console.log(`\n${c.dim}No pending images.${c.reset}`);
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
console.log(`\n${c.bold}Pending images (sent on next prompt):${c.reset}`);
|
|
950
|
-
for (const [i, block] of pendingAttachments.entries()) {
|
|
951
|
-
if (block.type === 'image' && block.source.kind === 'base64') {
|
|
952
|
-
const approxBytes = Math.floor((block.source.data.length * 3) / 4);
|
|
953
|
-
console.log(` ${i + 1}. ${block.source.mediaType} (~${approxBytes} bytes)`);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
if (!sub) {
|
|
959
|
-
console.log(`\n${c.yellow}Usage: /image <path> | /image list | /image clear${c.reset}`);
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
const result = loadImageAsBlock(sub, cwd);
|
|
963
|
-
if (!result.ok) {
|
|
964
|
-
console.log(`\n${c.red}✗ /image: ${result.error}${c.reset}`);
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
pendingAttachments.push(result.block);
|
|
968
|
-
console.log(`\n${c.green}✓ Attached${c.reset} ${c.dim}${result.mediaType}, ${result.bytes} bytes — will be sent with your next prompt${c.reset}`);
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
case '/mcp': {
|
|
972
|
-
const sub = args[0]?.toLowerCase();
|
|
973
|
-
if (!sub || sub === 'list') {
|
|
974
|
-
const { listAllMcpSources, mergedMcpServers } = await import('../agent/mcp-registry.js');
|
|
975
|
-
const view = listAllMcpSources(cwd);
|
|
976
|
-
console.log(`\n${c.bold}${c.cyan}MCP Servers${c.reset} ${c.dim}(project-wins precedence: local > project > global)${c.reset}`);
|
|
977
|
-
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
978
|
-
const renderSource = (label, s) => {
|
|
979
|
-
const names = Object.keys(s.servers);
|
|
980
|
-
if (names.length === 0) {
|
|
981
|
-
console.log(` ${c.dim}${label}: (empty)${s.configPath ? ` ${c.dim}${s.configPath}${c.reset}` : ''}`);
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
console.log(` ${c.bold}${label}${c.reset}${s.configPath ? ` ${c.dim}${s.configPath}${c.reset}` : ''}`);
|
|
985
|
-
for (const n of names) {
|
|
986
|
-
console.log(` ${c.cyan}•${c.reset} ${n}`);
|
|
987
|
-
}
|
|
988
|
-
};
|
|
989
|
-
renderSource('global', view.global);
|
|
990
|
-
renderSource('project', view.project);
|
|
991
|
-
renderSource('local', view.local);
|
|
992
|
-
const merged = mergedMcpServers(cwd);
|
|
993
|
-
const mergedCount = Object.keys(merged).length;
|
|
994
|
-
console.log(`\n${c.dim}merged total: ${mergedCount} server(s)${c.reset}`);
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
if (sub === 'add') {
|
|
998
|
-
const name = args[1];
|
|
999
|
-
if (!name || args.length < 3) {
|
|
1000
|
-
console.log(`\n${c.yellow}Usage: /mcp add <name> <command> [args...]${c.reset}`);
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
const cmd = args[2];
|
|
1004
|
-
const cmdArgs = args.slice(3);
|
|
1005
|
-
const { addLocalMcpServer } = await import('../agent/mcp-registry.js');
|
|
1006
|
-
addLocalMcpServer(cwd, name, { command: cmd, args: cmdArgs, type: 'stdio' });
|
|
1007
|
-
console.log(`\n${c.green}✓ Added local MCP server:${c.reset} ${name} ${c.dim}(command=${cmd} args=${JSON.stringify(cmdArgs)})${c.reset}`);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
if (sub === 'remove' || sub === 'rm') {
|
|
1011
|
-
const name = args[1];
|
|
1012
|
-
if (!name) {
|
|
1013
|
-
console.log(`\n${c.yellow}Usage: /mcp remove <name>${c.reset}`);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
const { removeLocalMcpServer } = await import('../agent/mcp-registry.js');
|
|
1017
|
-
const removed = removeLocalMcpServer(cwd, name);
|
|
1018
|
-
if (removed) {
|
|
1019
|
-
console.log(`\n${c.green}✓ Removed local MCP server:${c.reset} ${name}`);
|
|
1020
|
-
}
|
|
1021
|
-
else {
|
|
1022
|
-
console.log(`\n${c.yellow}No local MCP server named ${name}${c.reset}`);
|
|
1023
|
-
}
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
if (sub === 'test') {
|
|
1027
|
-
const name = args[1];
|
|
1028
|
-
if (!name) {
|
|
1029
|
-
console.log(`\n${c.yellow}Usage: /mcp test <name>${c.reset}`);
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
const { mergedMcpServers } = await import('../agent/mcp-registry.js');
|
|
1033
|
-
const all = mergedMcpServers(cwd);
|
|
1034
|
-
const cfg = all[name];
|
|
1035
|
-
if (!cfg) {
|
|
1036
|
-
console.log(`\n${c.yellow}No MCP server named ${name} (in any source)${c.reset}`);
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
const hasCommand = typeof cfg.command === 'string';
|
|
1040
|
-
const hasUrl = typeof cfg.url === 'string';
|
|
1041
|
-
if (hasCommand || hasUrl) {
|
|
1042
|
-
console.log(`\n${c.green}✓ ${name}${c.reset} — config looks valid (${hasCommand ? 'stdio' : 'sse'})`);
|
|
982
|
+
const tmpFile = path.join(os.tmpdir(), `fixo-paste-${id}-${Date.now()}.txt`);
|
|
983
|
+
try {
|
|
984
|
+
fs.writeFileSync(tmpFile, paste.content, 'utf-8');
|
|
985
|
+
// Release the terminal before handing it to the external editor
|
|
986
|
+
if (process.stdin.isTTY)
|
|
987
|
+
process.stdin.setRawMode(false);
|
|
988
|
+
process.stdout.write('\x1b[?2004l'); // disable bracketed paste while editor is open
|
|
989
|
+
const editor = process.env.VISUAL ?? process.env.EDITOR ?? (os.platform() === 'win32' ? 'notepad' : 'nano');
|
|
990
|
+
const { spawnSync } = await import('child_process');
|
|
991
|
+
spawnSync(editor, [tmpFile], { stdio: 'inherit' });
|
|
992
|
+
const edited = fs.readFileSync(tmpFile, 'utf-8');
|
|
993
|
+
fs.unlinkSync(tmpFile);
|
|
994
|
+
if (edited.trim().length === 0) {
|
|
995
|
+
console.log(`\n${c.yellow}⚠ Editor returned empty content — paste #${id} unchanged.${c.reset}`);
|
|
1043
996
|
}
|
|
1044
997
|
else {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
console.log(`\n${c.yellow}Unknown /mcp subcommand: ${sub}. Use: list | add | remove | test${c.reset}`);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
case '/todo': {
|
|
1053
|
-
const sub = args[0]?.toLowerCase();
|
|
1054
|
-
if (!sub || sub === 'list' || sub === 'ls') {
|
|
1055
|
-
const list = loadTodoList(cwd);
|
|
1056
|
-
const summary = summariseTodoList(list);
|
|
1057
|
-
console.log('');
|
|
1058
|
-
console.log(renderTodoList(list));
|
|
1059
|
-
if (summary.length > 0) {
|
|
1060
|
-
console.log(`\n${c.dim}(${summary})${c.reset}`);
|
|
998
|
+
paste.content = edited;
|
|
999
|
+
paste.lines = edited.split(/\r?\n/).filter(l => l.length > 0).length;
|
|
1000
|
+
console.log(`\n${c.green}✓ Updated Paste #${id} (${paste.lines} lines)${c.reset}`);
|
|
1061
1001
|
}
|
|
1062
|
-
return;
|
|
1063
1002
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
const list = addItem(loadTodoList(cwd), { content: text });
|
|
1071
|
-
const result = saveTodoList(cwd, list);
|
|
1072
|
-
if (!result.ok) {
|
|
1073
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
console.log(`\n${c.green}✓ Added todo:${c.reset} ${text}`);
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
if (sub === 'done' || sub === 'complete' || sub === 'cancel') {
|
|
1080
|
-
const id = args[1];
|
|
1081
|
-
if (!id) {
|
|
1082
|
-
console.log(`\n${c.yellow}Usage: /todo ${sub} <id>${c.reset}`);
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
const status = sub === 'cancel' ? 'cancelled' : 'done';
|
|
1086
|
-
let list = loadTodoList(cwd);
|
|
1087
|
-
const exists = list.items.some((it) => it.id === id);
|
|
1088
|
-
if (!exists) {
|
|
1089
|
-
console.log(`\n${c.red}✗ No todo with id "${id}"${c.reset}`);
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
list = setItemStatus(list, { id, status });
|
|
1093
|
-
const result = saveTodoList(cwd, list);
|
|
1094
|
-
if (!result.ok) {
|
|
1095
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
console.log(`\n${c.green}✓ Marked ${status}${c.reset}`);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
if (sub === 'start' || sub === 'progress') {
|
|
1102
|
-
const id = args[1];
|
|
1103
|
-
if (!id) {
|
|
1104
|
-
console.log(`\n${c.yellow}Usage: /todo ${sub} <id>${c.reset}`);
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
let list = loadTodoList(cwd);
|
|
1108
|
-
const exists = list.items.some((it) => it.id === id);
|
|
1109
|
-
if (!exists) {
|
|
1110
|
-
console.log(`\n${c.red}✗ No todo with id "${id}"${c.reset}`);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
list = setItemStatus(list, { id, status: 'in_progress' });
|
|
1114
|
-
const result = saveTodoList(cwd, list);
|
|
1115
|
-
if (!result.ok) {
|
|
1116
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
console.log(`\n${c.green}✓ Marked in_progress${c.reset}`);
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
1123
|
-
const id = args[1];
|
|
1124
|
-
if (!id) {
|
|
1125
|
-
console.log(`\n${c.yellow}Usage: /todo remove <id>${c.reset}`);
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
let list = loadTodoList(cwd);
|
|
1129
|
-
const exists = list.items.some((it) => it.id === id);
|
|
1130
|
-
if (!exists) {
|
|
1131
|
-
console.log(`\n${c.red}✗ No todo with id "${id}"${c.reset}`);
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
list = removeItem(list, { id });
|
|
1135
|
-
const result = saveTodoList(cwd, list);
|
|
1136
|
-
if (!result.ok) {
|
|
1137
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
console.log(`\n${c.green}✓ Removed todo${c.reset}`);
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
if (sub === 'clear') {
|
|
1144
|
-
const list = loadTodoList(cwd);
|
|
1145
|
-
const kept = list.items.filter((it) => it.status !== 'done' && it.status !== 'cancelled');
|
|
1146
|
-
const result = saveTodoList(cwd, { ...list, items: kept, updatedAt: Date.now() });
|
|
1147
|
-
if (!result.ok) {
|
|
1148
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1149
|
-
return;
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
console.log(`\n${c.red}✗ /edit failed: ${err.message}${c.reset}`);
|
|
1005
|
+
try {
|
|
1006
|
+
fs.unlinkSync(tmpFile);
|
|
1150
1007
|
}
|
|
1151
|
-
|
|
1152
|
-
console.log(`\n${c.green}✓ Cleared ${cleared} completed todo(s)${c.reset}`);
|
|
1153
|
-
return;
|
|
1008
|
+
catch { /* already gone */ }
|
|
1154
1009
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
console.log(` done <id> Mark a todo as done`);
|
|
1161
|
-
console.log(` cancel <id> Cancel a todo`);
|
|
1162
|
-
console.log(` remove <id> Remove a todo entirely`);
|
|
1163
|
-
console.log(` clear Remove all done/cancelled todos`);
|
|
1164
|
-
return;
|
|
1010
|
+
finally {
|
|
1011
|
+
// Reclaim raw mode and bracketed paste before returning to REPL
|
|
1012
|
+
if (process.stdin.isTTY)
|
|
1013
|
+
process.stdin.setRawMode(true);
|
|
1014
|
+
process.stdout.write('\x1b[?2004h');
|
|
1165
1015
|
}
|
|
1166
|
-
|
|
1016
|
+
promptForInput();
|
|
1167
1017
|
return;
|
|
1168
1018
|
}
|
|
1169
|
-
case '/
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
case '/stats':
|
|
1173
|
-
printStats(stats);
|
|
1174
|
-
{
|
|
1175
|
-
const ctxTokens = conversation.getTotalTokens();
|
|
1176
|
-
const ctxLimit = conversation.getContextLimit();
|
|
1177
|
-
const ctxPct = Math.round((ctxTokens / ctxLimit) * 100);
|
|
1178
|
-
const hasSummary = conversation.getSummary() ? ' (compacted)' : '';
|
|
1179
|
-
console.log(`${c.cyan}${c.bold}📊 Context Window${c.reset}`);
|
|
1180
|
-
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
1181
|
-
console.log(` History messages: ${c.bold}${conversation.getMessageCount()}${c.reset}${hasSummary}`);
|
|
1182
|
-
console.log(` Context usage: ${c.bold}${(ctxTokens / 1000).toFixed(0)}k / ${(ctxLimit / 1000).toFixed(0)}k${c.reset} (${ctxPct}%)`);
|
|
1183
|
-
console.log(` Turns: ${c.bold}${conversation.getTurnCount()}${c.reset}`);
|
|
1184
|
-
console.log('');
|
|
1019
|
+
case '/pastes': {
|
|
1020
|
+
if (pendingPastes.length === 0) {
|
|
1021
|
+
console.log(`\n${c.dim}No active paste attachments.${c.reset}`);
|
|
1185
1022
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
? `\n${runs.map(run => `${run.id} ${run.status} ${run.task.slice(0, 80)}`).join('\n')}`
|
|
1191
|
-
: '\n(no FixO runs recorded)');
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
case '/show-run':
|
|
1195
|
-
console.log(`\n${showRun(cwd, args[0] ?? '')}`);
|
|
1196
|
-
return;
|
|
1197
|
-
case '/memory':
|
|
1198
|
-
console.log(`\n${readMemory(cwd)}`);
|
|
1199
|
-
return;
|
|
1200
|
-
case '/remember': {
|
|
1201
|
-
const text = args.join(' ').trim();
|
|
1202
|
-
if (!text) {
|
|
1203
|
-
console.log(`\n${c.yellow}Usage: /remember <project fact>${c.reset}`);
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
rl.pause();
|
|
1207
|
-
const confirmed = await p.confirm({ message: `Add to project memory: ${text}?`, initialValue: false });
|
|
1208
|
-
rl.resume();
|
|
1209
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1210
|
-
appendMemory(cwd, text);
|
|
1211
|
-
console.log(`\n${c.green}✓ Memory updated${c.reset}`);
|
|
1212
|
-
}
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
case '/forget':
|
|
1216
|
-
rl.pause();
|
|
1217
|
-
{
|
|
1218
|
-
const confirmed = await p.confirm({ message: 'Clear FixO project memory?', initialValue: false });
|
|
1219
|
-
rl.resume();
|
|
1220
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1221
|
-
forgetMemory(cwd);
|
|
1222
|
-
console.log(`\n${c.green}✓ Memory cleared${c.reset}`);
|
|
1023
|
+
else {
|
|
1024
|
+
console.log(`\n${c.cyan}Active paste attachments:${c.reset}`);
|
|
1025
|
+
for (const p of pendingPastes) {
|
|
1026
|
+
console.log(` ${c.bold}#${p.id}${c.reset} ${p.lines} lines /view ${p.id} · /edit ${p.id}`);
|
|
1223
1027
|
}
|
|
1224
1028
|
}
|
|
1225
|
-
|
|
1226
|
-
case '/doctor':
|
|
1227
|
-
console.log(`\n${doctor(cwd)}`);
|
|
1228
|
-
return;
|
|
1229
|
-
case '/index': {
|
|
1230
|
-
const index = await buildIndex(cwd);
|
|
1231
|
-
workspaceFiles = index.files.map(f => f.path);
|
|
1232
|
-
console.log(`\n${c.green}✓ Indexed ${index.files.length} files${c.reset}`);
|
|
1029
|
+
promptForInput();
|
|
1233
1030
|
return;
|
|
1234
1031
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
const maxAttempts = 3;
|
|
1255
|
-
const modifiedFiles = [];
|
|
1256
|
-
while (attempt <= maxAttempts) {
|
|
1257
|
-
console.log(`\n${c.cyan}🔨 [Auto-Fix] Test failure detected (Attempt ${attempt}/${maxAttempts}). Invoking SingleAgent to repair...${c.reset}`);
|
|
1258
|
-
console.log(`${c.dim}${testResult}${c.reset}\n`);
|
|
1259
|
-
const repairTask = `The project tests are failing. Here is the test runner output:\n\n${testResult}\n\nPlease identify the files causing the failure, modify them to fix the issues, verify using the test commands, and ensure they pass.`;
|
|
1260
|
-
const context = {
|
|
1261
|
-
task: repairTask,
|
|
1262
|
-
model: currentModel,
|
|
1032
|
+
default: {
|
|
1033
|
+
const handler = commandRegistry[cmd];
|
|
1034
|
+
if (handler) {
|
|
1035
|
+
const ctx = {
|
|
1036
|
+
state: {
|
|
1037
|
+
currentModel,
|
|
1038
|
+
currentMode,
|
|
1039
|
+
currentSessionId,
|
|
1040
|
+
currentSessionLabel,
|
|
1041
|
+
sessionModifiedFiles,
|
|
1042
|
+
pendingAttachments,
|
|
1043
|
+
selectedFiles,
|
|
1044
|
+
stats,
|
|
1045
|
+
isTaskRunning,
|
|
1046
|
+
currentRunningAgent,
|
|
1047
|
+
},
|
|
1048
|
+
args,
|
|
1049
|
+
config,
|
|
1050
|
+
projectConfig,
|
|
1263
1051
|
cwd,
|
|
1264
1052
|
verbose,
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1053
|
+
conversation,
|
|
1054
|
+
agent,
|
|
1055
|
+
git,
|
|
1056
|
+
guard,
|
|
1057
|
+
rl,
|
|
1058
|
+
handleInput,
|
|
1059
|
+
clearSuggestions,
|
|
1060
|
+
refreshModelsForProvider,
|
|
1061
|
+
printStats,
|
|
1062
|
+
listRuns,
|
|
1063
|
+
showRun,
|
|
1064
|
+
buildIndex,
|
|
1065
|
+
workspaceFiles,
|
|
1066
|
+
findInIndex,
|
|
1067
|
+
explainIndexedTarget,
|
|
1271
1068
|
};
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
isTaskRunning = false;
|
|
1287
|
-
currentRunningAgent = null;
|
|
1288
|
-
agent.reset();
|
|
1289
|
-
}
|
|
1290
|
-
testResult = runProjectTests(cwd);
|
|
1291
|
-
if (testResult.includes('Status: 0')) {
|
|
1292
|
-
console.log(`\n${c.green}✓ All tests passed after repair attempt ${attempt}!${c.reset}`);
|
|
1293
|
-
break;
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
attempt++;
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
if (!testResult.includes('Status: 0')) {
|
|
1300
|
-
console.log(`\n${c.red}✗ Auto-fix failed after ${maxAttempts} attempts. Remaining failures:${c.reset}`);
|
|
1301
|
-
console.log(`${c.dim}${testResult}${c.reset}`);
|
|
1302
|
-
}
|
|
1303
|
-
else {
|
|
1304
|
-
// Auto-commit if enabled and changes were made
|
|
1305
|
-
if (config.preferences.autoCommit &&
|
|
1306
|
-
(projectConfig?.autoCommit !== false) &&
|
|
1307
|
-
modifiedFiles.length > 0) {
|
|
1308
|
-
console.log(`\n${c.green}✓ Auto-committing repaired test files...${c.reset}`);
|
|
1309
|
-
git.autoCommit('fix-tests: repair test failures', modifiedFiles);
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
case '/fix-ci':
|
|
1315
|
-
console.log(`\n${c.yellow}/fix-ci local mode: paste CI logs into a task or save them to a workspace file, then ask FixO to inspect that file.${c.reset}`);
|
|
1316
|
-
return;
|
|
1317
|
-
case '/plan':
|
|
1318
|
-
{
|
|
1319
|
-
const task = args.join(' ').trim();
|
|
1320
|
-
if (!task) {
|
|
1321
|
-
console.log(`\n${c.yellow}Usage: /plan <task>${c.reset}`);
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
const plan = savePlan(cwd, task);
|
|
1325
|
-
console.log(`\n${renderPlan(plan)}`);
|
|
1326
|
-
}
|
|
1327
|
-
return;
|
|
1328
|
-
case '/run-plan': {
|
|
1329
|
-
const dagFile = path.join(cwd, '.fixo', 'last-dag.json');
|
|
1330
|
-
if (fs.existsSync(dagFile)) {
|
|
1331
|
-
try {
|
|
1332
|
-
const { task, dag } = JSON.parse(fs.readFileSync(dagFile, 'utf-8'));
|
|
1333
|
-
console.log(`\n${c.cyan}[Saved Plan] Executing saved subtasks DAG for task: ${c.bold}${task}${c.reset}`);
|
|
1334
|
-
const { AgentPool } = await import('../agent/agent-pool.js');
|
|
1335
|
-
const pool = new AgentPool(3, projectConfig?.maxAttempts ?? 12);
|
|
1336
|
-
const context = {
|
|
1337
|
-
task,
|
|
1338
|
-
model: currentModel,
|
|
1339
|
-
cwd,
|
|
1340
|
-
verbose,
|
|
1341
|
-
selectedFiles: [...selectedFiles],
|
|
1342
|
-
systemPromptOverride: projectConfig?.systemPrompt,
|
|
1343
|
-
checkCommand: projectConfig?.checkCommand,
|
|
1344
|
-
policy: projectConfig?.policy ?? config.preferences.policy,
|
|
1345
|
-
mode: currentMode,
|
|
1346
|
-
};
|
|
1347
|
-
const success = await pool.execute(context, dag);
|
|
1348
|
-
if (success) {
|
|
1349
|
-
console.log(`\n${c.green}✓ Successfully completed complex task via parallel agents.${c.reset}`);
|
|
1350
|
-
}
|
|
1351
|
-
else {
|
|
1352
|
-
console.log(`\n${c.red}✗ Parallel workers failed to complete all subtasks.${c.reset}`);
|
|
1353
|
-
if (git.isGitRepo()) {
|
|
1354
|
-
console.log(`\n${c.yellow}[Agent Pool] Rolling back all uncommitted changes due to run failure...${c.reset}`);
|
|
1355
|
-
git.discardUncommittedChanges();
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
return;
|
|
1359
|
-
}
|
|
1360
|
-
catch (err) {
|
|
1361
|
-
console.log(`\n${c.red}✗ Failed to run saved DAG: ${err.message}${c.reset}`);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
const plan = loadPlan(cwd);
|
|
1365
|
-
if (!plan) {
|
|
1366
|
-
console.log(`\n${c.yellow}No saved plan or DAG. Generate one with /plan <task> or run a complex task in PLAN mode.${c.reset}`);
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
console.log(`\n${c.dim}Executing saved plan task: ${plan.task}${c.reset}`);
|
|
1370
|
-
await handleInput(plan.task);
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
case '/mode': {
|
|
1374
|
-
rl.pause();
|
|
1375
|
-
const selected = await p.select({
|
|
1376
|
-
message: 'Select execution mode:',
|
|
1377
|
-
options: [
|
|
1378
|
-
{ value: 'PLAN', label: 'PLAN Mode (Read-only, dry-run simulation)' },
|
|
1379
|
-
{ value: 'BUILD', label: 'BUILD Mode (Writing & modifying allowed)' },
|
|
1380
|
-
{ value: 'EXPLORE', label: 'EXPLORE Mode (Code exploration & LSP, no modifying)' },
|
|
1381
|
-
{ value: 'SCOUT', label: 'SCOUT Mode (Web search & fetch only)' },
|
|
1382
|
-
],
|
|
1383
|
-
initialValue: currentMode,
|
|
1384
|
-
});
|
|
1385
|
-
rl.resume();
|
|
1386
|
-
if (!p.isCancel(selected) && selected) {
|
|
1387
|
-
currentMode = selected;
|
|
1388
|
-
console.log(`\n${c.green}✓ Execution mode set to: ${c.bold}${currentMode}${c.reset}`);
|
|
1389
|
-
}
|
|
1390
|
-
else {
|
|
1391
|
-
console.log(`\n${c.dim}Execution mode remains: ${c.cyan}${currentMode}${c.reset}`);
|
|
1392
|
-
}
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
case '/rename': {
|
|
1396
|
-
// Renames the *active* session. Accepts the rest of the
|
|
1397
|
-
// input as a free-form label (so spaces don't need quoting).
|
|
1398
|
-
const rawLabel = args.join(' ').trim();
|
|
1399
|
-
const { isValidSessionLabel, MAX_LABEL_LENGTH } = await import('../runtime/session-snapshots.js');
|
|
1400
|
-
const { SessionManager } = await import('../agent/conversation.js');
|
|
1401
|
-
if (!rawLabel) {
|
|
1402
|
-
console.log(`\n${c.yellow}Usage: /rename <label>${c.reset}\n` +
|
|
1403
|
-
`${c.dim} Labels are 1..${MAX_LABEL_LENGTH} chars: letters, digits, space, dash, underscore, dot.${c.reset}`);
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
if (!isValidSessionLabel(rawLabel)) {
|
|
1407
|
-
console.log(`\n${c.red}✗ Invalid label.${c.reset} ${c.dim}Allowed: letters, digits, space, dash, underscore, dot — max ${MAX_LABEL_LENGTH} chars.${c.reset}`);
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
// Persist if the session has already been saved at least
|
|
1411
|
-
// once; otherwise just remember the label in memory until
|
|
1412
|
-
// the next save fires.
|
|
1413
|
-
try {
|
|
1414
|
-
SessionManager.renameSession(currentSessionId, rawLabel);
|
|
1415
|
-
}
|
|
1416
|
-
catch {
|
|
1417
|
-
/* tolerate first-rename-before-save */
|
|
1418
|
-
}
|
|
1419
|
-
currentSessionLabel = rawLabel;
|
|
1420
|
-
console.log(`\n${c.green}✓ Session renamed:${c.reset} ${c.cyan}${rawLabel}${c.reset} ${c.dim}(id: ${currentSessionId})${c.reset}`);
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
case '/session': {
|
|
1424
|
-
const sub = args[0];
|
|
1425
|
-
const { SessionManager } = await import('../agent/conversation.js');
|
|
1426
|
-
if (sub === 'rename') {
|
|
1427
|
-
const id = args[1];
|
|
1428
|
-
const rawLabel = args.slice(2).join(' ').trim();
|
|
1429
|
-
const { isValidSessionLabel, MAX_LABEL_LENGTH } = await import('../runtime/session-snapshots.js');
|
|
1430
|
-
if (!id || !rawLabel) {
|
|
1431
|
-
console.log(`\n${c.yellow}Usage: /session rename <id> <label>${c.reset}`);
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
if (!isValidSessionLabel(rawLabel)) {
|
|
1435
|
-
console.log(`\n${c.red}✗ Invalid label.${c.reset} ${c.dim}Max ${MAX_LABEL_LENGTH} chars; letters, digits, space, dash, underscore, dot only.${c.reset}`);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
const ok = SessionManager.renameSession(id, rawLabel);
|
|
1439
|
-
if (!ok) {
|
|
1440
|
-
console.log(`\n${c.red}✗ Session not found: ${id}${c.reset}`);
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
if (id === currentSessionId)
|
|
1444
|
-
currentSessionLabel = rawLabel;
|
|
1445
|
-
console.log(`\n${c.green}✓ Renamed${c.reset} ${c.dim}${id}${c.reset} → ${c.cyan}${rawLabel}${c.reset}`);
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
if (sub === 'list') {
|
|
1449
|
-
const list = SessionManager.listSessions();
|
|
1450
|
-
if (list.length === 0) {
|
|
1451
|
-
console.log(`\n${c.dim}No saved sessions found.${c.reset}`);
|
|
1452
|
-
}
|
|
1453
|
-
else {
|
|
1454
|
-
console.log(`\n${c.cyan}${c.bold}Saved Sessions:${c.reset}`);
|
|
1455
|
-
for (const s of list) {
|
|
1456
|
-
const date = new Date(s.timestamp).toLocaleString();
|
|
1457
|
-
const labelDisplay = s.label
|
|
1458
|
-
? `${c.cyan}${s.label}${c.reset} ${c.dim}(${s.sessionId.slice(0, 8)})${c.reset}`
|
|
1459
|
-
: `${c.cyan}${s.sessionId}${c.reset}`;
|
|
1460
|
-
console.log(` ${labelDisplay} - ${c.bold}${s.model}${c.reset} (${s.messageCount} msgs)`);
|
|
1461
|
-
console.log(` ${c.dim}Created: ${date} | Tokens: ${s.totalTokens.toLocaleString()}${c.reset}`);
|
|
1462
|
-
if (s.summary) {
|
|
1463
|
-
console.log(` ${c.dim}Summary: ${s.summary.slice(0, 80)}...${c.reset}`);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
else if (sub === 'load') {
|
|
1469
|
-
const uuid = args[1];
|
|
1470
|
-
if (!uuid) {
|
|
1471
|
-
console.log(`\n${c.yellow}Usage: /session load <uuid>${c.reset}`);
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
try {
|
|
1475
|
-
const data = SessionManager.loadSession(uuid);
|
|
1476
|
-
conversation.clear();
|
|
1477
|
-
conversation.importHistory(data.history);
|
|
1478
|
-
conversation.setSummary(data.summary || '');
|
|
1479
|
-
currentModel = data.model;
|
|
1480
|
-
conversation.setContextLimit(currentModel);
|
|
1481
|
-
sessionModifiedFiles = data.modifiedFiles || [];
|
|
1482
|
-
currentSessionId = data.sessionId;
|
|
1483
|
-
currentSessionLabel = data.label;
|
|
1484
|
-
stats.totalPromptTokens = data.tokenUsage?.prompt_tokens || 0;
|
|
1485
|
-
stats.totalCompletionTokens = data.tokenUsage?.completion_tokens || 0;
|
|
1486
|
-
console.log(`\n${c.green}✓ Session restored successfully: ${c.bold}${uuid}${c.reset}`);
|
|
1487
|
-
console.log(`${c.dim} Model set to: ${c.cyan}${currentModel}${c.reset}`);
|
|
1488
|
-
}
|
|
1489
|
-
catch (err) {
|
|
1490
|
-
console.log(`\n${c.red}✗ Failed to load session: ${err.message}${c.reset}`);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
else if (sub === 'new') {
|
|
1494
|
-
conversation.clear();
|
|
1495
|
-
sessionModifiedFiles = [];
|
|
1496
|
-
stats.totalPromptTokens = 0;
|
|
1497
|
-
stats.totalCompletionTokens = 0;
|
|
1498
|
-
stats.totalToolCalls = 0;
|
|
1499
|
-
stats.totalTasks = 0;
|
|
1500
|
-
stats.totalDurationMs = 0;
|
|
1501
|
-
const { randomUUID } = await import('node:crypto');
|
|
1502
|
-
currentSessionId = randomUUID();
|
|
1503
|
-
currentSessionLabel = undefined;
|
|
1504
|
-
SessionManager.saveSession(conversation, currentModel, sessionModifiedFiles, {
|
|
1505
|
-
prompt_tokens: stats.totalPromptTokens,
|
|
1506
|
-
completion_tokens: stats.totalCompletionTokens,
|
|
1507
|
-
total_tokens: stats.totalPromptTokens + stats.totalCompletionTokens,
|
|
1508
|
-
}, currentSessionId, currentSessionLabel);
|
|
1509
|
-
try {
|
|
1510
|
-
const { saveSnapshot } = await import('../runtime/session-snapshots.js');
|
|
1511
|
-
saveSnapshot({
|
|
1512
|
-
cwd,
|
|
1513
|
-
conversation: [],
|
|
1514
|
-
tokens: 0,
|
|
1515
|
-
model: currentModel,
|
|
1516
|
-
mode: currentMode,
|
|
1517
|
-
selectedFiles: [],
|
|
1518
|
-
summary: '',
|
|
1519
|
-
label: undefined,
|
|
1520
|
-
id: currentSessionId,
|
|
1521
|
-
fixedInstructions: projectConfig?.systemPrompt,
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
catch {
|
|
1525
|
-
// Ignore snapshot save errors on new session
|
|
1526
|
-
}
|
|
1527
|
-
console.log(`\n${c.green}✓ Active conversation memory purged. New session initialized: ${c.bold}${currentSessionId}${c.reset}`);
|
|
1528
|
-
}
|
|
1529
|
-
else {
|
|
1530
|
-
console.log(`\n${c.yellow}Usage: /session [list | load <uuid> | new | rename <id> <label>]${c.reset}`);
|
|
1531
|
-
}
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
case '/providers': {
|
|
1535
|
-
const sub = args[0];
|
|
1536
|
-
// ── Interactive flow (bare `/providers`): mirrors the
|
|
1537
|
-
// /model picker shape. The user picks a provider, then
|
|
1538
|
-
// an action, then enters a masked API key via p.password
|
|
1539
|
-
// when the action is add/update. The legacy text routes
|
|
1540
|
-
// below remain unchanged for muscle-memory + scripting.
|
|
1541
|
-
if (!sub) {
|
|
1542
|
-
rl.pause();
|
|
1543
|
-
const pickedProvider = await p.select({
|
|
1544
|
-
message: 'Select an AI provider:',
|
|
1545
|
-
options: PROVIDER_REGISTRY.map(def => ({
|
|
1546
|
-
value: def.name,
|
|
1547
|
-
label: def.displayName,
|
|
1548
|
-
hint: ProvidersManager.has(def.name) ? '[key ✓]' : '[no key]',
|
|
1549
|
-
})),
|
|
1550
|
-
});
|
|
1551
|
-
rl.resume();
|
|
1552
|
-
if (p.isCancel(pickedProvider)) {
|
|
1553
|
-
console.log(`\n${c.dim}/providers cancelled.${c.reset}`);
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
const def = ProvidersManager.getDefinition(pickedProvider);
|
|
1557
|
-
if (!def) {
|
|
1558
|
-
console.log(`\n${c.red}✗ Unknown provider: ${pickedProvider}${c.reset}`);
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
const hasKey = ProvidersManager.has(def.name);
|
|
1562
|
-
rl.pause();
|
|
1563
|
-
const action = await p.select({
|
|
1564
|
-
message: `${def.displayName} — choose an action:`,
|
|
1565
|
-
options: [
|
|
1566
|
-
{ value: 'add', label: hasKey ? 'Update API key' : 'Add API key' },
|
|
1567
|
-
{ value: 'test', label: 'Test connection', hint: hasKey ? '' : 'requires a key' },
|
|
1568
|
-
{ value: 'remove', label: 'Remove API key', hint: hasKey ? '' : 'no key configured' },
|
|
1569
|
-
{ value: 'cancel', label: 'Cancel' },
|
|
1570
|
-
],
|
|
1571
|
-
});
|
|
1572
|
-
rl.resume();
|
|
1573
|
-
if (p.isCancel(action) || action === 'cancel') {
|
|
1574
|
-
console.log(`\n${c.dim}/providers cancelled.${c.reset}`);
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
if (action === 'add') {
|
|
1578
|
-
console.log(`${c.dim} Get your API key at: ${def.docsUrl}${c.reset}`);
|
|
1579
|
-
rl.pause();
|
|
1580
|
-
const key = await p.password({
|
|
1581
|
-
message: `Enter your ${def.displayName} API key:`,
|
|
1582
|
-
validate: v => !v?.trim() ? 'API key is required' : undefined,
|
|
1583
|
-
});
|
|
1584
|
-
rl.resume();
|
|
1585
|
-
if (p.isCancel(key)) {
|
|
1586
|
-
console.log(`\n${c.dim}/providers cancelled.${c.reset}`);
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
ProvidersManager.add(def.name, key);
|
|
1590
|
-
console.log(`\n${c.green}✓ ${def.displayName} API key saved securely to ~/.fixocli/providers.json${c.reset}`);
|
|
1591
|
-
await refreshModelsForProvider(def.name);
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
if (action === 'remove') {
|
|
1595
|
-
if (!hasKey) {
|
|
1596
|
-
console.log(`\n${c.yellow}No key configured for ${def.displayName}.${c.reset}`);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
rl.pause();
|
|
1600
|
-
const confirmed = await p.confirm({
|
|
1601
|
-
message: `Remove API key for ${def.displayName}?`,
|
|
1602
|
-
initialValue: false,
|
|
1603
|
-
});
|
|
1604
|
-
rl.resume();
|
|
1605
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1606
|
-
const removed = ProvidersManager.remove(def.name);
|
|
1607
|
-
console.log(removed
|
|
1608
|
-
? `\n${c.green}✓ Removed API key for ${def.displayName}.${c.reset}`
|
|
1609
|
-
: `\n${c.yellow}No key found for provider: ${def.name}${c.reset}`);
|
|
1610
|
-
}
|
|
1611
|
-
return;
|
|
1069
|
+
await handler(ctx);
|
|
1070
|
+
// Sync state back
|
|
1071
|
+
currentModel = ctx.state.currentModel;
|
|
1072
|
+
currentMode = ctx.state.currentMode;
|
|
1073
|
+
currentSessionId = ctx.state.currentSessionId;
|
|
1074
|
+
currentSessionLabel = ctx.state.currentSessionLabel;
|
|
1075
|
+
sessionModifiedFiles = ctx.state.sessionModifiedFiles;
|
|
1076
|
+
pendingAttachments = ctx.state.pendingAttachments;
|
|
1077
|
+
selectedFiles = ctx.state.selectedFiles;
|
|
1078
|
+
stats = ctx.state.stats;
|
|
1079
|
+
isTaskRunning = ctx.state.isTaskRunning;
|
|
1080
|
+
currentRunningAgent = ctx.state.currentRunningAgent;
|
|
1081
|
+
if (ctx.workspaceFiles) {
|
|
1082
|
+
workspaceFiles = ctx.workspaceFiles;
|
|
1612
1083
|
}
|
|
1613
|
-
if (action === 'test') {
|
|
1614
|
-
if (!hasKey) {
|
|
1615
|
-
console.log(`\n${c.yellow}No key configured for ${def.displayName}. Add one first.${c.reset}`);
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
console.log(`\n${c.dim}Testing connection to ${def.displayName} via live /models fetch…${c.reset}`);
|
|
1619
|
-
await refreshModelsForProvider(def.name);
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
if (sub === 'list') {
|
|
1625
|
-
const list = ProvidersManager.list();
|
|
1626
|
-
if (list.length === 0) {
|
|
1627
|
-
console.log(`\n${c.yellow}No providers configured.${c.reset}`);
|
|
1628
|
-
console.log(`${c.dim} Use /providers add <name> to connect a provider (e.g. /providers add groq)${c.reset}`);
|
|
1629
|
-
console.log(`${c.dim} Available: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1630
|
-
}
|
|
1631
|
-
else {
|
|
1632
|
-
console.log(`\n${c.bold}${c.cyan}Connected Providers${c.reset}`);
|
|
1633
|
-
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1634
|
-
for (const entry of list) {
|
|
1635
|
-
const addedDate = new Date(entry.addedAt).toLocaleDateString();
|
|
1636
|
-
console.log(` ${c.cyan}${entry.name.padEnd(14)}${c.reset}${c.bold}${entry.displayName.padEnd(22)}${c.reset}${c.dim}${entry.maskedKey} (added ${addedDate})${c.reset}`);
|
|
1637
|
-
}
|
|
1638
|
-
console.log(`\n${c.dim} Use /providers remove <name> to remove a key.${c.reset}`);
|
|
1639
|
-
console.log(`${c.dim} Use /providers test <name> to verify a connection.${c.reset}`);
|
|
1640
|
-
}
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
if (sub === 'add') {
|
|
1644
|
-
const name = args[1]?.toLowerCase();
|
|
1645
|
-
if (!name) {
|
|
1646
|
-
console.log(`\n${c.yellow}Usage: /providers add <provider-name>${c.reset}`);
|
|
1647
|
-
console.log(`${c.dim} Available: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
1650
|
-
const def = ProvidersManager.getDefinition(name);
|
|
1651
|
-
if (!def) {
|
|
1652
|
-
console.log(`\n${c.red}✗ Unknown provider: ${name}${c.reset}`);
|
|
1653
|
-
console.log(`${c.dim} Available: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
|
-
console.log(`\n${c.cyan}${c.bold}Connecting to ${def.displayName}${c.reset}`);
|
|
1657
|
-
console.log(`${c.dim} Get your API key at: ${def.docsUrl}${c.reset}`);
|
|
1658
|
-
rl.pause();
|
|
1659
|
-
const apiKeyInput = await p.text({
|
|
1660
|
-
message: `Enter your ${def.displayName} API key:`,
|
|
1661
|
-
placeholder: 'sk-... or gsk_...',
|
|
1662
|
-
validate: v => !v.trim() ? 'API key is required' : undefined,
|
|
1663
|
-
});
|
|
1664
|
-
rl.resume();
|
|
1665
|
-
if (p.isCancel(apiKeyInput)) {
|
|
1666
|
-
console.log(`\n${c.dim}Provider add cancelled.${c.reset}`);
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
ProvidersManager.add(name, apiKeyInput);
|
|
1670
|
-
console.log(`\n${c.green}✓ ${def.displayName} API key saved securely to ~/.fixocli/providers.json${c.reset}`);
|
|
1671
|
-
console.log(`${c.dim} FixO will now route ${def.displayName} requests directly (bypassing the SaaS proxy).${c.reset}`);
|
|
1672
|
-
await refreshModelsForProvider(name);
|
|
1673
1084
|
return;
|
|
1674
1085
|
}
|
|
1675
|
-
if (sub === 'remove') {
|
|
1676
|
-
const name = args[1]?.toLowerCase();
|
|
1677
|
-
if (!name) {
|
|
1678
|
-
console.log(`\n${c.yellow}Usage: /providers remove <name>${c.reset}`);
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
rl.pause();
|
|
1682
|
-
const confirmed = await p.confirm({ message: `Remove API key for ${name}?`, initialValue: false });
|
|
1683
|
-
rl.resume();
|
|
1684
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1685
|
-
const removed = ProvidersManager.remove(name);
|
|
1686
|
-
console.log(removed
|
|
1687
|
-
? `\n${c.green}✓ Removed API key for ${name}.${c.reset}`
|
|
1688
|
-
: `\n${c.yellow}No key found for provider: ${name}${c.reset}`);
|
|
1689
|
-
}
|
|
1690
|
-
return;
|
|
1691
|
-
}
|
|
1692
|
-
if (sub === 'test') {
|
|
1693
|
-
const name = args[1]?.toLowerCase();
|
|
1694
|
-
if (!name) {
|
|
1695
|
-
console.log(`\n${c.yellow}Usage: /providers test <name>${c.reset}`);
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
const directConf = ProvidersManager.getDirectConfig(name);
|
|
1699
|
-
if (!directConf) {
|
|
1700
|
-
console.log(`\n${c.yellow}No key configured for ${name}. Use /providers add ${name} first.${c.reset}`);
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
console.log(`\n${c.dim}Testing connection to ${directConf.displayName} (${directConf.baseUrl})...${c.reset}`);
|
|
1704
|
-
try {
|
|
1705
|
-
const testHeaders = {
|
|
1706
|
-
'Authorization': `Bearer ${directConf.apiKey}`,
|
|
1707
|
-
};
|
|
1708
|
-
if (name === 'zen' || name === 'openrouter') {
|
|
1709
|
-
testHeaders['HTTP-Referer'] = 'https://opencode.ai/';
|
|
1710
|
-
testHeaders['X-Title'] = 'opencode';
|
|
1711
|
-
}
|
|
1712
|
-
else if (name === 'nvidia') {
|
|
1713
|
-
testHeaders['HTTP-Referer'] = 'https://opencode.ai/';
|
|
1714
|
-
testHeaders['X-Title'] = 'opencode';
|
|
1715
|
-
testHeaders['X-BILLING-INVOKE-ORIGIN'] = 'OpenCode';
|
|
1716
|
-
}
|
|
1717
|
-
else if (name === 'cerebras') {
|
|
1718
|
-
testHeaders['X-Cerebras-3rd-Party-Integration'] = 'opencode';
|
|
1719
|
-
}
|
|
1720
|
-
const resp = await fetch(`${directConf.baseUrl}/models`, {
|
|
1721
|
-
headers: testHeaders,
|
|
1722
|
-
signal: AbortSignal.timeout(8000),
|
|
1723
|
-
});
|
|
1724
|
-
if (resp.ok) {
|
|
1725
|
-
console.log(`${c.green}✓ Connection to ${directConf.displayName} successful! (HTTP ${resp.status})${c.reset}`);
|
|
1726
|
-
// Warm the cache so /model picker shows live IDs.
|
|
1727
|
-
await refreshModelsForProvider(name);
|
|
1728
|
-
}
|
|
1729
|
-
else {
|
|
1730
|
-
const text = await resp.text().catch(() => '');
|
|
1731
|
-
console.log(`${c.red}✗ ${directConf.displayName} returned HTTP ${resp.status}${text ? ': ' + text.slice(0, 100) : ''}${c.reset}`);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
catch (err) {
|
|
1735
|
-
console.log(`${c.red}✗ Connection failed: ${err.message}${c.reset}`);
|
|
1736
|
-
}
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
console.log(`\n${c.yellow}Usage: /providers [list | add <name> | remove <name> | test <name>]${c.reset}`);
|
|
1740
|
-
console.log(`${c.dim} Available providers: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
case '/compact': {
|
|
1744
|
-
const msgCount = conversation.getMessageCount();
|
|
1745
|
-
if (msgCount === 0) {
|
|
1746
|
-
console.log(`\n${c.dim}Nothing to compact — conversation is empty.${c.reset}`);
|
|
1747
|
-
return;
|
|
1748
|
-
}
|
|
1749
|
-
const tokensBefore = conversation.getTotalTokens();
|
|
1750
|
-
const contextLimit = conversation.getContextLimit();
|
|
1751
|
-
console.log(`\n${c.cyan}[Compact] Summarising ${msgCount} messages to free context tokens...${c.reset}`);
|
|
1752
|
-
console.log(`${c.dim} Current context: ${(tokensBefore / 1000).toFixed(0)}k / ${(contextLimit / 1000).toFixed(0)}k tokens${c.reset}`);
|
|
1753
|
-
try {
|
|
1754
|
-
const compacted = await conversation.compact(agent.getClient(), currentModel);
|
|
1755
|
-
if (compacted) {
|
|
1756
|
-
const info = conversation.getLastCompactionInfo();
|
|
1757
|
-
const tokensAfter = conversation.getTotalTokens();
|
|
1758
|
-
console.log(`${c.green}✓ Compacted: ${info?.messagesBefore ?? msgCount} messages → summary + ${conversation.getMessageCount()} recent messages.${c.reset}`);
|
|
1759
|
-
console.log(`${c.dim} Context: ${(tokensBefore / 1000).toFixed(0)}k → ${(tokensAfter / 1000).toFixed(0)}k tokens (~${((info?.tokensFreed ?? 0) / 1000).toFixed(0)}k freed).${c.reset}`);
|
|
1760
|
-
}
|
|
1761
|
-
else {
|
|
1762
|
-
console.log(`${c.dim}Not enough messages to compact (need more than 4 messages).${c.reset}`);
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
catch (err) {
|
|
1766
|
-
console.log(`${c.red}✗ Compact failed: ${err.message}${c.reset}`);
|
|
1767
|
-
}
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
case '/snapshot': {
|
|
1771
|
-
const label = args.join(' ').trim() || `snapshot-${Date.now()}`;
|
|
1772
|
-
if (!git.isGitRepo()) {
|
|
1773
|
-
console.log(`\n${c.yellow}⚠ Not a git repository — cannot create snapshot.${c.reset}`);
|
|
1774
|
-
return;
|
|
1775
|
-
}
|
|
1776
|
-
const hash = git.createSnapshot(label);
|
|
1777
|
-
if (hash) {
|
|
1778
|
-
console.log(`\n${c.green}✓ Workspace snapshot created: ${c.bold}${hash}${c.reset}${c.dim} (label: ${label})${c.reset}`);
|
|
1779
|
-
console.log(`${c.dim} Use /undo or git revert to roll back to this point.${c.reset}`);
|
|
1780
|
-
}
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
case '/skills': {
|
|
1784
|
-
const { skillsManager } = await import('../agent/skills.js');
|
|
1785
|
-
const list = skillsManager.getSkills();
|
|
1786
|
-
if (list.length === 0) {
|
|
1787
|
-
console.log(`\n${c.dim}No skills registered. Register skill profiles by adding SKILL.md under ~/.fixocli/skills/<name>/ or .fixocli/skills/<name>/${c.reset}`);
|
|
1788
|
-
}
|
|
1789
|
-
else {
|
|
1790
|
-
console.log(`\n${c.cyan}${c.bold}Registered Skills:${c.reset}`);
|
|
1791
|
-
for (const skill of list) {
|
|
1792
|
-
console.log(` - ${c.bold}${skill.name}${c.reset}${skill.description ? `: ${skill.description}` : ''} ${c.dim}(${skill.location})${c.reset}`);
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
return;
|
|
1796
|
-
}
|
|
1797
|
-
case '/theme':
|
|
1798
|
-
case '/variant': {
|
|
1799
|
-
const { themeMode, setThemeMode } = await import('./colors.js');
|
|
1800
|
-
const newMode = themeMode === 'dark' ? 'inverted' : 'dark';
|
|
1801
|
-
setThemeMode(newMode);
|
|
1802
|
-
console.log(`\n${c.cyan}✓ Theme set to: ${newMode === 'dark' ? 'Dark Void Minimalist' : 'High-Contrast Inverted'}${c.reset}`);
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
case '/telemetry': {
|
|
1806
|
-
const sub = args[0]?.toLowerCase();
|
|
1807
|
-
if (sub === 'on' || sub === 'enable') {
|
|
1808
|
-
config.preferences.telemetry = true;
|
|
1809
|
-
saveConfig(config);
|
|
1810
|
-
console.log(`\n${c.green}✓ Telemetry enabled${c.reset}`);
|
|
1811
|
-
}
|
|
1812
|
-
else if (sub === 'off' || sub === 'disable') {
|
|
1813
|
-
config.preferences.telemetry = false;
|
|
1814
|
-
saveConfig(config);
|
|
1815
|
-
console.log(`\n${c.green}✓ Telemetry disabled${c.reset}`);
|
|
1816
|
-
}
|
|
1817
|
-
else {
|
|
1818
|
-
console.log(`\n${c.dim}Telemetry is currently ${config.preferences.telemetry ? `${c.green}ON${c.reset}${c.dim}` : `${c.red}OFF${c.reset}${c.dim}`}. Usage: /telemetry on|off${c.reset}`);
|
|
1819
|
-
}
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
default:
|
|
1823
1086
|
console.log(`\n${c.yellow}Unknown command: ${cmd}. Type /help for available commands.${c.reset}`);
|
|
1824
1087
|
return;
|
|
1088
|
+
}
|
|
1825
1089
|
}
|
|
1826
1090
|
}
|
|
1827
1091
|
// ─── Shell commands (! prefix) ───
|
|
@@ -1862,20 +1126,48 @@ export async function startREPL(options) {
|
|
|
1862
1126
|
console.log(output);
|
|
1863
1127
|
}
|
|
1864
1128
|
catch (error) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1129
|
+
const err = error;
|
|
1130
|
+
if (err.stdout)
|
|
1131
|
+
console.log(err.stdout);
|
|
1132
|
+
if (err.stderr)
|
|
1133
|
+
console.error(`${c.red}${err.stderr}${c.reset}`);
|
|
1869
1134
|
}
|
|
1870
1135
|
return;
|
|
1871
1136
|
}
|
|
1137
|
+
// ─── Conversation echo (paste expansion) ──────────────────────────
|
|
1138
|
+
// When the submitted input contains paste attachments, overwrite the
|
|
1139
|
+
// readline-echoed `> [Paste #N +M lines]` line with a proper
|
|
1140
|
+
// conversation block so the user can see what they sent.
|
|
1141
|
+
// Mirrors the Claude Code / Antigravity transcript pattern.
|
|
1142
|
+
if (pendingPastes.length > 0) {
|
|
1143
|
+
// Step 1: reconstruct the original input as it would have looked without folding
|
|
1144
|
+
let unfoldedInput = rawInput;
|
|
1145
|
+
for (const paste of pendingPastes) {
|
|
1146
|
+
const token = `[Paste #${paste.id} +${paste.lines} lines]`;
|
|
1147
|
+
unfoldedInput = unfoldedInput.replace(token, paste.content);
|
|
1148
|
+
}
|
|
1149
|
+
// Step 2: move up ONE line and erase it — this erases the readline-
|
|
1150
|
+
// echoed token line ("> [Paste #2 +4 lines]") that is already on screen.
|
|
1151
|
+
process.stdout.write('\x1b[1A\x1b[2K');
|
|
1152
|
+
// Step 3: print the prompt and the unfolded input
|
|
1153
|
+
const lines = unfoldedInput.split(/\r\n|\r|\n/);
|
|
1154
|
+
if (lines.length > 0) {
|
|
1155
|
+
console.log(`> ${lines[0]}`);
|
|
1156
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1157
|
+
console.log(lines[i]);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
// Blank line before the agent spinner starts
|
|
1161
|
+
console.log('');
|
|
1162
|
+
}
|
|
1163
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1872
1164
|
// ─── Agent task ───
|
|
1873
1165
|
// Format any paths in the input for display
|
|
1874
1166
|
const displayInput = formatInputPaths(input, cwd);
|
|
1875
1167
|
if (displayInput !== input) {
|
|
1876
1168
|
// Re-display with highlighted paths
|
|
1877
1169
|
process.stdout.write(`\x1b[1A\x1b[2K`); // Move up and clear line
|
|
1878
|
-
console.log(
|
|
1170
|
+
console.log(`> ${displayInput}`);
|
|
1879
1171
|
}
|
|
1880
1172
|
// Extract any file paths from input for automatic pinning
|
|
1881
1173
|
const pathsInInput = extractFilePaths(input, cwd);
|
|
@@ -1895,105 +1187,34 @@ export async function startREPL(options) {
|
|
|
1895
1187
|
// Drain the queue — attachments are one-shot. The agent has its
|
|
1896
1188
|
// own copy via context above.
|
|
1897
1189
|
pendingAttachments = [];
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
for (const sub of dag.subtasks) {
|
|
1917
|
-
const deps = sub.dependencies.length > 0 ? ` (deps: ${sub.dependencies.join(', ')})` : '';
|
|
1918
|
-
const lineStr = ` - [${sub.persona.toUpperCase()}] ${sub.title}${deps}`;
|
|
1919
|
-
const pad = Math.max(0, width - lineStr.length - 4);
|
|
1920
|
-
console.log(`${c.cyan}│${c.reset} ${c.bold}${lineStr}${c.reset}${' '.repeat(pad)} ${c.cyan}│${c.reset}`);
|
|
1921
|
-
}
|
|
1922
|
-
console.log(`${c.cyan}${borderBottom}${c.reset}\n`);
|
|
1923
|
-
// Save the DAG to .fixo/last-dag.json
|
|
1924
|
-
const fixoDir = path.join(cwd, '.fixo');
|
|
1925
|
-
fs.mkdirSync(fixoDir, { recursive: true });
|
|
1926
|
-
fs.writeFileSync(path.join(fixoDir, 'last-dag.json'), JSON.stringify({ task: input, dag }, null, 2), 'utf-8');
|
|
1927
|
-
if (currentMode === 'PLAN') {
|
|
1928
|
-
console.log(`${c.green}✓ Plan generated and saved successfully.${c.reset}`);
|
|
1929
|
-
console.log(`${c.dim} To execute this plan, switch to BUILD mode (type /mode build or hit [TAB]) and run: /run-plan${c.reset}\n`);
|
|
1930
|
-
return;
|
|
1931
|
-
}
|
|
1932
|
-
const budgetLimit = projectConfig?.maxAttempts ?? 12;
|
|
1933
|
-
const pool = new AgentPool(3, budgetLimit);
|
|
1934
|
-
console.log(`\n${c.cyan}[Agent Pool] Executing DAG of subtasks (concurrency limit: 3, budget: ${budgetLimit} tool calls)...${c.reset}`);
|
|
1935
|
-
const success = await pool.execute(context, dag);
|
|
1936
|
-
const durationMs = Date.now() - startTime;
|
|
1937
|
-
const totalPromptTokens = orchestrator.tokensUsed.prompt_tokens + pool.tokensUsed.prompt_tokens;
|
|
1938
|
-
const totalCompletionTokens = orchestrator.tokensUsed.completion_tokens + pool.tokensUsed.completion_tokens;
|
|
1939
|
-
// Find modified files to report
|
|
1940
|
-
const { getModifiedFiles, getBranchPoint } = await import('../agent/worker-agent.js');
|
|
1941
|
-
const relativeModified = getModifiedFiles(cwd, getBranchPoint(cwd));
|
|
1942
|
-
const modifiedFiles = relativeModified.map(f => path.resolve(cwd, f));
|
|
1943
|
-
if (!success) {
|
|
1944
|
-
console.log(`\n${c.red}✗ Parallel workers failed to complete all subtasks.${c.reset}`);
|
|
1945
|
-
if (git.isGitRepo()) {
|
|
1946
|
-
console.log(`\n${c.yellow}[Agent Pool] Rolling back all uncommitted changes due to run failure...${c.reset}`);
|
|
1947
|
-
git.discardUncommittedChanges();
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
result = {
|
|
1951
|
-
success,
|
|
1952
|
-
response: success
|
|
1953
|
-
? 'Successfully completed complex task via parallel agents.'
|
|
1954
|
-
: 'Failed to complete all complex subtasks.',
|
|
1955
|
-
modifiedFiles,
|
|
1956
|
-
tokensUsed: {
|
|
1957
|
-
prompt_tokens: totalPromptTokens,
|
|
1958
|
-
completion_tokens: totalCompletionTokens,
|
|
1959
|
-
total_tokens: totalPromptTokens + totalCompletionTokens
|
|
1960
|
-
},
|
|
1961
|
-
toolCallCount: pool.toolCallCount,
|
|
1962
|
-
durationMs,
|
|
1963
|
-
model: context.model,
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
catch (err) {
|
|
1967
|
-
console.error(`\n${c.red}✗ Orchestrated execution failed: ${err.message || err}${c.reset}`);
|
|
1968
|
-
if (git.isGitRepo()) {
|
|
1969
|
-
console.log(`\n${c.yellow}[Agent Pool] Rolling back all uncommitted changes due to error...${c.reset}`);
|
|
1970
|
-
git.discardUncommittedChanges();
|
|
1971
|
-
}
|
|
1972
|
-
const durationMs = Date.now() - startTime;
|
|
1973
|
-
result = {
|
|
1974
|
-
success: false,
|
|
1975
|
-
response: `Orchestrated run failed: ${err.message || err}`,
|
|
1976
|
-
modifiedFiles: [],
|
|
1977
|
-
tokensUsed: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
1978
|
-
toolCallCount: 0,
|
|
1979
|
-
durationMs,
|
|
1980
|
-
model: context.model,
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
else {
|
|
1985
|
-
console.log(`\n${c.cyan}[Routing Engine] Simple task detected (${classification.reason}). Routing to SingleAgent...${c.reset}`);
|
|
1986
|
-
isTaskRunning = true;
|
|
1987
|
-
currentRunningAgent = agent;
|
|
1988
|
-
try {
|
|
1989
|
-
result = await agent.runStreaming(context, conversation, rl);
|
|
1990
|
-
}
|
|
1991
|
-
finally {
|
|
1190
|
+
// Phase 2.1 — routing decision + execution lives in
|
|
1191
|
+
// task-router.ts so it can be unit-tested independently of the
|
|
1192
|
+
// REPL and reused by future non-TUI entry points (--headless,
|
|
1193
|
+
// web backend, IDE extension). Console output is byte-identical
|
|
1194
|
+
// to the pre-extraction inline path. The rollback inside the
|
|
1195
|
+
// complex path uses git.discardChangesIn() (Phase 0.0 — scoped).
|
|
1196
|
+
const { routeAndExecute } = await import('../agent/task-router.js');
|
|
1197
|
+
const routed = await routeAndExecute(input, context, {
|
|
1198
|
+
agent,
|
|
1199
|
+
conversation,
|
|
1200
|
+
rl,
|
|
1201
|
+
projectConfig,
|
|
1202
|
+
verbose,
|
|
1203
|
+
onSimplePathStart: (a) => {
|
|
1204
|
+
isTaskRunning = true;
|
|
1205
|
+
currentRunningAgent = a;
|
|
1206
|
+
},
|
|
1207
|
+
onSimplePathEnd: () => {
|
|
1992
1208
|
isTaskRunning = false;
|
|
1993
1209
|
currentRunningAgent = null;
|
|
1994
|
-
|
|
1995
|
-
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
if (routed.route === 'plan-mode-deferred') {
|
|
1213
|
+
pendingPastes = [];
|
|
1214
|
+
return;
|
|
1996
1215
|
}
|
|
1216
|
+
const result = routed.result;
|
|
1217
|
+
pendingPastes = [];
|
|
1997
1218
|
// Print result summary
|
|
1998
1219
|
console.log('');
|
|
1999
1220
|
const modelPart = result.model ? `${result.model} · ` : '';
|