fixo-cli 1.0.3 → 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 +37 -0
- package/dist/agent/command-parser.d.ts.map +1 -1
- package/dist/agent/command-parser.js +473 -1
- 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 +32 -2
- package/dist/agent/conversation.d.ts.map +1 -1
- package/dist/agent/conversation.js +84 -9
- package/dist/agent/conversation.js.map +1 -1
- package/dist/agent/duration.d.ts +24 -0
- package/dist/agent/duration.d.ts.map +1 -0
- package/dist/agent/duration.js +42 -0
- package/dist/agent/duration.js.map +1 -0
- package/dist/agent/file-writing-rules.d.ts +19 -0
- package/dist/agent/file-writing-rules.d.ts.map +1 -0
- package/dist/agent/file-writing-rules.js +31 -0
- package/dist/agent/file-writing-rules.js.map +1 -0
- 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 +311 -7
- 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/provider-cooldown.d.ts.map +1 -1
- package/dist/agent/provider-cooldown.js +3 -2
- package/dist/agent/provider-cooldown.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 +13 -0
- package/dist/agent/single-agent.d.ts.map +1 -1
- package/dist/agent/single-agent.js +225 -37
- 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 +29 -11
- 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 +56 -16
- package/dist/agent/worker-agent.js.map +1 -1
- package/dist/config.d.ts +253 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +81 -1
- 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 +89 -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 +119 -0
- package/dist/runtime/loop-mitigation.d.ts.map +1 -0
- package/dist/runtime/loop-mitigation.js +192 -0
- package/dist/runtime/loop-mitigation.js.map +1 -0
- 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/session-snapshots.d.ts +52 -2
- package/dist/runtime/session-snapshots.d.ts.map +1 -1
- package/dist/runtime/session-snapshots.js +76 -1
- package/dist/runtime/session-snapshots.js.map +1 -1
- 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 +461 -1143
- 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/dist/ui/session-header.d.ts +13 -0
- package/dist/ui/session-header.d.ts.map +1 -1
- package/dist/ui/session-header.js +6 -0
- package/dist/ui/session-header.js.map +1 -1
- package/package.json +22 -4
- package/scripts/check-vendor-wasm.js +55 -0
- package/vendor/tree-sitter-bash.wasm +0 -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/vendor/tree-sitter.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 ────
|
|
@@ -45,6 +37,7 @@ export async function startREPL(options) {
|
|
|
45
37
|
await mcpBridgeManager.initialize(cwd);
|
|
46
38
|
const { randomUUID } = await import('node:crypto');
|
|
47
39
|
let currentSessionId = randomUUID();
|
|
40
|
+
let currentSessionLabel;
|
|
48
41
|
let sessionModifiedFiles = [];
|
|
49
42
|
let currentMode = 'BUILD';
|
|
50
43
|
let currentModel = projectConfig?.model ?? config.defaultModel ?? 'auto';
|
|
@@ -76,6 +69,8 @@ export async function startREPL(options) {
|
|
|
76
69
|
conversation.setContextLimit(currentModel);
|
|
77
70
|
currentMode = snap.mode;
|
|
78
71
|
selectedFiles = [...snap.selectedFiles];
|
|
72
|
+
currentSessionId = snap.id;
|
|
73
|
+
currentSessionLabel = snap.label;
|
|
79
74
|
console.log(`\n${c.green}✓ Resumed session${c.reset} ${c.dim}${snap.id}${c.reset}`);
|
|
80
75
|
console.log(` ${c.dim}messages=${snap.conversation.length} tokens=${snap.tokens} model=${snap.model} mode=${snap.mode}${c.reset}`);
|
|
81
76
|
if (snap.summary) {
|
|
@@ -104,13 +99,21 @@ export async function startREPL(options) {
|
|
|
104
99
|
}
|
|
105
100
|
let lastPromptRow = 0;
|
|
106
101
|
let mouseReportingEnabled = false;
|
|
107
|
-
|
|
102
|
+
let stats = {
|
|
108
103
|
totalPromptTokens: 0,
|
|
109
104
|
totalCompletionTokens: 0,
|
|
110
105
|
totalToolCalls: 0,
|
|
111
106
|
totalTasks: 0,
|
|
112
107
|
totalDurationMs: 0,
|
|
113
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
|
+
}
|
|
114
117
|
// The welcome screen (lava logo + command grid) is printed by
|
|
115
118
|
// `src/index.ts` before the REPL starts; the startREPL entry
|
|
116
119
|
// point jumps straight into the prompt loop.
|
|
@@ -126,7 +129,8 @@ export async function startREPL(options) {
|
|
|
126
129
|
}
|
|
127
130
|
catch (error) {
|
|
128
131
|
if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
|
|
129
|
-
|
|
132
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
133
|
+
console.warn(`[Debug Warning] Failed to read command history from ${historyFile}: ${msg}`);
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
// ──── Create readline interface ────
|
|
@@ -157,9 +161,7 @@ export async function startREPL(options) {
|
|
|
157
161
|
// existing /mode command semantics intact while still letting
|
|
158
162
|
// the new bar visualise the live mode.
|
|
159
163
|
const buildLavaStatusState = () => {
|
|
160
|
-
const modeForState = currentMode === 'PLAN' ? 'PLAN' :
|
|
161
|
-
currentMode === 'BUILD' ? 'BUILD' :
|
|
162
|
-
'BUILD';
|
|
164
|
+
const modeForState = currentMode === 'PLAN' ? 'PLAN' : 'BUILD';
|
|
163
165
|
let contextPercent = 0;
|
|
164
166
|
try {
|
|
165
167
|
const used = conversation.getTotalTokens();
|
|
@@ -183,10 +185,13 @@ export async function startREPL(options) {
|
|
|
183
185
|
mode: modeForState,
|
|
184
186
|
routing: 'auto',
|
|
185
187
|
model: currentModel,
|
|
186
|
-
|
|
188
|
+
// Show '(detached HEAD)' instead of bare 'detached' so the
|
|
189
|
+
// status bar is unambiguous — the previous label read as "the
|
|
190
|
+
// CLI is detached from the API server" to several users.
|
|
191
|
+
branch: currentBranch || '(detached HEAD)',
|
|
187
192
|
contextPercent,
|
|
188
193
|
providersCount,
|
|
189
|
-
transport: 'freellmapi',
|
|
194
|
+
transport: config.provider_mode === 'direct' ? 'direct' : 'freellmapi',
|
|
190
195
|
};
|
|
191
196
|
};
|
|
192
197
|
const drawLavaStatusBar = () => {
|
|
@@ -197,6 +202,10 @@ export async function startREPL(options) {
|
|
|
197
202
|
// returns.
|
|
198
203
|
renderStatusBar(buildLavaStatusState());
|
|
199
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
|
+
}
|
|
200
209
|
};
|
|
201
210
|
// Surface the result of a live model fetch as a one-line status.
|
|
202
211
|
// Invoked from /providers add and /providers test so the user
|
|
@@ -248,6 +257,16 @@ export async function startREPL(options) {
|
|
|
248
257
|
}
|
|
249
258
|
// Register synchronous exit cleanups
|
|
250
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
|
+
}
|
|
251
270
|
try {
|
|
252
271
|
const hist = rl.history;
|
|
253
272
|
if (Array.isArray(hist)) {
|
|
@@ -256,7 +275,8 @@ export async function startREPL(options) {
|
|
|
256
275
|
}
|
|
257
276
|
catch (error) {
|
|
258
277
|
if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
|
|
259
|
-
|
|
278
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
279
|
+
console.warn(`[Debug Warning] Failed to write history file on exit: ${msg}`);
|
|
260
280
|
}
|
|
261
281
|
}
|
|
262
282
|
disableMouseReportingSync();
|
|
@@ -280,39 +300,50 @@ export async function startREPL(options) {
|
|
|
280
300
|
let sigintCount = 0;
|
|
281
301
|
let lastSigintTime = 0;
|
|
282
302
|
let sigintResetTimer = null;
|
|
303
|
+
// Dedup guard: prevents double-firing when both `rl` and `process` SIGINT listeners fire.
|
|
304
|
+
let sigintHandling = false;
|
|
283
305
|
const sigintHandler = () => {
|
|
284
|
-
if (
|
|
285
|
-
// A task is running — cancel it instead of exiting
|
|
286
|
-
currentRunningAgent.abort();
|
|
306
|
+
if (sigintHandling)
|
|
287
307
|
return;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
300
336
|
if (sigintResetTimer)
|
|
301
337
|
clearTimeout(sigintResetTimer);
|
|
302
|
-
sigintResetTimer =
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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;
|
|
307
346
|
}
|
|
308
|
-
// Second press within the window — exit
|
|
309
|
-
if (sigintResetTimer)
|
|
310
|
-
clearTimeout(sigintResetTimer);
|
|
311
|
-
sigintResetTimer = null;
|
|
312
|
-
sigintCount = 0;
|
|
313
|
-
exitCleanup();
|
|
314
|
-
console.log('\n\n👋 FixO CLI session ended safely. Core engine offline.');
|
|
315
|
-
process.exit(0);
|
|
316
347
|
};
|
|
317
348
|
// Listen on both the readline interface (catches Ctrl+C during rl.question())
|
|
318
349
|
// and the process (fallback for non-readline scenarios).
|
|
@@ -349,7 +380,6 @@ export async function startREPL(options) {
|
|
|
349
380
|
enableMouseReporting();
|
|
350
381
|
const currentCursor = rl.cursor;
|
|
351
382
|
let output = '\n';
|
|
352
|
-
const width = 60;
|
|
353
383
|
const borderTop = `${c.snow}┌────────────────────────────────────────────────────────┐${c.reset}\n`;
|
|
354
384
|
const borderBottom = `${c.snow}└────────────────────────────────────────────────────────┘${c.reset}`;
|
|
355
385
|
output += borderTop;
|
|
@@ -462,7 +492,8 @@ export async function startREPL(options) {
|
|
|
462
492
|
}
|
|
463
493
|
catch (error) {
|
|
464
494
|
if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
|
|
465
|
-
|
|
495
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
496
|
+
console.warn(`[Debug Warning] Failed to load skills list: ${msg}`);
|
|
466
497
|
}
|
|
467
498
|
}
|
|
468
499
|
const matchingFiles = workspaceFiles.filter(f => f.toLowerCase().includes(q) || path.basename(f).toLowerCase().startsWith(q));
|
|
@@ -492,6 +523,7 @@ export async function startREPL(options) {
|
|
|
492
523
|
readline.emitKeypressEvents(process.stdin);
|
|
493
524
|
if (process.stdin.isTTY) {
|
|
494
525
|
process.stdin.setRawMode(true);
|
|
526
|
+
process.stdout.write('\x1b[?2004h');
|
|
495
527
|
}
|
|
496
528
|
const keypressHandler = (_char, key) => {
|
|
497
529
|
if (!isPrompting)
|
|
@@ -529,6 +561,30 @@ export async function startREPL(options) {
|
|
|
529
561
|
};
|
|
530
562
|
process.stdin.on('keypress', keypressHandler);
|
|
531
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
|
+
}
|
|
532
588
|
// Monkey-patch process.stdin.emit to intercept keypress and mouse events
|
|
533
589
|
const originalEmit = process.stdin.emit;
|
|
534
590
|
process.stdin.emit = function (event, ...args) {
|
|
@@ -537,6 +593,78 @@ export async function startREPL(options) {
|
|
|
537
593
|
if (rawData) {
|
|
538
594
|
let str = mouseBuffer + rawData.toString();
|
|
539
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
|
+
}
|
|
540
668
|
// Intercept cursor position response
|
|
541
669
|
if (str.startsWith('\x1b[') && str.endsWith('R')) {
|
|
542
670
|
const match = str.match(/\x1b\[(\d+);(\d+)R/);
|
|
@@ -628,6 +756,38 @@ export async function startREPL(options) {
|
|
|
628
756
|
}
|
|
629
757
|
if (event === 'keypress') {
|
|
630
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
|
+
}
|
|
631
791
|
// Intercept Escape or Ctrl+C to cancel a running task (when not prompting)
|
|
632
792
|
if (key && key.name === 'escape' && isTaskRunning && currentRunningAgent) {
|
|
633
793
|
currentRunningAgent.abort();
|
|
@@ -651,17 +811,42 @@ export async function startREPL(options) {
|
|
|
651
811
|
// legacy dirLabel/branchLabel/modelLabel/modeLabel row
|
|
652
812
|
// is gone — the new bar carries all of that information.
|
|
653
813
|
drawLavaStatusBar();
|
|
654
|
-
process.stdout.write(`${
|
|
814
|
+
process.stdout.write(`${c.dim}─────────────────────────────────────────────────────────────────${c.reset}\n> `);
|
|
655
815
|
return true; // swallow keypress
|
|
656
816
|
}
|
|
657
817
|
}
|
|
658
818
|
return originalEmit.apply(this, [event, ...args]);
|
|
659
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
|
+
}
|
|
660
843
|
// ──── REPL loop ────
|
|
661
844
|
const promptForInput = () => {
|
|
662
845
|
// Restore raw mode and resume streams to recover from any clack/spinner interactions
|
|
663
846
|
if (process.stdin.isTTY) {
|
|
664
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');
|
|
665
850
|
}
|
|
666
851
|
process.stdin.resume();
|
|
667
852
|
rl.resume();
|
|
@@ -671,7 +856,8 @@ export async function startREPL(options) {
|
|
|
671
856
|
// visible in the bar; the prompt itself is the lava `›` glyph.
|
|
672
857
|
drawLavaStatusBar();
|
|
673
858
|
isPrompting = true;
|
|
674
|
-
|
|
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) => {
|
|
675
861
|
isPrompting = false;
|
|
676
862
|
disableMouseReporting();
|
|
677
863
|
clearSuggestions();
|
|
@@ -696,12 +882,45 @@ export async function startREPL(options) {
|
|
|
696
882
|
else if (msg.includes('429')) {
|
|
697
883
|
console.log(`${c.dim} → Rate limited. Wait a moment or add more API keys.${c.reset}`);
|
|
698
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
|
+
}
|
|
699
903
|
}
|
|
700
904
|
promptForInput();
|
|
701
905
|
});
|
|
702
906
|
};
|
|
703
907
|
// ──── Input handler ────
|
|
704
|
-
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
|
+
}
|
|
705
924
|
// ─── Slash commands ───
|
|
706
925
|
if (input.startsWith('/')) {
|
|
707
926
|
const parts = input.split(/\s+/).filter(Boolean);
|
|
@@ -723,1026 +942,150 @@ export async function startREPL(options) {
|
|
|
723
942
|
case '/help':
|
|
724
943
|
printHelp();
|
|
725
944
|
return;
|
|
726
|
-
case '/
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
console.log(`\n${c.bold}${c.cyan}Available Models by Provider${c.reset}`);
|
|
732
|
-
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
733
|
-
for (const def of PROVIDER_REGISTRY) {
|
|
734
|
-
const hasKey = ProvidersManager.has(def.name);
|
|
735
|
-
const keyStatus = hasKey ? `${c.green}[key ✓]${c.reset}` : `${c.dim}[no key]${c.reset}`;
|
|
736
|
-
const cached = ProvidersManager.getCachedModels(def.name);
|
|
737
|
-
const modelList = cached?.models?.length ? cached.models : def.models;
|
|
738
|
-
const sourceTag = cached?.source === 'live'
|
|
739
|
-
? ''
|
|
740
|
-
: ` ${c.dim}[unverified]${c.reset}`;
|
|
741
|
-
console.log(`\n ${c.snow}${c.bold}${def.displayName}${c.reset} ${keyStatus}${sourceTag}`);
|
|
742
|
-
for (const model of modelList) {
|
|
743
|
-
console.log(` ${c.cyan}•${c.reset} ${model}`);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
console.log(`\n${c.dim} Use /providers add <name> to connect a provider with your API key.${c.reset}`);
|
|
747
|
-
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();
|
|
748
950
|
return;
|
|
749
951
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
{ value: 'all', label: 'Show all models (flat list)', hint: 'classic view' },
|
|
757
|
-
...PROVIDER_REGISTRY.map(def => ({
|
|
758
|
-
value: def.name,
|
|
759
|
-
label: def.displayName,
|
|
760
|
-
hint: ProvidersManager.has(def.name) ? ' [key ✓]' : ' [no key]'
|
|
761
|
-
})),
|
|
762
|
-
{ value: '__manual__', label: 'Enter model ID manually…', hint: '' },
|
|
763
|
-
],
|
|
764
|
-
initialValue: PROVIDER_REGISTRY.find(def => def.models.includes(currentModel))?.name || 'all',
|
|
765
|
-
});
|
|
766
|
-
rl.resume();
|
|
767
|
-
if (p.isCancel(pickedProvider)) {
|
|
768
|
-
console.log(`\n${c.dim}Model unchanged: ${c.cyan}${currentModel}${c.reset}`);
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
if (pickedProvider === '__manual__') {
|
|
772
|
-
rl.pause();
|
|
773
|
-
const manual = await p.text({
|
|
774
|
-
message: 'Enter model ID:',
|
|
775
|
-
placeholder: 'e.g. gpt-4o, claude-opus-4-5, gemini-2.5-pro',
|
|
776
|
-
validate: v => !v.trim() ? 'Model ID is required' : undefined,
|
|
777
|
-
});
|
|
778
|
-
rl.resume();
|
|
779
|
-
if (!p.isCancel(manual) && manual) {
|
|
780
|
-
currentModel = manual.trim();
|
|
781
|
-
conversation.setContextLimit(currentModel);
|
|
782
|
-
console.log(`\n${c.green}✓ Model set to: ${c.bold}${currentModel}${c.reset}`);
|
|
783
|
-
}
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
if (pickedProvider === 'all') {
|
|
787
|
-
rl.pause();
|
|
788
|
-
const allOptions = PROVIDER_REGISTRY.flatMap(def => def.models.map(m => ({
|
|
789
|
-
value: m,
|
|
790
|
-
label: `${m}`,
|
|
791
|
-
hint: def.displayName + (ProvidersManager.has(def.name) ? ' [key ✓]' : ''),
|
|
792
|
-
})));
|
|
793
|
-
const picked = await p.select({
|
|
794
|
-
message: 'Select a model from the flat list:',
|
|
795
|
-
options: [
|
|
796
|
-
{ value: currentModel, label: `Keep current: ${currentModel}`, hint: 'no change' },
|
|
797
|
-
...allOptions,
|
|
798
|
-
],
|
|
799
|
-
initialValue: currentModel,
|
|
800
|
-
});
|
|
801
|
-
rl.resume();
|
|
802
|
-
if (p.isCancel(picked)) {
|
|
803
|
-
console.log(`\n${c.dim}Model unchanged: ${c.cyan}${currentModel}${c.reset}`);
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
currentModel = picked;
|
|
807
|
-
// Store hint — find which provider this model belongs to
|
|
808
|
-
const owningDef = PROVIDER_REGISTRY.find(d => d.models.includes(currentModel)
|
|
809
|
-
|| ProvidersManager.getCachedModels(d.name)?.models?.includes(currentModel));
|
|
810
|
-
if (owningDef)
|
|
811
|
-
ProvidersManager.setModelProviderHint(currentModel, owningDef.name);
|
|
812
|
-
conversation.setContextLimit(currentModel);
|
|
813
|
-
console.log(`\n${c.green}✓ Model set to: ${c.bold}${currentModel}${c.reset}`);
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
const def = PROVIDER_REGISTRY.find(p => p.name === pickedProvider);
|
|
817
|
-
const hasKey = ProvidersManager.has(def.name);
|
|
818
|
-
const keyStatus = hasKey ? `${c.green}[key ✓]${c.reset}` : `${c.red}[no key]${c.reset}`;
|
|
819
|
-
// Prefer the cached live model list; fall back to the
|
|
820
|
-
// registry list (tagged `[unverified]`) when no fresh
|
|
821
|
-
// cache exists. Drops the synthetic "(free)" suffix
|
|
822
|
-
// since we no longer know that without provider
|
|
823
|
-
// metadata.
|
|
824
|
-
const cached = ProvidersManager.getCachedModels(def.name);
|
|
825
|
-
const modelList = cached?.models?.length ? cached.models : def.models;
|
|
826
|
-
const sourceSuffix = cached?.source === 'live'
|
|
827
|
-
? ''
|
|
828
|
-
: ` ${c.dim}[unverified]${c.reset}`;
|
|
829
|
-
rl.pause();
|
|
830
|
-
const picked = await p.select({
|
|
831
|
-
message: `Select a model from ${c.bold}${def.displayName}${c.reset} ${keyStatus}${sourceSuffix}:`,
|
|
832
|
-
options: modelList.map(m => {
|
|
833
|
-
return {
|
|
834
|
-
value: m,
|
|
835
|
-
label: m,
|
|
836
|
-
hint: m === currentModel ? 'currently selected' : ''
|
|
837
|
-
};
|
|
838
|
-
}),
|
|
839
|
-
initialValue: modelList.includes(currentModel) ? currentModel : undefined,
|
|
840
|
-
});
|
|
841
|
-
rl.resume();
|
|
842
|
-
if (p.isCancel(picked)) {
|
|
843
|
-
console.log(`\n${c.dim}Model unchanged: ${c.cyan}${currentModel}${c.reset}`);
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
currentModel = picked;
|
|
847
|
-
// Store explicit model-provider association so
|
|
848
|
-
// resolveDirectConfig can route this model directly
|
|
849
|
-
// to this provider (critical for live-fetched models
|
|
850
|
-
// that don't appear in the static registry).
|
|
851
|
-
ProvidersManager.setModelProviderHint(currentModel, def.name);
|
|
852
|
-
conversation.setContextLimit(currentModel);
|
|
853
|
-
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();
|
|
854
958
|
return;
|
|
855
959
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
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();
|
|
859
967
|
return;
|
|
860
968
|
}
|
|
861
|
-
case '/
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
else {
|
|
867
|
-
console.log(`\n${c.dim}Selected files:${c.reset}`);
|
|
868
|
-
for (const f of selectedFiles) {
|
|
869
|
-
console.log(` ${c.cyan}${path.basename(f)}${c.reset} ${c.dim}(${f})${c.reset}`);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
let rawPath = args.join(' ');
|
|
875
|
-
if ((rawPath.startsWith("'") && rawPath.endsWith("'")) ||
|
|
876
|
-
(rawPath.startsWith('"') && rawPath.endsWith('"'))) {
|
|
877
|
-
rawPath = rawPath.slice(1, -1);
|
|
878
|
-
}
|
|
879
|
-
let filePath;
|
|
880
|
-
try {
|
|
881
|
-
filePath = guard.ensureFile(rawPath);
|
|
882
|
-
}
|
|
883
|
-
catch (error) {
|
|
884
|
-
console.log(`\n${c.red}✗ ${error instanceof Error ? error.message : String(error)}${c.reset}`);
|
|
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();
|
|
885
974
|
return;
|
|
886
975
|
}
|
|
887
|
-
|
|
888
|
-
|
|
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();
|
|
889
980
|
return;
|
|
890
981
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (args[0]) {
|
|
906
|
-
console.log(`\n${undoRun(cwd, args[0])}`);
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
rl.pause();
|
|
910
|
-
const confirmed = await p.confirm({
|
|
911
|
-
message: 'Are you sure you want to completely discard the last automated agent commit and restore all files?',
|
|
912
|
-
initialValue: false,
|
|
913
|
-
});
|
|
914
|
-
rl.resume();
|
|
915
|
-
if (p.isCancel(confirmed) || !confirmed) {
|
|
916
|
-
console.log(`\n${c.yellow} ⚠ Undo cancelled.${c.reset}`);
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
git.undoLastCommit();
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
case '/clear':
|
|
923
|
-
conversation.clear();
|
|
924
|
-
pendingAttachments = [];
|
|
925
|
-
console.log(`\n${c.green}✓ Conversation cleared${c.reset}`);
|
|
926
|
-
return;
|
|
927
|
-
case '/image': {
|
|
928
|
-
// `/image <path>` — queue a local image for the next turn.
|
|
929
|
-
// `/image clear` — drop the queue.
|
|
930
|
-
// `/image list` — show what's queued.
|
|
931
|
-
const sub = args[0];
|
|
932
|
-
if (sub === 'clear') {
|
|
933
|
-
const n = pendingAttachments.length;
|
|
934
|
-
pendingAttachments = [];
|
|
935
|
-
console.log(`\n${c.green}✓ Cleared ${n} pending image(s)${c.reset}`);
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
if (sub === 'list') {
|
|
939
|
-
if (pendingAttachments.length === 0) {
|
|
940
|
-
console.log(`\n${c.dim}No pending images.${c.reset}`);
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
console.log(`\n${c.bold}Pending images (sent on next prompt):${c.reset}`);
|
|
944
|
-
for (const [i, block] of pendingAttachments.entries()) {
|
|
945
|
-
if (block.type === 'image' && block.source.kind === 'base64') {
|
|
946
|
-
const approxBytes = Math.floor((block.source.data.length * 3) / 4);
|
|
947
|
-
console.log(` ${i + 1}. ${block.source.mediaType} (~${approxBytes} bytes)`);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
if (!sub) {
|
|
953
|
-
console.log(`\n${c.yellow}Usage: /image <path> | /image list | /image clear${c.reset}`);
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
const result = loadImageAsBlock(sub, cwd);
|
|
957
|
-
if (!result.ok) {
|
|
958
|
-
console.log(`\n${c.red}✗ /image: ${result.error}${c.reset}`);
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
pendingAttachments.push(result.block);
|
|
962
|
-
console.log(`\n${c.green}✓ Attached${c.reset} ${c.dim}${result.mediaType}, ${result.bytes} bytes — will be sent with your next prompt${c.reset}`);
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
case '/mcp': {
|
|
966
|
-
const sub = args[0]?.toLowerCase();
|
|
967
|
-
if (!sub || sub === 'list') {
|
|
968
|
-
const { listAllMcpSources, mergedMcpServers } = await import('../agent/mcp-registry.js');
|
|
969
|
-
const view = listAllMcpSources(cwd);
|
|
970
|
-
console.log(`\n${c.bold}${c.cyan}MCP Servers${c.reset} ${c.dim}(project-wins precedence: local > project > global)${c.reset}`);
|
|
971
|
-
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
972
|
-
const renderSource = (label, s) => {
|
|
973
|
-
const names = Object.keys(s.servers);
|
|
974
|
-
if (names.length === 0) {
|
|
975
|
-
console.log(` ${c.dim}${label}: (empty)${s.configPath ? ` ${c.dim}${s.configPath}${c.reset}` : ''}`);
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
console.log(` ${c.bold}${label}${c.reset}${s.configPath ? ` ${c.dim}${s.configPath}${c.reset}` : ''}`);
|
|
979
|
-
for (const n of names) {
|
|
980
|
-
console.log(` ${c.cyan}•${c.reset} ${n}`);
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
renderSource('global', view.global);
|
|
984
|
-
renderSource('project', view.project);
|
|
985
|
-
renderSource('local', view.local);
|
|
986
|
-
const merged = mergedMcpServers(cwd);
|
|
987
|
-
const mergedCount = Object.keys(merged).length;
|
|
988
|
-
console.log(`\n${c.dim}merged total: ${mergedCount} server(s)${c.reset}`);
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
if (sub === 'add') {
|
|
992
|
-
const name = args[1];
|
|
993
|
-
if (!name || args.length < 3) {
|
|
994
|
-
console.log(`\n${c.yellow}Usage: /mcp add <name> <command> [args...]${c.reset}`);
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
const cmd = args[2];
|
|
998
|
-
const cmdArgs = args.slice(3);
|
|
999
|
-
const { addLocalMcpServer } = await import('../agent/mcp-registry.js');
|
|
1000
|
-
addLocalMcpServer(cwd, name, { command: cmd, args: cmdArgs, type: 'stdio' });
|
|
1001
|
-
console.log(`\n${c.green}✓ Added local MCP server:${c.reset} ${name} ${c.dim}(command=${cmd} args=${JSON.stringify(cmdArgs)})${c.reset}`);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (sub === 'remove' || sub === 'rm') {
|
|
1005
|
-
const name = args[1];
|
|
1006
|
-
if (!name) {
|
|
1007
|
-
console.log(`\n${c.yellow}Usage: /mcp remove <name>${c.reset}`);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
const { removeLocalMcpServer } = await import('../agent/mcp-registry.js');
|
|
1011
|
-
const removed = removeLocalMcpServer(cwd, name);
|
|
1012
|
-
if (removed) {
|
|
1013
|
-
console.log(`\n${c.green}✓ Removed local MCP server:${c.reset} ${name}`);
|
|
1014
|
-
}
|
|
1015
|
-
else {
|
|
1016
|
-
console.log(`\n${c.yellow}No local MCP server named ${name}${c.reset}`);
|
|
1017
|
-
}
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (sub === 'test') {
|
|
1021
|
-
const name = args[1];
|
|
1022
|
-
if (!name) {
|
|
1023
|
-
console.log(`\n${c.yellow}Usage: /mcp test <name>${c.reset}`);
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
const { mergedMcpServers } = await import('../agent/mcp-registry.js');
|
|
1027
|
-
const all = mergedMcpServers(cwd);
|
|
1028
|
-
const cfg = all[name];
|
|
1029
|
-
if (!cfg) {
|
|
1030
|
-
console.log(`\n${c.yellow}No MCP server named ${name} (in any source)${c.reset}`);
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const hasCommand = typeof cfg.command === 'string';
|
|
1034
|
-
const hasUrl = typeof cfg.url === 'string';
|
|
1035
|
-
if (hasCommand || hasUrl) {
|
|
1036
|
-
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}`);
|
|
1037
996
|
}
|
|
1038
997
|
else {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
}
|
|
1043
|
-
console.log(`\n${c.yellow}Unknown /mcp subcommand: ${sub}. Use: list | add | remove | test${c.reset}`);
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
case '/todo': {
|
|
1047
|
-
const sub = args[0]?.toLowerCase();
|
|
1048
|
-
if (!sub || sub === 'list' || sub === 'ls') {
|
|
1049
|
-
const list = loadTodoList(cwd);
|
|
1050
|
-
const summary = summariseTodoList(list);
|
|
1051
|
-
console.log('');
|
|
1052
|
-
console.log(renderTodoList(list));
|
|
1053
|
-
if (summary.length > 0) {
|
|
1054
|
-
console.log(`\n${c.dim}(${summary})${c.reset}`);
|
|
1055
|
-
}
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
if (sub === 'add') {
|
|
1059
|
-
const text = args.slice(1).join(' ').trim();
|
|
1060
|
-
if (text.length === 0) {
|
|
1061
|
-
console.log(`\n${c.yellow}Usage: /todo add <text>${c.reset}`);
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
const list = addItem(loadTodoList(cwd), { content: text });
|
|
1065
|
-
const result = saveTodoList(cwd, list);
|
|
1066
|
-
if (!result.ok) {
|
|
1067
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
console.log(`\n${c.green}✓ Added todo:${c.reset} ${text}`);
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
if (sub === 'done' || sub === 'complete' || sub === 'cancel') {
|
|
1074
|
-
const id = args[1];
|
|
1075
|
-
if (!id) {
|
|
1076
|
-
console.log(`\n${c.yellow}Usage: /todo ${sub} <id>${c.reset}`);
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
const status = sub === 'cancel' ? 'cancelled' : 'done';
|
|
1080
|
-
let list = loadTodoList(cwd);
|
|
1081
|
-
const exists = list.items.some((it) => it.id === id);
|
|
1082
|
-
if (!exists) {
|
|
1083
|
-
console.log(`\n${c.red}✗ No todo with id "${id}"${c.reset}`);
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
list = setItemStatus(list, { id, status });
|
|
1087
|
-
const result = saveTodoList(cwd, list);
|
|
1088
|
-
if (!result.ok) {
|
|
1089
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
console.log(`\n${c.green}✓ Marked ${status}${c.reset}`);
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
if (sub === 'start' || sub === 'progress') {
|
|
1096
|
-
const id = args[1];
|
|
1097
|
-
if (!id) {
|
|
1098
|
-
console.log(`\n${c.yellow}Usage: /todo ${sub} <id>${c.reset}`);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
let list = loadTodoList(cwd);
|
|
1102
|
-
const exists = list.items.some((it) => it.id === id);
|
|
1103
|
-
if (!exists) {
|
|
1104
|
-
console.log(`\n${c.red}✗ No todo with id "${id}"${c.reset}`);
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
list = setItemStatus(list, { id, status: 'in_progress' });
|
|
1108
|
-
const result = saveTodoList(cwd, list);
|
|
1109
|
-
if (!result.ok) {
|
|
1110
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1111
|
-
return;
|
|
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}`);
|
|
1112
1001
|
}
|
|
1113
|
-
console.log(`\n${c.green}✓ Marked in_progress${c.reset}`);
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
1117
|
-
const id = args[1];
|
|
1118
|
-
if (!id) {
|
|
1119
|
-
console.log(`\n${c.yellow}Usage: /todo remove <id>${c.reset}`);
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
let list = loadTodoList(cwd);
|
|
1123
|
-
const exists = list.items.some((it) => it.id === id);
|
|
1124
|
-
if (!exists) {
|
|
1125
|
-
console.log(`\n${c.red}✗ No todo with id "${id}"${c.reset}`);
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
list = removeItem(list, { id });
|
|
1129
|
-
const result = saveTodoList(cwd, list);
|
|
1130
|
-
if (!result.ok) {
|
|
1131
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
console.log(`\n${c.green}✓ Removed todo${c.reset}`);
|
|
1135
|
-
return;
|
|
1136
1002
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if (!result.ok) {
|
|
1142
|
-
console.log(`\n${c.red}✗ Failed to save todo: ${result.error}${c.reset}`);
|
|
1143
|
-
return;
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
console.log(`\n${c.red}✗ /edit failed: ${err.message}${c.reset}`);
|
|
1005
|
+
try {
|
|
1006
|
+
fs.unlinkSync(tmpFile);
|
|
1144
1007
|
}
|
|
1145
|
-
|
|
1146
|
-
console.log(`\n${c.green}✓ Cleared ${cleared} completed todo(s)${c.reset}`);
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
if (sub === 'help' || sub === '-h' || sub === '--help') {
|
|
1150
|
-
console.log(`\n${c.bold}Usage: /todo <subcommand>${c.reset}`);
|
|
1151
|
-
console.log(` list List all todo items`);
|
|
1152
|
-
console.log(` add <text> Add a new todo`);
|
|
1153
|
-
console.log(` start <id> Mark a todo as in-progress`);
|
|
1154
|
-
console.log(` done <id> Mark a todo as done`);
|
|
1155
|
-
console.log(` cancel <id> Cancel a todo`);
|
|
1156
|
-
console.log(` remove <id> Remove a todo entirely`);
|
|
1157
|
-
console.log(` clear Remove all done/cancelled todos`);
|
|
1158
|
-
return;
|
|
1008
|
+
catch { /* already gone */ }
|
|
1159
1009
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
return;
|
|
1166
|
-
case '/stats':
|
|
1167
|
-
printStats(stats);
|
|
1168
|
-
{
|
|
1169
|
-
const ctxTokens = conversation.getTotalTokens();
|
|
1170
|
-
const ctxLimit = conversation.getContextLimit();
|
|
1171
|
-
const ctxPct = Math.round((ctxTokens / ctxLimit) * 100);
|
|
1172
|
-
const hasSummary = conversation.getSummary() ? ' (compacted)' : '';
|
|
1173
|
-
console.log(`${c.cyan}${c.bold}📊 Context Window${c.reset}`);
|
|
1174
|
-
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
1175
|
-
console.log(` History messages: ${c.bold}${conversation.getMessageCount()}${c.reset}${hasSummary}`);
|
|
1176
|
-
console.log(` Context usage: ${c.bold}${(ctxTokens / 1000).toFixed(0)}k / ${(ctxLimit / 1000).toFixed(0)}k${c.reset} (${ctxPct}%)`);
|
|
1177
|
-
console.log(` Turns: ${c.bold}${conversation.getTurnCount()}${c.reset}`);
|
|
1178
|
-
console.log('');
|
|
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');
|
|
1179
1015
|
}
|
|
1180
|
-
|
|
1181
|
-
case '/runs': {
|
|
1182
|
-
const runs = listRuns(cwd, 12);
|
|
1183
|
-
console.log(runs.length
|
|
1184
|
-
? `\n${runs.map(run => `${run.id} ${run.status} ${run.task.slice(0, 80)}`).join('\n')}`
|
|
1185
|
-
: '\n(no FixO runs recorded)');
|
|
1016
|
+
promptForInput();
|
|
1186
1017
|
return;
|
|
1187
1018
|
}
|
|
1188
|
-
case '/
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
case '/memory':
|
|
1192
|
-
console.log(`\n${readMemory(cwd)}`);
|
|
1193
|
-
return;
|
|
1194
|
-
case '/remember': {
|
|
1195
|
-
const text = args.join(' ').trim();
|
|
1196
|
-
if (!text) {
|
|
1197
|
-
console.log(`\n${c.yellow}Usage: /remember <project fact>${c.reset}`);
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
rl.pause();
|
|
1201
|
-
const confirmed = await p.confirm({ message: `Add to project memory: ${text}?`, initialValue: false });
|
|
1202
|
-
rl.resume();
|
|
1203
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1204
|
-
appendMemory(cwd, text);
|
|
1205
|
-
console.log(`\n${c.green}✓ Memory updated${c.reset}`);
|
|
1019
|
+
case '/pastes': {
|
|
1020
|
+
if (pendingPastes.length === 0) {
|
|
1021
|
+
console.log(`\n${c.dim}No active paste attachments.${c.reset}`);
|
|
1206
1022
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
{
|
|
1212
|
-
const confirmed = await p.confirm({ message: 'Clear FixO project memory?', initialValue: false });
|
|
1213
|
-
rl.resume();
|
|
1214
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1215
|
-
forgetMemory(cwd);
|
|
1216
|
-
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}`);
|
|
1217
1027
|
}
|
|
1218
1028
|
}
|
|
1219
|
-
|
|
1220
|
-
case '/doctor':
|
|
1221
|
-
console.log(`\n${doctor(cwd)}`);
|
|
1222
|
-
return;
|
|
1223
|
-
case '/index': {
|
|
1224
|
-
const index = await buildIndex(cwd);
|
|
1225
|
-
workspaceFiles = index.files.map(f => f.path);
|
|
1226
|
-
console.log(`\n${c.green}✓ Indexed ${index.files.length} files${c.reset}`);
|
|
1029
|
+
promptForInput();
|
|
1227
1030
|
return;
|
|
1228
1031
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const maxAttempts = 3;
|
|
1249
|
-
const modifiedFiles = [];
|
|
1250
|
-
while (attempt <= maxAttempts) {
|
|
1251
|
-
console.log(`\n${c.cyan}🔨 [Auto-Fix] Test failure detected (Attempt ${attempt}/${maxAttempts}). Invoking SingleAgent to repair...${c.reset}`);
|
|
1252
|
-
console.log(`${c.dim}${testResult}${c.reset}\n`);
|
|
1253
|
-
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.`;
|
|
1254
|
-
const context = {
|
|
1255
|
-
task: repairTask,
|
|
1256
|
-
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,
|
|
1257
1051
|
cwd,
|
|
1258
1052
|
verbose,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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,
|
|
1265
1068
|
};
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
isTaskRunning = false;
|
|
1281
|
-
currentRunningAgent = null;
|
|
1282
|
-
agent.reset();
|
|
1283
|
-
}
|
|
1284
|
-
testResult = runProjectTests(cwd);
|
|
1285
|
-
if (testResult.includes('Status: 0')) {
|
|
1286
|
-
console.log(`\n${c.green}✓ All tests passed after repair attempt ${attempt}!${c.reset}`);
|
|
1287
|
-
break;
|
|
1288
|
-
}
|
|
1289
|
-
else {
|
|
1290
|
-
attempt++;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
if (!testResult.includes('Status: 0')) {
|
|
1294
|
-
console.log(`\n${c.red}✗ Auto-fix failed after ${maxAttempts} attempts. Remaining failures:${c.reset}`);
|
|
1295
|
-
console.log(`${c.dim}${testResult}${c.reset}`);
|
|
1296
|
-
}
|
|
1297
|
-
else {
|
|
1298
|
-
// Auto-commit if enabled and changes were made
|
|
1299
|
-
if (config.preferences.autoCommit &&
|
|
1300
|
-
(projectConfig?.autoCommit !== false) &&
|
|
1301
|
-
modifiedFiles.length > 0) {
|
|
1302
|
-
console.log(`\n${c.green}✓ Auto-committing repaired test files...${c.reset}`);
|
|
1303
|
-
git.autoCommit('fix-tests: repair test failures', modifiedFiles);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
case '/fix-ci':
|
|
1309
|
-
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}`);
|
|
1310
|
-
return;
|
|
1311
|
-
case '/plan':
|
|
1312
|
-
{
|
|
1313
|
-
const task = args.join(' ').trim();
|
|
1314
|
-
if (!task) {
|
|
1315
|
-
console.log(`\n${c.yellow}Usage: /plan <task>${c.reset}`);
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
const plan = savePlan(cwd, task);
|
|
1319
|
-
console.log(`\n${renderPlan(plan)}`);
|
|
1320
|
-
}
|
|
1321
|
-
return;
|
|
1322
|
-
case '/run-plan': {
|
|
1323
|
-
const dagFile = path.join(cwd, '.fixo', 'last-dag.json');
|
|
1324
|
-
if (fs.existsSync(dagFile)) {
|
|
1325
|
-
try {
|
|
1326
|
-
const { task, dag } = JSON.parse(fs.readFileSync(dagFile, 'utf-8'));
|
|
1327
|
-
console.log(`\n${c.cyan}[Saved Plan] Executing saved subtasks DAG for task: ${c.bold}${task}${c.reset}`);
|
|
1328
|
-
const { AgentPool } = await import('../agent/agent-pool.js');
|
|
1329
|
-
const pool = new AgentPool(3, projectConfig?.maxAttempts ?? 12);
|
|
1330
|
-
const context = {
|
|
1331
|
-
task,
|
|
1332
|
-
model: currentModel,
|
|
1333
|
-
cwd,
|
|
1334
|
-
verbose,
|
|
1335
|
-
selectedFiles: [...selectedFiles],
|
|
1336
|
-
systemPromptOverride: projectConfig?.systemPrompt,
|
|
1337
|
-
checkCommand: projectConfig?.checkCommand,
|
|
1338
|
-
policy: projectConfig?.policy ?? config.preferences.policy,
|
|
1339
|
-
mode: currentMode,
|
|
1340
|
-
};
|
|
1341
|
-
const success = await pool.execute(context, dag);
|
|
1342
|
-
if (success) {
|
|
1343
|
-
console.log(`\n${c.green}✓ Successfully completed complex task via parallel agents.${c.reset}`);
|
|
1344
|
-
}
|
|
1345
|
-
else {
|
|
1346
|
-
console.log(`\n${c.red}✗ Parallel workers failed to complete all subtasks.${c.reset}`);
|
|
1347
|
-
if (git.isGitRepo()) {
|
|
1348
|
-
console.log(`\n${c.yellow}[Agent Pool] Rolling back all uncommitted changes due to run failure...${c.reset}`);
|
|
1349
|
-
git.discardUncommittedChanges();
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
catch (err) {
|
|
1355
|
-
console.log(`\n${c.red}✗ Failed to run saved DAG: ${err.message}${c.reset}`);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
const plan = loadPlan(cwd);
|
|
1359
|
-
if (!plan) {
|
|
1360
|
-
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}`);
|
|
1361
|
-
return;
|
|
1362
|
-
}
|
|
1363
|
-
console.log(`\n${c.dim}Executing saved plan task: ${plan.task}${c.reset}`);
|
|
1364
|
-
await handleInput(plan.task);
|
|
1365
|
-
return;
|
|
1366
|
-
}
|
|
1367
|
-
case '/mode': {
|
|
1368
|
-
rl.pause();
|
|
1369
|
-
const selected = await p.select({
|
|
1370
|
-
message: 'Select execution mode:',
|
|
1371
|
-
options: [
|
|
1372
|
-
{ value: 'PLAN', label: 'PLAN Mode (Read-only, dry-run simulation)' },
|
|
1373
|
-
{ value: 'BUILD', label: 'BUILD Mode (Writing & modifying allowed)' },
|
|
1374
|
-
{ value: 'EXPLORE', label: 'EXPLORE Mode (Code exploration & LSP, no modifying)' },
|
|
1375
|
-
{ value: 'SCOUT', label: 'SCOUT Mode (Web search & fetch only)' },
|
|
1376
|
-
],
|
|
1377
|
-
initialValue: currentMode,
|
|
1378
|
-
});
|
|
1379
|
-
rl.resume();
|
|
1380
|
-
if (!p.isCancel(selected) && selected) {
|
|
1381
|
-
currentMode = selected;
|
|
1382
|
-
console.log(`\n${c.green}✓ Execution mode set to: ${c.bold}${currentMode}${c.reset}`);
|
|
1383
|
-
}
|
|
1384
|
-
else {
|
|
1385
|
-
console.log(`\n${c.dim}Execution mode remains: ${c.cyan}${currentMode}${c.reset}`);
|
|
1386
|
-
}
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
case '/session': {
|
|
1390
|
-
const sub = args[0];
|
|
1391
|
-
const { SessionManager } = await import('../agent/conversation.js');
|
|
1392
|
-
if (sub === 'list') {
|
|
1393
|
-
const list = SessionManager.listSessions();
|
|
1394
|
-
if (list.length === 0) {
|
|
1395
|
-
console.log(`\n${c.dim}No saved sessions found.${c.reset}`);
|
|
1396
|
-
}
|
|
1397
|
-
else {
|
|
1398
|
-
console.log(`\n${c.cyan}${c.bold}Saved Sessions:${c.reset}`);
|
|
1399
|
-
for (const s of list) {
|
|
1400
|
-
const date = new Date(s.timestamp).toLocaleString();
|
|
1401
|
-
console.log(` ${c.cyan}${s.sessionId}${c.reset} - ${c.bold}${s.model}${c.reset} (${s.messageCount} msgs)`);
|
|
1402
|
-
console.log(` ${c.dim}Created: ${date} | Tokens: ${s.totalTokens.toLocaleString()}${c.reset}`);
|
|
1403
|
-
if (s.summary) {
|
|
1404
|
-
console.log(` ${c.dim}Summary: ${s.summary.slice(0, 80)}...${c.reset}`);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
else if (sub === 'load') {
|
|
1410
|
-
const uuid = args[1];
|
|
1411
|
-
if (!uuid) {
|
|
1412
|
-
console.log(`\n${c.yellow}Usage: /session load <uuid>${c.reset}`);
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
try {
|
|
1416
|
-
const data = SessionManager.loadSession(uuid);
|
|
1417
|
-
conversation.clear();
|
|
1418
|
-
conversation.importHistory(data.history);
|
|
1419
|
-
conversation.setSummary(data.summary || '');
|
|
1420
|
-
currentModel = data.model;
|
|
1421
|
-
conversation.setContextLimit(currentModel);
|
|
1422
|
-
sessionModifiedFiles = data.modifiedFiles || [];
|
|
1423
|
-
currentSessionId = data.sessionId;
|
|
1424
|
-
stats.totalPromptTokens = data.tokenUsage?.prompt_tokens || 0;
|
|
1425
|
-
stats.totalCompletionTokens = data.tokenUsage?.completion_tokens || 0;
|
|
1426
|
-
console.log(`\n${c.green}✓ Session restored successfully: ${c.bold}${uuid}${c.reset}`);
|
|
1427
|
-
console.log(`${c.dim} Model set to: ${c.cyan}${currentModel}${c.reset}`);
|
|
1428
|
-
}
|
|
1429
|
-
catch (err) {
|
|
1430
|
-
console.log(`\n${c.red}✗ Failed to load session: ${err.message}${c.reset}`);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
else if (sub === 'new') {
|
|
1434
|
-
conversation.clear();
|
|
1435
|
-
sessionModifiedFiles = [];
|
|
1436
|
-
stats.totalPromptTokens = 0;
|
|
1437
|
-
stats.totalCompletionTokens = 0;
|
|
1438
|
-
stats.totalToolCalls = 0;
|
|
1439
|
-
stats.totalTasks = 0;
|
|
1440
|
-
stats.totalDurationMs = 0;
|
|
1441
|
-
const { randomUUID } = await import('node:crypto');
|
|
1442
|
-
currentSessionId = randomUUID();
|
|
1443
|
-
SessionManager.saveSession(conversation, currentModel, sessionModifiedFiles, {
|
|
1444
|
-
prompt_tokens: stats.totalPromptTokens,
|
|
1445
|
-
completion_tokens: stats.totalCompletionTokens,
|
|
1446
|
-
total_tokens: stats.totalPromptTokens + stats.totalCompletionTokens,
|
|
1447
|
-
}, currentSessionId);
|
|
1448
|
-
console.log(`\n${c.green}✓ Active conversation memory purged. New session initialized: ${c.bold}${currentSessionId}${c.reset}`);
|
|
1449
|
-
}
|
|
1450
|
-
else {
|
|
1451
|
-
console.log(`\n${c.yellow}Usage: /session [list | load <uuid> | new]${c.reset}`);
|
|
1452
|
-
}
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
case '/providers': {
|
|
1456
|
-
const sub = args[0];
|
|
1457
|
-
// ── Interactive flow (bare `/providers`): mirrors the
|
|
1458
|
-
// /model picker shape. The user picks a provider, then
|
|
1459
|
-
// an action, then enters a masked API key via p.password
|
|
1460
|
-
// when the action is add/update. The legacy text routes
|
|
1461
|
-
// below remain unchanged for muscle-memory + scripting.
|
|
1462
|
-
if (!sub) {
|
|
1463
|
-
rl.pause();
|
|
1464
|
-
const pickedProvider = await p.select({
|
|
1465
|
-
message: 'Select an AI provider:',
|
|
1466
|
-
options: PROVIDER_REGISTRY.map(def => ({
|
|
1467
|
-
value: def.name,
|
|
1468
|
-
label: def.displayName,
|
|
1469
|
-
hint: ProvidersManager.has(def.name) ? '[key ✓]' : '[no key]',
|
|
1470
|
-
})),
|
|
1471
|
-
});
|
|
1472
|
-
rl.resume();
|
|
1473
|
-
if (p.isCancel(pickedProvider)) {
|
|
1474
|
-
console.log(`\n${c.dim}/providers cancelled.${c.reset}`);
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
|
-
const def = ProvidersManager.getDefinition(pickedProvider);
|
|
1478
|
-
if (!def) {
|
|
1479
|
-
console.log(`\n${c.red}✗ Unknown provider: ${pickedProvider}${c.reset}`);
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
const hasKey = ProvidersManager.has(def.name);
|
|
1483
|
-
rl.pause();
|
|
1484
|
-
const action = await p.select({
|
|
1485
|
-
message: `${def.displayName} — choose an action:`,
|
|
1486
|
-
options: [
|
|
1487
|
-
{ value: 'add', label: hasKey ? 'Update API key' : 'Add API key' },
|
|
1488
|
-
{ value: 'test', label: 'Test connection', hint: hasKey ? '' : 'requires a key' },
|
|
1489
|
-
{ value: 'remove', label: 'Remove API key', hint: hasKey ? '' : 'no key configured' },
|
|
1490
|
-
{ value: 'cancel', label: 'Cancel' },
|
|
1491
|
-
],
|
|
1492
|
-
});
|
|
1493
|
-
rl.resume();
|
|
1494
|
-
if (p.isCancel(action) || action === 'cancel') {
|
|
1495
|
-
console.log(`\n${c.dim}/providers cancelled.${c.reset}`);
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
if (action === 'add') {
|
|
1499
|
-
console.log(`${c.dim} Get your API key at: ${def.docsUrl}${c.reset}`);
|
|
1500
|
-
rl.pause();
|
|
1501
|
-
const key = await p.password({
|
|
1502
|
-
message: `Enter your ${def.displayName} API key:`,
|
|
1503
|
-
validate: v => !v?.trim() ? 'API key is required' : undefined,
|
|
1504
|
-
});
|
|
1505
|
-
rl.resume();
|
|
1506
|
-
if (p.isCancel(key)) {
|
|
1507
|
-
console.log(`\n${c.dim}/providers cancelled.${c.reset}`);
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
ProvidersManager.add(def.name, key);
|
|
1511
|
-
console.log(`\n${c.green}✓ ${def.displayName} API key saved securely to ~/.fixocli/providers.json${c.reset}`);
|
|
1512
|
-
await refreshModelsForProvider(def.name);
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
if (action === 'remove') {
|
|
1516
|
-
if (!hasKey) {
|
|
1517
|
-
console.log(`\n${c.yellow}No key configured for ${def.displayName}.${c.reset}`);
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
rl.pause();
|
|
1521
|
-
const confirmed = await p.confirm({
|
|
1522
|
-
message: `Remove API key for ${def.displayName}?`,
|
|
1523
|
-
initialValue: false,
|
|
1524
|
-
});
|
|
1525
|
-
rl.resume();
|
|
1526
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1527
|
-
const removed = ProvidersManager.remove(def.name);
|
|
1528
|
-
console.log(removed
|
|
1529
|
-
? `\n${c.green}✓ Removed API key for ${def.displayName}.${c.reset}`
|
|
1530
|
-
: `\n${c.yellow}No key found for provider: ${def.name}${c.reset}`);
|
|
1531
|
-
}
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
if (action === 'test') {
|
|
1535
|
-
if (!hasKey) {
|
|
1536
|
-
console.log(`\n${c.yellow}No key configured for ${def.displayName}. Add one first.${c.reset}`);
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
console.log(`\n${c.dim}Testing connection to ${def.displayName} via live /models fetch…${c.reset}`);
|
|
1540
|
-
await refreshModelsForProvider(def.name);
|
|
1541
|
-
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;
|
|
1542
1083
|
}
|
|
1543
1084
|
return;
|
|
1544
1085
|
}
|
|
1545
|
-
if (sub === 'list') {
|
|
1546
|
-
const list = ProvidersManager.list();
|
|
1547
|
-
if (list.length === 0) {
|
|
1548
|
-
console.log(`\n${c.yellow}No providers configured.${c.reset}`);
|
|
1549
|
-
console.log(`${c.dim} Use /providers add <name> to connect a provider (e.g. /providers add groq)${c.reset}`);
|
|
1550
|
-
console.log(`${c.dim} Available: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1551
|
-
}
|
|
1552
|
-
else {
|
|
1553
|
-
console.log(`\n${c.bold}${c.cyan}Connected Providers${c.reset}`);
|
|
1554
|
-
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1555
|
-
for (const entry of list) {
|
|
1556
|
-
const addedDate = new Date(entry.addedAt).toLocaleDateString();
|
|
1557
|
-
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}`);
|
|
1558
|
-
}
|
|
1559
|
-
console.log(`\n${c.dim} Use /providers remove <name> to remove a key.${c.reset}`);
|
|
1560
|
-
console.log(`${c.dim} Use /providers test <name> to verify a connection.${c.reset}`);
|
|
1561
|
-
}
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
if (sub === 'add') {
|
|
1565
|
-
const name = args[1]?.toLowerCase();
|
|
1566
|
-
if (!name) {
|
|
1567
|
-
console.log(`\n${c.yellow}Usage: /providers add <provider-name>${c.reset}`);
|
|
1568
|
-
console.log(`${c.dim} Available: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
const def = ProvidersManager.getDefinition(name);
|
|
1572
|
-
if (!def) {
|
|
1573
|
-
console.log(`\n${c.red}✗ Unknown provider: ${name}${c.reset}`);
|
|
1574
|
-
console.log(`${c.dim} Available: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
console.log(`\n${c.cyan}${c.bold}Connecting to ${def.displayName}${c.reset}`);
|
|
1578
|
-
console.log(`${c.dim} Get your API key at: ${def.docsUrl}${c.reset}`);
|
|
1579
|
-
rl.pause();
|
|
1580
|
-
const apiKeyInput = await p.text({
|
|
1581
|
-
message: `Enter your ${def.displayName} API key:`,
|
|
1582
|
-
placeholder: 'sk-... or gsk_...',
|
|
1583
|
-
validate: v => !v.trim() ? 'API key is required' : undefined,
|
|
1584
|
-
});
|
|
1585
|
-
rl.resume();
|
|
1586
|
-
if (p.isCancel(apiKeyInput)) {
|
|
1587
|
-
console.log(`\n${c.dim}Provider add cancelled.${c.reset}`);
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
ProvidersManager.add(name, apiKeyInput);
|
|
1591
|
-
console.log(`\n${c.green}✓ ${def.displayName} API key saved securely to ~/.fixocli/providers.json${c.reset}`);
|
|
1592
|
-
console.log(`${c.dim} FixO will now route ${def.displayName} requests directly (bypassing the SaaS proxy).${c.reset}`);
|
|
1593
|
-
await refreshModelsForProvider(name);
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
if (sub === 'remove') {
|
|
1597
|
-
const name = args[1]?.toLowerCase();
|
|
1598
|
-
if (!name) {
|
|
1599
|
-
console.log(`\n${c.yellow}Usage: /providers remove <name>${c.reset}`);
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
rl.pause();
|
|
1603
|
-
const confirmed = await p.confirm({ message: `Remove API key for ${name}?`, initialValue: false });
|
|
1604
|
-
rl.resume();
|
|
1605
|
-
if (!p.isCancel(confirmed) && confirmed) {
|
|
1606
|
-
const removed = ProvidersManager.remove(name);
|
|
1607
|
-
console.log(removed
|
|
1608
|
-
? `\n${c.green}✓ Removed API key for ${name}.${c.reset}`
|
|
1609
|
-
: `\n${c.yellow}No key found for provider: ${name}${c.reset}`);
|
|
1610
|
-
}
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
if (sub === 'test') {
|
|
1614
|
-
const name = args[1]?.toLowerCase();
|
|
1615
|
-
if (!name) {
|
|
1616
|
-
console.log(`\n${c.yellow}Usage: /providers test <name>${c.reset}`);
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
const directConf = ProvidersManager.getDirectConfig(name);
|
|
1620
|
-
if (!directConf) {
|
|
1621
|
-
console.log(`\n${c.yellow}No key configured for ${name}. Use /providers add ${name} first.${c.reset}`);
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
console.log(`\n${c.dim}Testing connection to ${directConf.displayName} (${directConf.baseUrl})...${c.reset}`);
|
|
1625
|
-
try {
|
|
1626
|
-
const testHeaders = {
|
|
1627
|
-
'Authorization': `Bearer ${directConf.apiKey}`,
|
|
1628
|
-
};
|
|
1629
|
-
if (name === 'zen' || name === 'openrouter') {
|
|
1630
|
-
testHeaders['HTTP-Referer'] = 'https://opencode.ai/';
|
|
1631
|
-
testHeaders['X-Title'] = 'opencode';
|
|
1632
|
-
}
|
|
1633
|
-
else if (name === 'nvidia') {
|
|
1634
|
-
testHeaders['HTTP-Referer'] = 'https://opencode.ai/';
|
|
1635
|
-
testHeaders['X-Title'] = 'opencode';
|
|
1636
|
-
testHeaders['X-BILLING-INVOKE-ORIGIN'] = 'OpenCode';
|
|
1637
|
-
}
|
|
1638
|
-
else if (name === 'cerebras') {
|
|
1639
|
-
testHeaders['X-Cerebras-3rd-Party-Integration'] = 'opencode';
|
|
1640
|
-
}
|
|
1641
|
-
const resp = await fetch(`${directConf.baseUrl}/models`, {
|
|
1642
|
-
headers: testHeaders,
|
|
1643
|
-
signal: AbortSignal.timeout(8000),
|
|
1644
|
-
});
|
|
1645
|
-
if (resp.ok) {
|
|
1646
|
-
console.log(`${c.green}✓ Connection to ${directConf.displayName} successful! (HTTP ${resp.status})${c.reset}`);
|
|
1647
|
-
// Warm the cache so /model picker shows live IDs.
|
|
1648
|
-
await refreshModelsForProvider(name);
|
|
1649
|
-
}
|
|
1650
|
-
else {
|
|
1651
|
-
const text = await resp.text().catch(() => '');
|
|
1652
|
-
console.log(`${c.red}✗ ${directConf.displayName} returned HTTP ${resp.status}${text ? ': ' + text.slice(0, 100) : ''}${c.reset}`);
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
catch (err) {
|
|
1656
|
-
console.log(`${c.red}✗ Connection failed: ${err.message}${c.reset}`);
|
|
1657
|
-
}
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
console.log(`\n${c.yellow}Usage: /providers [list | add <name> | remove <name> | test <name>]${c.reset}`);
|
|
1661
|
-
console.log(`${c.dim} Available providers: ${PROVIDER_REGISTRY.map(p => p.name).join(', ')}${c.reset}`);
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
case '/compact': {
|
|
1665
|
-
const msgCount = conversation.getMessageCount();
|
|
1666
|
-
if (msgCount === 0) {
|
|
1667
|
-
console.log(`\n${c.dim}Nothing to compact — conversation is empty.${c.reset}`);
|
|
1668
|
-
return;
|
|
1669
|
-
}
|
|
1670
|
-
const tokensBefore = conversation.getTotalTokens();
|
|
1671
|
-
const contextLimit = conversation.getContextLimit();
|
|
1672
|
-
console.log(`\n${c.cyan}[Compact] Summarising ${msgCount} messages to free context tokens...${c.reset}`);
|
|
1673
|
-
console.log(`${c.dim} Current context: ${(tokensBefore / 1000).toFixed(0)}k / ${(contextLimit / 1000).toFixed(0)}k tokens${c.reset}`);
|
|
1674
|
-
try {
|
|
1675
|
-
const compacted = await conversation.compact(agent.getClient(), currentModel);
|
|
1676
|
-
if (compacted) {
|
|
1677
|
-
const info = conversation.getLastCompactionInfo();
|
|
1678
|
-
const tokensAfter = conversation.getTotalTokens();
|
|
1679
|
-
console.log(`${c.green}✓ Compacted: ${info?.messagesBefore ?? msgCount} messages → summary + ${conversation.getMessageCount()} recent messages.${c.reset}`);
|
|
1680
|
-
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}`);
|
|
1681
|
-
}
|
|
1682
|
-
else {
|
|
1683
|
-
console.log(`${c.dim}Not enough messages to compact (need more than 4 messages).${c.reset}`);
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
catch (err) {
|
|
1687
|
-
console.log(`${c.red}✗ Compact failed: ${err.message}${c.reset}`);
|
|
1688
|
-
}
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
case '/snapshot': {
|
|
1692
|
-
const label = args.join(' ').trim() || `snapshot-${Date.now()}`;
|
|
1693
|
-
if (!git.isGitRepo()) {
|
|
1694
|
-
console.log(`\n${c.yellow}⚠ Not a git repository — cannot create snapshot.${c.reset}`);
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
const hash = git.createSnapshot(label);
|
|
1698
|
-
if (hash) {
|
|
1699
|
-
console.log(`\n${c.green}✓ Workspace snapshot created: ${c.bold}${hash}${c.reset}${c.dim} (label: ${label})${c.reset}`);
|
|
1700
|
-
console.log(`${c.dim} Use /undo or git revert to roll back to this point.${c.reset}`);
|
|
1701
|
-
}
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
case '/skills': {
|
|
1705
|
-
const { skillsManager } = await import('../agent/skills.js');
|
|
1706
|
-
const list = skillsManager.getSkills();
|
|
1707
|
-
if (list.length === 0) {
|
|
1708
|
-
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}`);
|
|
1709
|
-
}
|
|
1710
|
-
else {
|
|
1711
|
-
console.log(`\n${c.cyan}${c.bold}Registered Skills:${c.reset}`);
|
|
1712
|
-
for (const skill of list) {
|
|
1713
|
-
console.log(` - ${c.bold}${skill.name}${c.reset}${skill.description ? `: ${skill.description}` : ''} ${c.dim}(${skill.location})${c.reset}`);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
return;
|
|
1717
|
-
}
|
|
1718
|
-
case '/theme':
|
|
1719
|
-
case '/variant': {
|
|
1720
|
-
const { themeMode, setThemeMode } = await import('./colors.js');
|
|
1721
|
-
const newMode = themeMode === 'dark' ? 'inverted' : 'dark';
|
|
1722
|
-
setThemeMode(newMode);
|
|
1723
|
-
console.log(`\n${c.cyan}✓ Theme set to: ${newMode === 'dark' ? 'Dark Void Minimalist' : 'High-Contrast Inverted'}${c.reset}`);
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
case '/telemetry': {
|
|
1727
|
-
const sub = args[0]?.toLowerCase();
|
|
1728
|
-
if (sub === 'on' || sub === 'enable') {
|
|
1729
|
-
config.preferences.telemetry = true;
|
|
1730
|
-
saveConfig(config);
|
|
1731
|
-
console.log(`\n${c.green}✓ Telemetry enabled${c.reset}`);
|
|
1732
|
-
}
|
|
1733
|
-
else if (sub === 'off' || sub === 'disable') {
|
|
1734
|
-
config.preferences.telemetry = false;
|
|
1735
|
-
saveConfig(config);
|
|
1736
|
-
console.log(`\n${c.green}✓ Telemetry disabled${c.reset}`);
|
|
1737
|
-
}
|
|
1738
|
-
else {
|
|
1739
|
-
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}`);
|
|
1740
|
-
}
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
default:
|
|
1744
1086
|
console.log(`\n${c.yellow}Unknown command: ${cmd}. Type /help for available commands.${c.reset}`);
|
|
1745
1087
|
return;
|
|
1088
|
+
}
|
|
1746
1089
|
}
|
|
1747
1090
|
}
|
|
1748
1091
|
// ─── Shell commands (! prefix) ───
|
|
@@ -1783,20 +1126,48 @@ export async function startREPL(options) {
|
|
|
1783
1126
|
console.log(output);
|
|
1784
1127
|
}
|
|
1785
1128
|
catch (error) {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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}`);
|
|
1790
1134
|
}
|
|
1791
1135
|
return;
|
|
1792
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
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1793
1164
|
// ─── Agent task ───
|
|
1794
1165
|
// Format any paths in the input for display
|
|
1795
1166
|
const displayInput = formatInputPaths(input, cwd);
|
|
1796
1167
|
if (displayInput !== input) {
|
|
1797
1168
|
// Re-display with highlighted paths
|
|
1798
1169
|
process.stdout.write(`\x1b[1A\x1b[2K`); // Move up and clear line
|
|
1799
|
-
console.log(
|
|
1170
|
+
console.log(`> ${displayInput}`);
|
|
1800
1171
|
}
|
|
1801
1172
|
// Extract any file paths from input for automatic pinning
|
|
1802
1173
|
const pathsInInput = extractFilePaths(input, cwd);
|
|
@@ -1816,105 +1187,34 @@ export async function startREPL(options) {
|
|
|
1816
1187
|
// Drain the queue — attachments are one-shot. The agent has its
|
|
1817
1188
|
// own copy via context above.
|
|
1818
1189
|
pendingAttachments = [];
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
for (const sub of dag.subtasks) {
|
|
1838
|
-
const deps = sub.dependencies.length > 0 ? ` (deps: ${sub.dependencies.join(', ')})` : '';
|
|
1839
|
-
const lineStr = ` - [${sub.persona.toUpperCase()}] ${sub.title}${deps}`;
|
|
1840
|
-
const pad = Math.max(0, width - lineStr.length - 4);
|
|
1841
|
-
console.log(`${c.cyan}│${c.reset} ${c.bold}${lineStr}${c.reset}${' '.repeat(pad)} ${c.cyan}│${c.reset}`);
|
|
1842
|
-
}
|
|
1843
|
-
console.log(`${c.cyan}${borderBottom}${c.reset}\n`);
|
|
1844
|
-
// Save the DAG to .fixo/last-dag.json
|
|
1845
|
-
const fixoDir = path.join(cwd, '.fixo');
|
|
1846
|
-
fs.mkdirSync(fixoDir, { recursive: true });
|
|
1847
|
-
fs.writeFileSync(path.join(fixoDir, 'last-dag.json'), JSON.stringify({ task: input, dag }, null, 2), 'utf-8');
|
|
1848
|
-
if (currentMode === 'PLAN') {
|
|
1849
|
-
console.log(`${c.green}✓ Plan generated and saved successfully.${c.reset}`);
|
|
1850
|
-
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`);
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
const budgetLimit = projectConfig?.maxAttempts ?? 12;
|
|
1854
|
-
const pool = new AgentPool(3, budgetLimit);
|
|
1855
|
-
console.log(`\n${c.cyan}[Agent Pool] Executing DAG of subtasks (concurrency limit: 3, budget: ${budgetLimit} tool calls)...${c.reset}`);
|
|
1856
|
-
const success = await pool.execute(context, dag);
|
|
1857
|
-
const durationMs = Date.now() - startTime;
|
|
1858
|
-
const totalPromptTokens = orchestrator.tokensUsed.prompt_tokens + pool.tokensUsed.prompt_tokens;
|
|
1859
|
-
const totalCompletionTokens = orchestrator.tokensUsed.completion_tokens + pool.tokensUsed.completion_tokens;
|
|
1860
|
-
// Find modified files to report
|
|
1861
|
-
const { getModifiedFiles, getBranchPoint } = await import('../agent/worker-agent.js');
|
|
1862
|
-
const relativeModified = getModifiedFiles(cwd, getBranchPoint(cwd));
|
|
1863
|
-
const modifiedFiles = relativeModified.map(f => path.resolve(cwd, f));
|
|
1864
|
-
if (!success) {
|
|
1865
|
-
console.log(`\n${c.red}✗ Parallel workers failed to complete all subtasks.${c.reset}`);
|
|
1866
|
-
if (git.isGitRepo()) {
|
|
1867
|
-
console.log(`\n${c.yellow}[Agent Pool] Rolling back all uncommitted changes due to run failure...${c.reset}`);
|
|
1868
|
-
git.discardUncommittedChanges();
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
result = {
|
|
1872
|
-
success,
|
|
1873
|
-
response: success
|
|
1874
|
-
? 'Successfully completed complex task via parallel agents.'
|
|
1875
|
-
: 'Failed to complete all complex subtasks.',
|
|
1876
|
-
modifiedFiles,
|
|
1877
|
-
tokensUsed: {
|
|
1878
|
-
prompt_tokens: totalPromptTokens,
|
|
1879
|
-
completion_tokens: totalCompletionTokens,
|
|
1880
|
-
total_tokens: totalPromptTokens + totalCompletionTokens
|
|
1881
|
-
},
|
|
1882
|
-
toolCallCount: pool.toolCallCount,
|
|
1883
|
-
durationMs,
|
|
1884
|
-
model: context.model,
|
|
1885
|
-
};
|
|
1886
|
-
}
|
|
1887
|
-
catch (err) {
|
|
1888
|
-
console.error(`\n${c.red}✗ Orchestrated execution failed: ${err.message || err}${c.reset}`);
|
|
1889
|
-
if (git.isGitRepo()) {
|
|
1890
|
-
console.log(`\n${c.yellow}[Agent Pool] Rolling back all uncommitted changes due to error...${c.reset}`);
|
|
1891
|
-
git.discardUncommittedChanges();
|
|
1892
|
-
}
|
|
1893
|
-
const durationMs = Date.now() - startTime;
|
|
1894
|
-
result = {
|
|
1895
|
-
success: false,
|
|
1896
|
-
response: `Orchestrated run failed: ${err.message || err}`,
|
|
1897
|
-
modifiedFiles: [],
|
|
1898
|
-
tokensUsed: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
1899
|
-
toolCallCount: 0,
|
|
1900
|
-
durationMs,
|
|
1901
|
-
model: context.model,
|
|
1902
|
-
};
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
else {
|
|
1906
|
-
console.log(`\n${c.cyan}[Routing Engine] Simple task detected (${classification.reason}). Routing to SingleAgent...${c.reset}`);
|
|
1907
|
-
isTaskRunning = true;
|
|
1908
|
-
currentRunningAgent = agent;
|
|
1909
|
-
try {
|
|
1910
|
-
result = await agent.runStreaming(context, conversation, rl);
|
|
1911
|
-
}
|
|
1912
|
-
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: () => {
|
|
1913
1208
|
isTaskRunning = false;
|
|
1914
1209
|
currentRunningAgent = null;
|
|
1915
|
-
|
|
1916
|
-
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
if (routed.route === 'plan-mode-deferred') {
|
|
1213
|
+
pendingPastes = [];
|
|
1214
|
+
return;
|
|
1917
1215
|
}
|
|
1216
|
+
const result = routed.result;
|
|
1217
|
+
pendingPastes = [];
|
|
1918
1218
|
// Print result summary
|
|
1919
1219
|
console.log('');
|
|
1920
1220
|
const modelPart = result.model ? `${result.model} · ` : '';
|
|
@@ -1985,7 +1285,25 @@ export async function startREPL(options) {
|
|
|
1985
1285
|
prompt_tokens: stats.totalPromptTokens,
|
|
1986
1286
|
completion_tokens: stats.totalCompletionTokens,
|
|
1987
1287
|
total_tokens: stats.totalPromptTokens + stats.totalCompletionTokens,
|
|
1988
|
-
}, currentSessionId);
|
|
1288
|
+
}, currentSessionId, currentSessionLabel);
|
|
1289
|
+
const { saveSnapshot } = await import('../runtime/session-snapshots.js');
|
|
1290
|
+
saveSnapshot({
|
|
1291
|
+
cwd,
|
|
1292
|
+
conversation: conversation.exportHistory().map((m, idx) => ({
|
|
1293
|
+
role: m.role,
|
|
1294
|
+
content: m.content || '',
|
|
1295
|
+
name: m.name,
|
|
1296
|
+
index: idx,
|
|
1297
|
+
})),
|
|
1298
|
+
tokens: stats.totalPromptTokens + stats.totalCompletionTokens,
|
|
1299
|
+
model: currentModel,
|
|
1300
|
+
mode: currentMode,
|
|
1301
|
+
selectedFiles: [...selectedFiles],
|
|
1302
|
+
summary: conversation.getSummary(),
|
|
1303
|
+
label: currentSessionLabel,
|
|
1304
|
+
id: currentSessionId,
|
|
1305
|
+
fixedInstructions: projectConfig?.systemPrompt,
|
|
1306
|
+
});
|
|
1989
1307
|
}
|
|
1990
1308
|
catch (err) {
|
|
1991
1309
|
// Ignore session save errors
|