nex-code 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/bundle.js +505 -0
- package/dist/nex-code.js +485 -0
- package/package.json +8 -6
- package/bin/nex-code.js +0 -99
- package/cli/agent.js +0 -835
- package/cli/compactor.js +0 -85
- package/cli/context-engine.js +0 -507
- package/cli/context.js +0 -98
- package/cli/costs.js +0 -290
- package/cli/diff.js +0 -366
- package/cli/file-history.js +0 -94
- package/cli/format.js +0 -211
- package/cli/fuzzy-match.js +0 -270
- package/cli/git.js +0 -211
- package/cli/hooks.js +0 -173
- package/cli/index.js +0 -1289
- package/cli/mcp.js +0 -284
- package/cli/memory.js +0 -170
- package/cli/ollama.js +0 -130
- package/cli/permissions.js +0 -124
- package/cli/picker.js +0 -201
- package/cli/planner.js +0 -282
- package/cli/providers/anthropic.js +0 -333
- package/cli/providers/base.js +0 -116
- package/cli/providers/gemini.js +0 -239
- package/cli/providers/local.js +0 -249
- package/cli/providers/ollama.js +0 -228
- package/cli/providers/openai.js +0 -237
- package/cli/providers/registry.js +0 -454
- package/cli/render.js +0 -495
- package/cli/safety.js +0 -241
- package/cli/session.js +0 -133
- package/cli/skills.js +0 -412
- package/cli/spinner.js +0 -371
- package/cli/sub-agent.js +0 -441
- package/cli/tasks.js +0 -179
- package/cli/tool-tiers.js +0 -164
- package/cli/tool-validator.js +0 -138
- package/cli/tools.js +0 -1050
- package/cli/ui.js +0 -93
package/cli/index.js
DELETED
|
@@ -1,1289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/index.js — Main REPL + Command Handling
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const { C, banner, cleanupTerminal } = require('./ui');
|
|
9
|
-
const { processInput, clearConversation, getConversationLength, getConversationMessages, setConversationMessages, setAbortSignalGetter } = require('./agent');
|
|
10
|
-
const { getActiveModel, setActiveModel, getModelNames } = require('./ollama');
|
|
11
|
-
const { listProviders, getActiveProviderName, listAllModels, setFallbackChain, getFallbackChain, getProvider } = require('./providers/registry');
|
|
12
|
-
const { printContext } = require('./context');
|
|
13
|
-
const { setAutoConfirm, getAutoConfirm, setReadlineInterface } = require('./safety');
|
|
14
|
-
const { getUsage } = require('./context-engine');
|
|
15
|
-
const { TOOL_DEFINITIONS } = require('./tools');
|
|
16
|
-
const { saveSession, loadSession, listSessions, getLastSession } = require('./session');
|
|
17
|
-
const { remember, forget, listMemories } = require('./memory');
|
|
18
|
-
const { listPermissions, setPermission, savePermissions } = require('./permissions');
|
|
19
|
-
const {
|
|
20
|
-
createPlan, getActivePlan, setPlanMode, isPlanMode,
|
|
21
|
-
approvePlan, startExecution, formatPlan, savePlan, listPlans, clearPlan,
|
|
22
|
-
setAutonomyLevel, getAutonomyLevel, AUTONOMY_LEVELS,
|
|
23
|
-
} = require('./planner');
|
|
24
|
-
const { isGitRepo, getCurrentBranch, formatDiffSummary, analyzeDiff, commit, createBranch } = require('./git');
|
|
25
|
-
const { listServers, connectAll, disconnectAll } = require('./mcp');
|
|
26
|
-
const { listHooks, runHooks, HOOK_EVENTS } = require('./hooks');
|
|
27
|
-
const { undo, redo, getHistory, getUndoCount, getRedoCount, clearHistory } = require('./file-history');
|
|
28
|
-
const { formatCosts, resetCosts, setCostLimit, removeCostLimit, getCostLimits, checkBudget, getProviderSpend, saveCostLimits } = require('./costs');
|
|
29
|
-
const { loadAllSkills, listSkills, enableSkill, disableSkill, getSkillCommands, handleSkillCommand } = require('./skills');
|
|
30
|
-
const { showModelPicker } = require('./picker');
|
|
31
|
-
|
|
32
|
-
const CWD = process.cwd();
|
|
33
|
-
|
|
34
|
-
// ─── Abort Controller (for Ctrl+C cancellation) ─────────────
|
|
35
|
-
let _abortController = null;
|
|
36
|
-
|
|
37
|
-
function getAbortSignal() {
|
|
38
|
-
return _abortController?.signal ?? null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ─── Slash Command Registry ──────────────────────────────────
|
|
42
|
-
const SLASH_COMMANDS = [
|
|
43
|
-
{ cmd: '/help', desc: 'Show full help' },
|
|
44
|
-
{ cmd: '/model', desc: 'Show/switch model' },
|
|
45
|
-
{ cmd: '/providers', desc: 'List providers and models' },
|
|
46
|
-
{ cmd: '/fallback', desc: 'Show/set fallback chain' },
|
|
47
|
-
{ cmd: '/tokens', desc: 'Token usage and context budget' },
|
|
48
|
-
{ cmd: '/costs', desc: 'Session token costs' },
|
|
49
|
-
{ cmd: '/budget', desc: 'Show/set cost limits per provider' },
|
|
50
|
-
{ cmd: '/clear', desc: 'Clear conversation' },
|
|
51
|
-
{ cmd: '/context', desc: 'Show project context' },
|
|
52
|
-
{ cmd: '/autoconfirm', desc: 'Toggle auto-confirm' },
|
|
53
|
-
{ cmd: '/save', desc: 'Save session' },
|
|
54
|
-
{ cmd: '/load', desc: 'Load a saved session' },
|
|
55
|
-
{ cmd: '/sessions', desc: 'List saved sessions' },
|
|
56
|
-
{ cmd: '/resume', desc: 'Resume last session' },
|
|
57
|
-
{ cmd: '/remember', desc: 'Save a memory' },
|
|
58
|
-
{ cmd: '/forget', desc: 'Delete a memory' },
|
|
59
|
-
{ cmd: '/memory', desc: 'Show all memories' },
|
|
60
|
-
{ cmd: '/permissions', desc: 'Show tool permissions' },
|
|
61
|
-
{ cmd: '/allow', desc: 'Auto-allow a tool' },
|
|
62
|
-
{ cmd: '/deny', desc: 'Block a tool' },
|
|
63
|
-
{ cmd: '/plan', desc: 'Plan mode (analyze before executing)' },
|
|
64
|
-
{ cmd: '/plans', desc: 'List saved plans' },
|
|
65
|
-
{ cmd: '/auto', desc: 'Set autonomy level' },
|
|
66
|
-
{ cmd: '/commit', desc: 'Smart commit (diff + message)' },
|
|
67
|
-
{ cmd: '/diff', desc: 'Show current diff' },
|
|
68
|
-
{ cmd: '/branch', desc: 'Create feature branch' },
|
|
69
|
-
{ cmd: '/mcp', desc: 'MCP servers and tools' },
|
|
70
|
-
{ cmd: '/hooks', desc: 'Show configured hooks' },
|
|
71
|
-
{ cmd: '/skills', desc: 'List, enable, disable skills' },
|
|
72
|
-
{ cmd: '/tasks', desc: 'Show task list' },
|
|
73
|
-
{ cmd: '/undo', desc: 'Undo last file change' },
|
|
74
|
-
{ cmd: '/redo', desc: 'Redo last undone change' },
|
|
75
|
-
{ cmd: '/history', desc: 'Show file change history' },
|
|
76
|
-
{ cmd: '/exit', desc: 'Quit' },
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
function showCommandList() {
|
|
80
|
-
const skillCmds = getSkillCommands();
|
|
81
|
-
const allCmds = [...SLASH_COMMANDS, ...skillCmds];
|
|
82
|
-
const maxLen = Math.max(...allCmds.map((c) => c.cmd.length));
|
|
83
|
-
console.log('');
|
|
84
|
-
for (const { cmd, desc } of SLASH_COMMANDS) {
|
|
85
|
-
console.log(` ${C.cyan}${cmd.padEnd(maxLen + 2)}${C.reset}${C.dim}${desc}${C.reset}`);
|
|
86
|
-
}
|
|
87
|
-
for (const { cmd, desc } of skillCmds) {
|
|
88
|
-
console.log(` ${C.cyan}${cmd.padEnd(maxLen + 2)}${C.reset}${C.dim}${desc} ${C.yellow}[skill]${C.reset}`);
|
|
89
|
-
}
|
|
90
|
-
console.log(`\n${C.dim}Type /help for detailed usage${C.reset}\n`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function completeFilePath(partial) {
|
|
94
|
-
try {
|
|
95
|
-
let dir, prefix;
|
|
96
|
-
if (partial.endsWith('/') || partial.endsWith(path.sep)) {
|
|
97
|
-
dir = partial;
|
|
98
|
-
prefix = '';
|
|
99
|
-
} else {
|
|
100
|
-
dir = path.dirname(partial);
|
|
101
|
-
prefix = path.basename(partial);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Resolve ~ to home directory
|
|
105
|
-
if (dir.startsWith('~')) {
|
|
106
|
-
dir = path.join(require('os').homedir(), dir.slice(1));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const resolved = path.isAbsolute(dir) ? dir : path.resolve(CWD, dir);
|
|
110
|
-
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) return [];
|
|
111
|
-
|
|
112
|
-
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
113
|
-
const matches = [];
|
|
114
|
-
for (const entry of entries) {
|
|
115
|
-
// Skip hidden files and node_modules
|
|
116
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
117
|
-
if (prefix && !entry.name.startsWith(prefix)) continue;
|
|
118
|
-
|
|
119
|
-
const basePath = partial.endsWith('/') || partial.endsWith(path.sep)
|
|
120
|
-
? partial
|
|
121
|
-
: path.dirname(partial) + '/';
|
|
122
|
-
const completedPath = (basePath === './' && !partial.startsWith('./'))
|
|
123
|
-
? entry.name
|
|
124
|
-
: basePath + entry.name;
|
|
125
|
-
matches.push(entry.isDirectory() ? completedPath + '/' : completedPath);
|
|
126
|
-
}
|
|
127
|
-
return matches;
|
|
128
|
-
} catch {
|
|
129
|
-
return [];
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function completer(line) {
|
|
134
|
-
// Slash commands
|
|
135
|
-
if (line.startsWith('/')) {
|
|
136
|
-
const allCmds = [...SLASH_COMMANDS, ...getSkillCommands()];
|
|
137
|
-
const hits = allCmds.map((c) => c.cmd).filter((c) => c.startsWith(line));
|
|
138
|
-
return [hits.length ? hits : allCmds.map((c) => c.cmd), line];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// File path completion: check last token
|
|
142
|
-
const tokens = line.split(/\s+/);
|
|
143
|
-
const lastToken = tokens[tokens.length - 1] || '';
|
|
144
|
-
if (lastToken && (lastToken.includes('/') || lastToken.startsWith('./') || lastToken.startsWith('../') || lastToken.startsWith('~'))) {
|
|
145
|
-
const matches = completeFilePath(lastToken);
|
|
146
|
-
return [matches, lastToken];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return [[], line];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function showHelp() {
|
|
153
|
-
console.log(`
|
|
154
|
-
${C.bold}${C.cyan}Commands:${C.reset}
|
|
155
|
-
${C.cyan}/help${C.reset} ${C.dim}Show this help${C.reset}
|
|
156
|
-
${C.cyan}/model [spec]${C.reset} ${C.dim}Show/switch model (e.g. openai:gpt-4o, claude-sonnet)${C.reset}
|
|
157
|
-
${C.cyan}/providers${C.reset} ${C.dim}Show available providers and models${C.reset}
|
|
158
|
-
${C.cyan}/fallback [chain]${C.reset} ${C.dim}Show/set fallback chain (e.g. anthropic,openai,local)${C.reset}
|
|
159
|
-
${C.cyan}/tokens${C.reset} ${C.dim}Show token usage and context budget${C.reset}
|
|
160
|
-
${C.cyan}/costs${C.reset} ${C.dim}Show session token costs${C.reset}
|
|
161
|
-
${C.cyan}/budget [prov] [n]${C.reset}${C.dim}Show/set cost limits per provider${C.reset}
|
|
162
|
-
${C.cyan}/clear${C.reset} ${C.dim}Clear conversation context${C.reset}
|
|
163
|
-
${C.cyan}/context${C.reset} ${C.dim}Show project context${C.reset}
|
|
164
|
-
${C.cyan}/autoconfirm${C.reset} ${C.dim}Toggle auto-confirm for file changes${C.reset}
|
|
165
|
-
|
|
166
|
-
${C.bold}${C.cyan}Sessions:${C.reset}
|
|
167
|
-
${C.cyan}/save [name]${C.reset} ${C.dim}Save current session${C.reset}
|
|
168
|
-
${C.cyan}/load <name>${C.reset} ${C.dim}Load a saved session${C.reset}
|
|
169
|
-
${C.cyan}/sessions${C.reset} ${C.dim}List all saved sessions${C.reset}
|
|
170
|
-
${C.cyan}/resume${C.reset} ${C.dim}Resume last session${C.reset}
|
|
171
|
-
|
|
172
|
-
${C.bold}${C.cyan}Memory:${C.reset}
|
|
173
|
-
${C.cyan}/remember <text>${C.reset} ${C.dim}Save a memory (key=value or freeform)${C.reset}
|
|
174
|
-
${C.cyan}/forget <key>${C.reset} ${C.dim}Delete a memory${C.reset}
|
|
175
|
-
${C.cyan}/memory${C.reset} ${C.dim}Show all memories${C.reset}
|
|
176
|
-
|
|
177
|
-
${C.bold}${C.cyan}Permissions:${C.reset}
|
|
178
|
-
${C.cyan}/permissions${C.reset} ${C.dim}Show tool permissions${C.reset}
|
|
179
|
-
${C.cyan}/allow <tool>${C.reset} ${C.dim}Auto-allow a tool${C.reset}
|
|
180
|
-
${C.cyan}/deny <tool>${C.reset} ${C.dim}Block a tool${C.reset}
|
|
181
|
-
|
|
182
|
-
${C.bold}${C.cyan}Planning:${C.reset}
|
|
183
|
-
${C.cyan}/plan [task]${C.reset} ${C.dim}Enter plan mode (analyze, don't execute)${C.reset}
|
|
184
|
-
${C.cyan}/plan status${C.reset} ${C.dim}Show current plan progress${C.reset}
|
|
185
|
-
${C.cyan}/plan approve${C.reset} ${C.dim}Approve current plan${C.reset}
|
|
186
|
-
${C.cyan}/plans${C.reset} ${C.dim}List saved plans${C.reset}
|
|
187
|
-
${C.cyan}/auto [level]${C.reset} ${C.dim}Set autonomy: interactive/semi-auto/autonomous${C.reset}
|
|
188
|
-
|
|
189
|
-
${C.bold}${C.cyan}Git:${C.reset}
|
|
190
|
-
${C.cyan}/commit [msg]${C.reset} ${C.dim}Smart commit (analyze diff, suggest message)${C.reset}
|
|
191
|
-
${C.cyan}/diff${C.reset} ${C.dim}Show current diff summary${C.reset}
|
|
192
|
-
${C.cyan}/branch [name]${C.reset} ${C.dim}Create feature branch${C.reset}
|
|
193
|
-
|
|
194
|
-
${C.bold}${C.cyan}Extensibility:${C.reset}
|
|
195
|
-
${C.cyan}/mcp${C.reset} ${C.dim}Show MCP servers and tools${C.reset}
|
|
196
|
-
${C.cyan}/mcp connect${C.reset} ${C.dim}Connect all configured MCP servers${C.reset}
|
|
197
|
-
${C.cyan}/hooks${C.reset} ${C.dim}Show configured hooks${C.reset}
|
|
198
|
-
${C.cyan}/skills${C.reset} ${C.dim}List loaded skills${C.reset}
|
|
199
|
-
${C.cyan}/skills enable${C.reset} ${C.dim}Enable a skill by name${C.reset}
|
|
200
|
-
${C.cyan}/skills disable${C.reset} ${C.dim}Disable a skill by name${C.reset}
|
|
201
|
-
|
|
202
|
-
${C.bold}${C.cyan}Tasks:${C.reset}
|
|
203
|
-
${C.cyan}/tasks${C.reset} ${C.dim}Show current task list${C.reset}
|
|
204
|
-
${C.cyan}/tasks clear${C.reset} ${C.dim}Clear all tasks${C.reset}
|
|
205
|
-
|
|
206
|
-
${C.bold}${C.cyan}Undo / Redo:${C.reset}
|
|
207
|
-
${C.cyan}/undo${C.reset} ${C.dim}Undo last file change${C.reset}
|
|
208
|
-
${C.cyan}/redo${C.reset} ${C.dim}Redo last undone change${C.reset}
|
|
209
|
-
${C.cyan}/history${C.reset} ${C.dim}Show file change history${C.reset}
|
|
210
|
-
|
|
211
|
-
${C.cyan}/exit${C.reset} ${C.dim}Quit${C.reset}
|
|
212
|
-
`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function renderBar(percentage) {
|
|
216
|
-
const width = 30;
|
|
217
|
-
const filled = Math.round((percentage / 100) * width);
|
|
218
|
-
const empty = width - filled;
|
|
219
|
-
const color = percentage > 80 ? C.red : percentage > 50 ? C.yellow : C.green;
|
|
220
|
-
return ` ${color}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset} ${percentage}%`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function showProviders() {
|
|
224
|
-
const providerList = listProviders();
|
|
225
|
-
const activeProvider = getActiveProviderName();
|
|
226
|
-
const activeModel = getActiveModel();
|
|
227
|
-
|
|
228
|
-
console.log(`\n${C.bold}${C.cyan}Providers:${C.reset}`);
|
|
229
|
-
for (const p of providerList) {
|
|
230
|
-
const isActive = p.provider === activeProvider;
|
|
231
|
-
const status = p.configured ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
|
|
232
|
-
const marker = isActive ? ` ${C.cyan}(active)${C.reset}` : '';
|
|
233
|
-
console.log(` ${status} ${C.bold}${p.provider}${C.reset}${marker}`);
|
|
234
|
-
|
|
235
|
-
for (const m of p.models) {
|
|
236
|
-
const modelMarker = m.id === activeModel.id && isActive ? ` ${C.yellow}◄${C.reset}` : '';
|
|
237
|
-
console.log(` ${C.dim}${m.id}${C.reset} — ${m.name}${modelMarker}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
console.log();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function handleSlashCommand(input, rl) {
|
|
244
|
-
const [cmd, ...rest] = input.split(/\s+/);
|
|
245
|
-
|
|
246
|
-
switch (cmd) {
|
|
247
|
-
case '/help':
|
|
248
|
-
showHelp();
|
|
249
|
-
return true;
|
|
250
|
-
|
|
251
|
-
case '/model': {
|
|
252
|
-
const name = rest.join(' ').trim();
|
|
253
|
-
if (!name) {
|
|
254
|
-
if (rl) {
|
|
255
|
-
await showModelPicker(rl);
|
|
256
|
-
} else {
|
|
257
|
-
const model = getActiveModel();
|
|
258
|
-
const providerName = getActiveProviderName();
|
|
259
|
-
console.log(
|
|
260
|
-
`${C.bold}${C.cyan}Active model:${C.reset} ${C.dim}${providerName}:${model.id} (${model.name})${C.reset}`
|
|
261
|
-
);
|
|
262
|
-
console.log(`${C.gray}Use /model <provider:model> to switch. /providers to see all.${C.reset}`);
|
|
263
|
-
}
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
if (name === 'list') {
|
|
267
|
-
showProviders();
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
270
|
-
if (setActiveModel(name)) {
|
|
271
|
-
const model = getActiveModel();
|
|
272
|
-
const providerName = getActiveProviderName();
|
|
273
|
-
console.log(`${C.green}Switched to ${providerName}:${model.id} (${model.name})${C.reset}`);
|
|
274
|
-
} else {
|
|
275
|
-
console.log(`${C.red}Unknown model: ${name}${C.reset}`);
|
|
276
|
-
console.log(`${C.gray}Use /providers to see available models${C.reset}`);
|
|
277
|
-
}
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
case '/providers':
|
|
282
|
-
showProviders();
|
|
283
|
-
return true;
|
|
284
|
-
|
|
285
|
-
case '/fallback': {
|
|
286
|
-
const chainArg = rest.join(' ').trim();
|
|
287
|
-
if (!chainArg) {
|
|
288
|
-
const chain = getFallbackChain();
|
|
289
|
-
if (chain.length === 0) {
|
|
290
|
-
console.log(`${C.dim}No fallback chain configured${C.reset}`);
|
|
291
|
-
console.log(`${C.dim}Use /fallback anthropic,openai,local to set${C.reset}`);
|
|
292
|
-
} else {
|
|
293
|
-
console.log(`${C.bold}${C.cyan}Fallback chain:${C.reset} ${chain.join(' → ')}`);
|
|
294
|
-
}
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
const chain = chainArg.split(',').map((s) => s.trim()).filter(Boolean);
|
|
298
|
-
setFallbackChain(chain);
|
|
299
|
-
console.log(`${C.green}Fallback chain: ${chain.join(' → ')}${C.reset}`);
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
case '/tokens': {
|
|
304
|
-
const messages = getConversationMessages();
|
|
305
|
-
const usage = getUsage(messages, TOOL_DEFINITIONS);
|
|
306
|
-
const model = getActiveModel();
|
|
307
|
-
const providerName = getActiveProviderName();
|
|
308
|
-
|
|
309
|
-
console.log(`\n${C.bold}${C.cyan}Token Usage:${C.reset}`);
|
|
310
|
-
console.log(` ${C.dim}Model:${C.reset} ${providerName}:${model.id} (${(usage.limit / 1000).toFixed(0)}k context)`);
|
|
311
|
-
console.log(` ${C.dim}Used:${C.reset} ${usage.used.toLocaleString()} / ${usage.limit.toLocaleString()} (${usage.percentage}%)`);
|
|
312
|
-
|
|
313
|
-
const bar = renderBar(usage.percentage);
|
|
314
|
-
console.log(` ${bar}`);
|
|
315
|
-
|
|
316
|
-
console.log(`\n ${C.dim}Breakdown:${C.reset}`);
|
|
317
|
-
console.log(` System prompt: ${usage.breakdown.system.toLocaleString()} tokens`);
|
|
318
|
-
console.log(` Conversation: ${usage.breakdown.conversation.toLocaleString()} tokens`);
|
|
319
|
-
console.log(` Tool results: ${usage.breakdown.toolResults.toLocaleString()} tokens`);
|
|
320
|
-
console.log(` Tool definitions: ${usage.breakdown.toolDefinitions.toLocaleString()} tokens`);
|
|
321
|
-
console.log(` Messages: ${usage.messageCount}`);
|
|
322
|
-
console.log();
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
case '/costs': {
|
|
327
|
-
const costArg = rest.join(' ').trim();
|
|
328
|
-
if (costArg === 'reset') {
|
|
329
|
-
resetCosts();
|
|
330
|
-
console.log(`${C.green}Cost tracking reset${C.reset}`);
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
console.log(`\n${formatCosts()}\n`);
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
case '/budget': {
|
|
338
|
-
const budgetArg = rest[0];
|
|
339
|
-
if (!budgetArg) {
|
|
340
|
-
// Show all limits + current spend
|
|
341
|
-
const limits = getCostLimits();
|
|
342
|
-
const provList = listProviders();
|
|
343
|
-
console.log(`\n${C.bold}${C.cyan}Cost Limits:${C.reset}`);
|
|
344
|
-
let hasAny = false;
|
|
345
|
-
for (const p of provList) {
|
|
346
|
-
const spent = getProviderSpend(p.provider);
|
|
347
|
-
const limit = limits[p.provider];
|
|
348
|
-
if (limit !== undefined) {
|
|
349
|
-
hasAny = true;
|
|
350
|
-
const pct = Math.min(100, Math.round((spent / limit) * 100));
|
|
351
|
-
const barWidth = 10;
|
|
352
|
-
const filled = Math.round((pct / 100) * barWidth);
|
|
353
|
-
const empty = barWidth - filled;
|
|
354
|
-
const barColor = pct >= 100 ? C.red : pct >= 80 ? C.yellow : C.green;
|
|
355
|
-
const bar = `${barColor}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset}`;
|
|
356
|
-
console.log(` ${C.bold}${p.provider}:${C.reset} $${spent.toFixed(2)} / $${limit.toFixed(2)} (${pct}%) ${bar}`);
|
|
357
|
-
} else {
|
|
358
|
-
const isFree = p.provider === 'ollama' || p.provider === 'local';
|
|
359
|
-
if (isFree) {
|
|
360
|
-
console.log(` ${C.bold}${p.provider}:${C.reset} ${C.dim}free (no limit)${C.reset}`);
|
|
361
|
-
} else if (spent > 0) {
|
|
362
|
-
console.log(` ${C.bold}${p.provider}:${C.reset} $${spent.toFixed(2)} ${C.dim}(no limit)${C.reset}`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (!hasAny) {
|
|
367
|
-
console.log(` ${C.dim}No limits set. Use /budget <provider> <amount> to set one.${C.reset}`);
|
|
368
|
-
}
|
|
369
|
-
console.log();
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
const budgetVal = rest[1];
|
|
373
|
-
if (!budgetVal) {
|
|
374
|
-
// Show single provider budget
|
|
375
|
-
const budget = checkBudget(budgetArg);
|
|
376
|
-
if (budget.limit !== null) {
|
|
377
|
-
console.log(`${C.bold}${budgetArg}:${C.reset} $${budget.spent.toFixed(2)} / $${budget.limit.toFixed(2)} ($${budget.remaining.toFixed(2)} remaining)`);
|
|
378
|
-
} else {
|
|
379
|
-
console.log(`${C.bold}${budgetArg}:${C.reset} $${budget.spent.toFixed(2)} ${C.dim}(no limit)${C.reset}`);
|
|
380
|
-
}
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
if (budgetVal === 'off' || budgetVal === 'remove' || budgetVal === 'clear') {
|
|
384
|
-
removeCostLimit(budgetArg);
|
|
385
|
-
saveCostLimits();
|
|
386
|
-
console.log(`${C.green}Removed cost limit for ${budgetArg}${C.reset}`);
|
|
387
|
-
return true;
|
|
388
|
-
}
|
|
389
|
-
const amount = parseFloat(budgetVal);
|
|
390
|
-
if (isNaN(amount) || amount <= 0) {
|
|
391
|
-
console.log(`${C.red}Invalid amount: ${budgetVal}. Use a positive number or 'off'.${C.reset}`);
|
|
392
|
-
return true;
|
|
393
|
-
}
|
|
394
|
-
setCostLimit(budgetArg, amount);
|
|
395
|
-
saveCostLimits();
|
|
396
|
-
console.log(`${C.green}Set ${budgetArg} budget limit: $${amount.toFixed(2)}${C.reset}`);
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
case '/clear':
|
|
401
|
-
clearConversation();
|
|
402
|
-
clearHistory();
|
|
403
|
-
console.log(`${C.green}Conversation cleared${C.reset}`);
|
|
404
|
-
return true;
|
|
405
|
-
|
|
406
|
-
case '/context':
|
|
407
|
-
printContext(CWD);
|
|
408
|
-
return true;
|
|
409
|
-
|
|
410
|
-
case '/autoconfirm': {
|
|
411
|
-
const newVal = !getAutoConfirm();
|
|
412
|
-
setAutoConfirm(newVal);
|
|
413
|
-
console.log(`${C.green}Auto-confirm: ${newVal ? 'ON' : 'OFF'}${C.reset}`);
|
|
414
|
-
if (newVal) {
|
|
415
|
-
console.log(`${C.yellow} ⚠ File changes will be applied without confirmation${C.reset}`);
|
|
416
|
-
}
|
|
417
|
-
return true;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
case '/save': {
|
|
421
|
-
const sessionName = rest.join(' ').trim() || `session-${Date.now()}`;
|
|
422
|
-
const messages = getConversationMessages();
|
|
423
|
-
if (messages.length === 0) {
|
|
424
|
-
console.log(`${C.yellow}No conversation to save${C.reset}`);
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
const model = getActiveModel();
|
|
428
|
-
const providerName = getActiveProviderName();
|
|
429
|
-
saveSession(sessionName, messages, { model: model.id, provider: providerName });
|
|
430
|
-
console.log(`${C.green}Session saved: ${sessionName} (${messages.length} messages)${C.reset}`);
|
|
431
|
-
return true;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
case '/load': {
|
|
435
|
-
const loadName = rest.join(' ').trim();
|
|
436
|
-
if (!loadName) {
|
|
437
|
-
console.log(`${C.red}Usage: /load <name>${C.reset}`);
|
|
438
|
-
return true;
|
|
439
|
-
}
|
|
440
|
-
const session = loadSession(loadName);
|
|
441
|
-
if (!session) {
|
|
442
|
-
console.log(`${C.red}Session not found: ${loadName}${C.reset}`);
|
|
443
|
-
return true;
|
|
444
|
-
}
|
|
445
|
-
setConversationMessages(session.messages);
|
|
446
|
-
console.log(`${C.green}Loaded session: ${session.name} (${session.messageCount} messages)${C.reset}`);
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
case '/sessions': {
|
|
451
|
-
const sessions = listSessions();
|
|
452
|
-
if (sessions.length === 0) {
|
|
453
|
-
console.log(`${C.dim}No saved sessions${C.reset}`);
|
|
454
|
-
return true;
|
|
455
|
-
}
|
|
456
|
-
console.log(`\n${C.bold}${C.cyan}Sessions:${C.reset}`);
|
|
457
|
-
for (const s of sessions) {
|
|
458
|
-
const date = s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '?';
|
|
459
|
-
const auto = s.name === '_autosave' ? ` ${C.dim}(auto)${C.reset}` : '';
|
|
460
|
-
console.log(` ${C.cyan}${s.name}${C.reset}${auto} — ${s.messageCount} msgs, ${date}`);
|
|
461
|
-
}
|
|
462
|
-
console.log();
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
case '/resume': {
|
|
467
|
-
const last = getLastSession();
|
|
468
|
-
if (!last) {
|
|
469
|
-
console.log(`${C.yellow}No session to resume${C.reset}`);
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
setConversationMessages(last.messages);
|
|
473
|
-
console.log(`${C.green}Resumed: ${last.name} (${last.messageCount} messages)${C.reset}`);
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
case '/remember': {
|
|
478
|
-
const text = rest.join(' ').trim();
|
|
479
|
-
if (!text) {
|
|
480
|
-
console.log(`${C.red}Usage: /remember <key>=<value> or /remember <text>${C.reset}`);
|
|
481
|
-
return true;
|
|
482
|
-
}
|
|
483
|
-
const eqIdx = text.indexOf('=');
|
|
484
|
-
let key, value;
|
|
485
|
-
if (eqIdx > 0) {
|
|
486
|
-
key = text.substring(0, eqIdx).trim();
|
|
487
|
-
value = text.substring(eqIdx + 1).trim();
|
|
488
|
-
} else {
|
|
489
|
-
key = text.substring(0, 40).replace(/\s+/g, '-');
|
|
490
|
-
value = text;
|
|
491
|
-
}
|
|
492
|
-
remember(key, value);
|
|
493
|
-
console.log(`${C.green}Remembered: ${key}${C.reset}`);
|
|
494
|
-
return true;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
case '/forget': {
|
|
498
|
-
const forgetKey = rest.join(' ').trim();
|
|
499
|
-
if (!forgetKey) {
|
|
500
|
-
console.log(`${C.red}Usage: /forget <key>${C.reset}`);
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
if (forget(forgetKey)) {
|
|
504
|
-
console.log(`${C.green}Forgotten: ${forgetKey}${C.reset}`);
|
|
505
|
-
} else {
|
|
506
|
-
console.log(`${C.red}Memory not found: ${forgetKey}${C.reset}`);
|
|
507
|
-
}
|
|
508
|
-
return true;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
case '/memory': {
|
|
512
|
-
const memories = listMemories();
|
|
513
|
-
if (memories.length === 0) {
|
|
514
|
-
console.log(`${C.dim}No memories saved${C.reset}`);
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
console.log(`\n${C.bold}${C.cyan}Memory:${C.reset}`);
|
|
518
|
-
for (const m of memories) {
|
|
519
|
-
console.log(` ${C.cyan}${m.key}${C.reset} = ${m.value}`);
|
|
520
|
-
}
|
|
521
|
-
console.log();
|
|
522
|
-
return true;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
case '/plan': {
|
|
526
|
-
const arg = rest.join(' ').trim();
|
|
527
|
-
if (arg === 'status') {
|
|
528
|
-
const plan = getActivePlan();
|
|
529
|
-
console.log(formatPlan(plan));
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
if (arg === 'approve') {
|
|
533
|
-
if (approvePlan()) {
|
|
534
|
-
console.log(`${C.green}Plan approved! Starting execution...${C.reset}`);
|
|
535
|
-
startExecution();
|
|
536
|
-
setPlanMode(false);
|
|
537
|
-
} else {
|
|
538
|
-
console.log(`${C.red}No plan to approve${C.reset}`);
|
|
539
|
-
}
|
|
540
|
-
return true;
|
|
541
|
-
}
|
|
542
|
-
// Enter plan mode
|
|
543
|
-
setPlanMode(true);
|
|
544
|
-
console.log(`${C.cyan}${C.bold}Plan mode activated${C.reset}`);
|
|
545
|
-
console.log(`${C.dim}Analysis only — no file changes until approved${C.reset}`);
|
|
546
|
-
if (arg) {
|
|
547
|
-
console.log(`${C.dim}Task: ${arg}${C.reset}`);
|
|
548
|
-
}
|
|
549
|
-
return true;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
case '/plans': {
|
|
553
|
-
const plans = listPlans();
|
|
554
|
-
if (plans.length === 0) {
|
|
555
|
-
console.log(`${C.dim}No saved plans${C.reset}`);
|
|
556
|
-
return true;
|
|
557
|
-
}
|
|
558
|
-
console.log(`\n${C.bold}${C.cyan}Plans:${C.reset}`);
|
|
559
|
-
for (const p of plans) {
|
|
560
|
-
const statusIcon = p.status === 'completed' ? `${C.green}✓` : p.status === 'executing' ? `${C.blue}→` : `${C.dim}○`;
|
|
561
|
-
console.log(` ${statusIcon} ${C.reset}${C.bold}${p.name}${C.reset} — ${p.task || '?'} (${p.steps} steps, ${p.status})`);
|
|
562
|
-
}
|
|
563
|
-
console.log();
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
case '/auto': {
|
|
568
|
-
const level = rest.join(' ').trim();
|
|
569
|
-
if (!level) {
|
|
570
|
-
console.log(`${C.bold}${C.cyan}Autonomy:${C.reset} ${getAutonomyLevel()}`);
|
|
571
|
-
console.log(`${C.dim}Levels: ${AUTONOMY_LEVELS.join(', ')}${C.reset}`);
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
if (setAutonomyLevel(level)) {
|
|
575
|
-
console.log(`${C.green}Autonomy: ${level}${C.reset}`);
|
|
576
|
-
} else {
|
|
577
|
-
console.log(`${C.red}Unknown level: ${level}. Use: ${AUTONOMY_LEVELS.join(', ')}${C.reset}`);
|
|
578
|
-
}
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
case '/permissions': {
|
|
583
|
-
const perms = listPermissions();
|
|
584
|
-
console.log(`\n${C.bold}${C.cyan}Tool Permissions:${C.reset}`);
|
|
585
|
-
for (const p of perms) {
|
|
586
|
-
const icon = p.mode === 'allow' ? `${C.green}✓` : p.mode === 'deny' ? `${C.red}✗` : `${C.yellow}?`;
|
|
587
|
-
console.log(` ${icon} ${C.reset}${C.bold}${p.tool}${C.reset} ${C.dim}(${p.mode})${C.reset}`);
|
|
588
|
-
}
|
|
589
|
-
console.log(`\n${C.dim}Use /allow <tool> or /deny <tool> to change${C.reset}\n`);
|
|
590
|
-
return true;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
case '/allow': {
|
|
594
|
-
const tool = rest.join(' ').trim();
|
|
595
|
-
if (!tool) {
|
|
596
|
-
console.log(`${C.red}Usage: /allow <tool>${C.reset}`);
|
|
597
|
-
return true;
|
|
598
|
-
}
|
|
599
|
-
setPermission(tool, 'allow');
|
|
600
|
-
savePermissions();
|
|
601
|
-
console.log(`${C.green}${tool}: allow${C.reset}`);
|
|
602
|
-
return true;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
case '/deny': {
|
|
606
|
-
const tool = rest.join(' ').trim();
|
|
607
|
-
if (!tool) {
|
|
608
|
-
console.log(`${C.red}Usage: /deny <tool>${C.reset}`);
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
|
-
setPermission(tool, 'deny');
|
|
612
|
-
savePermissions();
|
|
613
|
-
console.log(`${C.red}${tool}: deny${C.reset}`);
|
|
614
|
-
return true;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
case '/commit': {
|
|
618
|
-
if (!isGitRepo()) {
|
|
619
|
-
console.log(`${C.red}Not a git repository${C.reset}`);
|
|
620
|
-
return true;
|
|
621
|
-
}
|
|
622
|
-
const msg = rest.join(' ').trim();
|
|
623
|
-
if (msg) {
|
|
624
|
-
const hash = commit(msg);
|
|
625
|
-
if (hash) {
|
|
626
|
-
console.log(`${C.green}Committed: ${hash} — ${msg}${C.reset}`);
|
|
627
|
-
} else {
|
|
628
|
-
console.log(`${C.red}Commit failed${C.reset}`);
|
|
629
|
-
}
|
|
630
|
-
return true;
|
|
631
|
-
}
|
|
632
|
-
// Smart commit: analyze and suggest
|
|
633
|
-
const analysis = analyzeDiff();
|
|
634
|
-
if (!analysis) {
|
|
635
|
-
console.log(`${C.yellow}No changes to commit${C.reset}`);
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
|
-
console.log(formatDiffSummary());
|
|
639
|
-
console.log(`${C.dim}Use /commit <message> to commit with a custom message${C.reset}`);
|
|
640
|
-
return true;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
case '/diff': {
|
|
644
|
-
if (!isGitRepo()) {
|
|
645
|
-
console.log(`${C.red}Not a git repository${C.reset}`);
|
|
646
|
-
return true;
|
|
647
|
-
}
|
|
648
|
-
console.log(formatDiffSummary());
|
|
649
|
-
return true;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
case '/branch': {
|
|
653
|
-
if (!isGitRepo()) {
|
|
654
|
-
console.log(`${C.red}Not a git repository${C.reset}`);
|
|
655
|
-
return true;
|
|
656
|
-
}
|
|
657
|
-
const branchArg = rest.join(' ').trim();
|
|
658
|
-
if (!branchArg) {
|
|
659
|
-
const current = getCurrentBranch();
|
|
660
|
-
console.log(`${C.bold}${C.cyan}Branch:${C.reset} ${current || '(detached)'}`);
|
|
661
|
-
return true;
|
|
662
|
-
}
|
|
663
|
-
const branchName = createBranch(branchArg);
|
|
664
|
-
if (branchName) {
|
|
665
|
-
console.log(`${C.green}Created and switched to: ${branchName}${C.reset}`);
|
|
666
|
-
} else {
|
|
667
|
-
console.log(`${C.red}Failed to create branch${C.reset}`);
|
|
668
|
-
}
|
|
669
|
-
return true;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
case '/mcp': {
|
|
673
|
-
const mcpArg = rest.join(' ').trim();
|
|
674
|
-
if (mcpArg === 'connect') {
|
|
675
|
-
console.log(`${C.dim}Connecting MCP servers...${C.reset}`);
|
|
676
|
-
connectAll().then((results) => {
|
|
677
|
-
for (const r of results) {
|
|
678
|
-
if (r.error) {
|
|
679
|
-
console.log(` ${C.red}✗${C.reset} ${r.name}: ${r.error}`);
|
|
680
|
-
} else {
|
|
681
|
-
console.log(` ${C.green}✓${C.reset} ${r.name}: ${r.tools} tools`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
if (results.length === 0) {
|
|
685
|
-
console.log(`${C.dim}No MCP servers configured in .nex/config.json${C.reset}`);
|
|
686
|
-
}
|
|
687
|
-
}).catch((err) => {
|
|
688
|
-
console.log(`${C.red}MCP connection error: ${err.message}${C.reset}`);
|
|
689
|
-
});
|
|
690
|
-
return true;
|
|
691
|
-
}
|
|
692
|
-
if (mcpArg === 'disconnect') {
|
|
693
|
-
disconnectAll();
|
|
694
|
-
console.log(`${C.green}All MCP servers disconnected${C.reset}`);
|
|
695
|
-
return true;
|
|
696
|
-
}
|
|
697
|
-
// Show status
|
|
698
|
-
const servers = listServers();
|
|
699
|
-
if (servers.length === 0) {
|
|
700
|
-
console.log(`${C.dim}No MCP servers configured${C.reset}`);
|
|
701
|
-
console.log(`${C.dim}Add servers to .nex/config.json under "mcpServers"${C.reset}`);
|
|
702
|
-
return true;
|
|
703
|
-
}
|
|
704
|
-
console.log(`\n${C.bold}${C.cyan}MCP Servers:${C.reset}`);
|
|
705
|
-
for (const s of servers) {
|
|
706
|
-
const status = s.connected ? `${C.green}✓ connected${C.reset}` : `${C.dim}○ disconnected${C.reset}`;
|
|
707
|
-
console.log(` ${status} ${C.bold}${s.name}${C.reset} (${s.command}) — ${s.toolCount} tools`);
|
|
708
|
-
}
|
|
709
|
-
console.log(`\n${C.dim}Use /mcp connect to connect all servers${C.reset}\n`);
|
|
710
|
-
return true;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
case '/hooks': {
|
|
714
|
-
const hookList = listHooks();
|
|
715
|
-
if (hookList.length === 0) {
|
|
716
|
-
console.log(`${C.dim}No hooks configured${C.reset}`);
|
|
717
|
-
console.log(`${C.dim}Add hooks to .nex/config.json or .nex/hooks/${C.reset}`);
|
|
718
|
-
return true;
|
|
719
|
-
}
|
|
720
|
-
console.log(`\n${C.bold}${C.cyan}Hooks:${C.reset}`);
|
|
721
|
-
for (const h of hookList) {
|
|
722
|
-
console.log(` ${C.cyan}${h.event}${C.reset}`);
|
|
723
|
-
for (const cmd of h.commands) {
|
|
724
|
-
console.log(` ${C.dim}→ ${cmd}${C.reset}`);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
console.log();
|
|
728
|
-
return true;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
case '/skills': {
|
|
732
|
-
const skillArg = rest.join(' ').trim();
|
|
733
|
-
if (skillArg.startsWith('enable ')) {
|
|
734
|
-
const name = skillArg.substring(7).trim();
|
|
735
|
-
if (enableSkill(name)) {
|
|
736
|
-
console.log(`${C.green}Skill enabled: ${name}${C.reset}`);
|
|
737
|
-
} else {
|
|
738
|
-
console.log(`${C.red}Skill not found: ${name}${C.reset}`);
|
|
739
|
-
}
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
742
|
-
if (skillArg.startsWith('disable ')) {
|
|
743
|
-
const name = skillArg.substring(8).trim();
|
|
744
|
-
if (disableSkill(name)) {
|
|
745
|
-
console.log(`${C.yellow}Skill disabled: ${name}${C.reset}`);
|
|
746
|
-
} else {
|
|
747
|
-
console.log(`${C.red}Skill not found: ${name}${C.reset}`);
|
|
748
|
-
}
|
|
749
|
-
return true;
|
|
750
|
-
}
|
|
751
|
-
const skills = listSkills();
|
|
752
|
-
if (skills.length === 0) {
|
|
753
|
-
console.log(`${C.dim}No skills loaded${C.reset}`);
|
|
754
|
-
console.log(`${C.dim}Add .md or .js files to .nex/skills/${C.reset}`);
|
|
755
|
-
return true;
|
|
756
|
-
}
|
|
757
|
-
console.log(`\n${C.bold}${C.cyan}Skills:${C.reset}`);
|
|
758
|
-
for (const s of skills) {
|
|
759
|
-
const status = s.enabled ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
|
|
760
|
-
const tag = s.type === 'prompt' ? `${C.dim}(prompt)${C.reset}` : `${C.dim}(script)${C.reset}`;
|
|
761
|
-
const extras = [];
|
|
762
|
-
if (s.commands > 0) extras.push(`${s.commands} cmd`);
|
|
763
|
-
if (s.tools > 0) extras.push(`${s.tools} tools`);
|
|
764
|
-
const info = extras.length > 0 ? ` — ${extras.join(', ')}` : '';
|
|
765
|
-
console.log(` ${status} ${C.bold}${s.name}${C.reset} ${tag}${info}`);
|
|
766
|
-
}
|
|
767
|
-
console.log(`\n${C.dim}Use /skills enable <name> or /skills disable <name>${C.reset}\n`);
|
|
768
|
-
return true;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
case '/tasks': {
|
|
772
|
-
const { renderTaskList, clearTasks } = require('./tasks');
|
|
773
|
-
const taskArg = rest.join(' ').trim();
|
|
774
|
-
if (taskArg === 'clear') {
|
|
775
|
-
clearTasks();
|
|
776
|
-
console.log(`${C.green}Tasks cleared${C.reset}`);
|
|
777
|
-
return true;
|
|
778
|
-
}
|
|
779
|
-
console.log('\n' + renderTaskList() + '\n');
|
|
780
|
-
return true;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
case '/undo': {
|
|
784
|
-
const undone = undo();
|
|
785
|
-
if (!undone) {
|
|
786
|
-
console.log(`${C.yellow}Nothing to undo${C.reset}`);
|
|
787
|
-
return true;
|
|
788
|
-
}
|
|
789
|
-
if (undone.wasCreated) {
|
|
790
|
-
console.log(`${C.green}Undone: deleted ${undone.filePath} (was created by ${undone.tool})${C.reset}`);
|
|
791
|
-
} else {
|
|
792
|
-
console.log(`${C.green}Undone: restored ${undone.filePath} (${undone.tool})${C.reset}`);
|
|
793
|
-
}
|
|
794
|
-
const remaining = getUndoCount();
|
|
795
|
-
if (remaining > 0) console.log(`${C.dim}${remaining} more change(s) to undo${C.reset}`);
|
|
796
|
-
return true;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
case '/redo': {
|
|
800
|
-
const redone = redo();
|
|
801
|
-
if (!redone) {
|
|
802
|
-
console.log(`${C.yellow}Nothing to redo${C.reset}`);
|
|
803
|
-
return true;
|
|
804
|
-
}
|
|
805
|
-
console.log(`${C.green}Redone: ${redone.filePath} (${redone.tool})${C.reset}`);
|
|
806
|
-
const redoRemaining = getRedoCount();
|
|
807
|
-
if (redoRemaining > 0) console.log(`${C.dim}${redoRemaining} more change(s) to redo${C.reset}`);
|
|
808
|
-
return true;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
case '/history': {
|
|
812
|
-
const history = getHistory(20);
|
|
813
|
-
if (history.length === 0) {
|
|
814
|
-
console.log(`${C.dim}No file changes in this session${C.reset}`);
|
|
815
|
-
return true;
|
|
816
|
-
}
|
|
817
|
-
console.log(`\n${C.bold}${C.cyan}File Change History:${C.reset}`);
|
|
818
|
-
for (const entry of history) {
|
|
819
|
-
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
820
|
-
console.log(` ${C.dim}${time}${C.reset} ${C.yellow}${entry.tool}${C.reset} ${entry.filePath}`);
|
|
821
|
-
}
|
|
822
|
-
console.log(`\n${C.dim}${getUndoCount()} undo / ${getRedoCount()} redo available${C.reset}\n`);
|
|
823
|
-
return true;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
case '/exit':
|
|
827
|
-
case '/quit':
|
|
828
|
-
console.log(`\n${C.gray}Bye!${C.reset}`);
|
|
829
|
-
process.exit(0);
|
|
830
|
-
|
|
831
|
-
default:
|
|
832
|
-
// Check if it's a skill command before reporting unknown
|
|
833
|
-
if (handleSkillCommand(input)) return true;
|
|
834
|
-
console.log(`${C.red}Unknown command: ${cmd}. Type /help${C.reset}`);
|
|
835
|
-
return true;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// ─── History Persistence ─────────────────────────────────────
|
|
840
|
-
const HISTORY_MAX = 1000;
|
|
841
|
-
|
|
842
|
-
function getHistoryPath() {
|
|
843
|
-
return path.join(process.cwd(), '.nex', 'repl_history');
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function loadHistory() {
|
|
847
|
-
try {
|
|
848
|
-
const histPath = getHistoryPath();
|
|
849
|
-
if (fs.existsSync(histPath)) {
|
|
850
|
-
const lines = fs.readFileSync(histPath, 'utf-8').split('\n').filter(Boolean);
|
|
851
|
-
return lines.slice(-HISTORY_MAX);
|
|
852
|
-
}
|
|
853
|
-
} catch { /* ignore */ }
|
|
854
|
-
return [];
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function appendHistory(line) {
|
|
858
|
-
try {
|
|
859
|
-
const histPath = getHistoryPath();
|
|
860
|
-
const dir = path.dirname(histPath);
|
|
861
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
862
|
-
fs.appendFileSync(histPath, line + '\n');
|
|
863
|
-
} catch { /* ignore */ }
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// ─── Smart Prompt ────────────────────────────────────────────
|
|
867
|
-
function getPrompt() {
|
|
868
|
-
const parts = [];
|
|
869
|
-
|
|
870
|
-
if (isPlanMode()) parts.push('plan');
|
|
871
|
-
|
|
872
|
-
const level = getAutonomyLevel();
|
|
873
|
-
if (level !== 'interactive') parts.push(level);
|
|
874
|
-
|
|
875
|
-
const providerName = getActiveProviderName();
|
|
876
|
-
const model = getActiveModel();
|
|
877
|
-
parts.push(`${providerName}:${model.id}`);
|
|
878
|
-
|
|
879
|
-
const tag = parts.length > 0 ? `${C.dim}[${parts.join(' · ')}]${C.reset} ` : '';
|
|
880
|
-
return `${tag}${C.bold}${C.cyan}>${C.reset} `;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// ─── Bracketed Paste Detection ──────────────────────────────
|
|
884
|
-
const PASTE_START = '\x1b[200~';
|
|
885
|
-
const PASTE_END = '\x1b[201~';
|
|
886
|
-
|
|
887
|
-
function hasPasteStart(data) {
|
|
888
|
-
return typeof data === 'string' && data.includes(PASTE_START);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function hasPasteEnd(data) {
|
|
892
|
-
return typeof data === 'string' && data.includes(PASTE_END);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function stripPasteSequences(data) {
|
|
896
|
-
if (typeof data !== 'string') return data;
|
|
897
|
-
return data.split(PASTE_START).join('').split(PASTE_END).join('');
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function startREPL() {
|
|
901
|
-
// Wire abort signal into agent.js (avoids circular dependency)
|
|
902
|
-
setAbortSignalGetter(getAbortSignal);
|
|
903
|
-
|
|
904
|
-
// Check that at least one provider is configured
|
|
905
|
-
const providerList = listProviders();
|
|
906
|
-
const hasConfigured = providerList.some((p) => p.configured);
|
|
907
|
-
|
|
908
|
-
if (!hasConfigured) {
|
|
909
|
-
// Check if local Ollama is running
|
|
910
|
-
const localProvider = getProvider('local');
|
|
911
|
-
let localDetected = false;
|
|
912
|
-
if (localProvider) {
|
|
913
|
-
try {
|
|
914
|
-
const { execSync } = require('child_process');
|
|
915
|
-
execSync('curl -s --max-time 2 http://localhost:11434/api/tags', { encoding: 'utf-8', stdio: 'pipe' });
|
|
916
|
-
setActiveModel('local:llama3');
|
|
917
|
-
console.log(`${C.green}✓ Local Ollama detected — using local models${C.reset}`);
|
|
918
|
-
console.log(`${C.dim}Tip: Set API keys for cloud providers for more model options (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)${C.reset}\n`);
|
|
919
|
-
localDetected = true;
|
|
920
|
-
} catch {
|
|
921
|
-
// Local Ollama not available
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
if (!localDetected) {
|
|
925
|
-
console.error(`\n${C.red}✗ No provider configured and no local Ollama detected.${C.reset}\n`);
|
|
926
|
-
console.error(`${C.white}nex-code needs at least one LLM provider to work.${C.reset}\n`);
|
|
927
|
-
console.error(`${C.white}Option 1 — Free local models (no API key needed):${C.reset}`);
|
|
928
|
-
console.error(`${C.gray} Install Ollama: ${C.cyan}https://ollama.com/download${C.reset}`);
|
|
929
|
-
console.error(`${C.gray} Pull a model: ${C.cyan}ollama pull qwen3-coder${C.reset}`);
|
|
930
|
-
console.error(`${C.gray} Then restart: ${C.cyan}nex-code${C.reset}\n`);
|
|
931
|
-
console.error(`${C.white}Option 2 — Cloud providers (set one in .env or as env var):${C.reset}`);
|
|
932
|
-
console.error(`${C.gray} OLLAMA_API_KEY=... ${C.dim}# Ollama Cloud (free tier available)${C.reset}`);
|
|
933
|
-
console.error(`${C.gray} OPENAI_API_KEY=... ${C.dim}# OpenAI${C.reset}`);
|
|
934
|
-
console.error(`${C.gray} ANTHROPIC_API_KEY=... ${C.dim}# Anthropic${C.reset}`);
|
|
935
|
-
console.error(`${C.gray} GEMINI_API_KEY=... ${C.dim}# Google Gemini${C.reset}\n`);
|
|
936
|
-
console.error(`${C.dim}Create a .env file in your project directory or export the variable.${C.reset}`);
|
|
937
|
-
process.exit(1);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
loadAllSkills();
|
|
942
|
-
|
|
943
|
-
const model = getActiveModel();
|
|
944
|
-
const providerName = getActiveProviderName();
|
|
945
|
-
banner(`${providerName}:${model.id}`, CWD, { yolo: getAutoConfirm() });
|
|
946
|
-
printContext(CWD);
|
|
947
|
-
|
|
948
|
-
const history = loadHistory();
|
|
949
|
-
|
|
950
|
-
const rl = readline.createInterface({
|
|
951
|
-
input: process.stdin,
|
|
952
|
-
output: process.stdout,
|
|
953
|
-
prompt: getPrompt(),
|
|
954
|
-
completer,
|
|
955
|
-
history,
|
|
956
|
-
historySize: HISTORY_MAX,
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
setReadlineInterface(rl);
|
|
960
|
-
|
|
961
|
-
// ─── SIGINT (Ctrl+C) Handler ────────────────────────────────
|
|
962
|
-
let _processing = false;
|
|
963
|
-
let _sigintCount = 0;
|
|
964
|
-
|
|
965
|
-
process.on('SIGINT', () => {
|
|
966
|
-
cleanupTerminal();
|
|
967
|
-
if (_processing) {
|
|
968
|
-
_sigintCount++;
|
|
969
|
-
if (_sigintCount >= 3) {
|
|
970
|
-
// Force exit after 3 rapid Ctrl+C during processing
|
|
971
|
-
if (process.stdin.isTTY) process.stdout.write('\x1b[?2004l');
|
|
972
|
-
console.log(`\n${C.gray}Bye!${C.reset}`);
|
|
973
|
-
process.exit(0);
|
|
974
|
-
}
|
|
975
|
-
// Abort the running HTTP stream
|
|
976
|
-
if (_abortController) {
|
|
977
|
-
_abortController.abort();
|
|
978
|
-
}
|
|
979
|
-
console.log(`\n${C.yellow} Cancelled${C.reset}`);
|
|
980
|
-
_processing = false;
|
|
981
|
-
rl.setPrompt(getPrompt());
|
|
982
|
-
rl.prompt();
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
// At prompt — clean exit
|
|
986
|
-
if (process.stdin.isTTY) process.stdout.write('\x1b[?2004l');
|
|
987
|
-
console.log(`\n${C.gray}Bye!${C.reset}`);
|
|
988
|
-
process.exit(0);
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
// ─── Bracketed Paste Mode ──────────────────────────────────
|
|
992
|
-
let _pasteActive = false;
|
|
993
|
-
let _pasteLines = [];
|
|
994
|
-
let _pendingPaste = null;
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* Complete a paste: store text, show [Pasted content] indicator, wait for Enter.
|
|
998
|
-
*/
|
|
999
|
-
function _completePaste() {
|
|
1000
|
-
const combined = _pasteLines.join('\n').trim();
|
|
1001
|
-
_pasteLines = [];
|
|
1002
|
-
_pasteActive = false;
|
|
1003
|
-
if (!combined) return true;
|
|
1004
|
-
|
|
1005
|
-
_pendingPaste = combined;
|
|
1006
|
-
const lines = combined.split('\n');
|
|
1007
|
-
const lineCount = lines.length;
|
|
1008
|
-
|
|
1009
|
-
// Show paste indicator — user must press Enter to submit
|
|
1010
|
-
const preview = lines[0].length > 80 ? lines[0].substring(0, 77) + '...' : lines[0];
|
|
1011
|
-
const label = lineCount > 1 ? `[Pasted content — ${lineCount} lines]` : '[Pasted content]';
|
|
1012
|
-
console.log(`\n${C.dim} ${label}${C.reset}`);
|
|
1013
|
-
console.log(`${C.dim} ⎿ ${preview}${C.reset}`);
|
|
1014
|
-
if (lineCount > 1) {
|
|
1015
|
-
console.log(`${C.dim} ⎿ … +${lineCount - 1} more lines${C.reset}`);
|
|
1016
|
-
}
|
|
1017
|
-
console.log(`${C.dim} Press Enter to send${C.reset}`);
|
|
1018
|
-
|
|
1019
|
-
// Don't write text into readline — just wait for Enter
|
|
1020
|
-
return true;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
if (process.stdin.isTTY) {
|
|
1024
|
-
process.stdout.write('\x1b[?2004h'); // enable bracketed paste
|
|
1025
|
-
|
|
1026
|
-
const origEmit = process.stdin.emit.bind(process.stdin);
|
|
1027
|
-
process.stdin.emit = function (event, ...args) {
|
|
1028
|
-
if (event !== 'data') return origEmit.call(process.stdin, event, ...args);
|
|
1029
|
-
|
|
1030
|
-
// Normalize: Buffer → string
|
|
1031
|
-
let data = args[0];
|
|
1032
|
-
if (Buffer.isBuffer(data)) data = data.toString('utf8');
|
|
1033
|
-
if (typeof data !== 'string') return origEmit.call(process.stdin, event, ...args);
|
|
1034
|
-
|
|
1035
|
-
const hasStart = data.includes(PASTE_START);
|
|
1036
|
-
const hasEnd = data.includes(PASTE_END);
|
|
1037
|
-
|
|
1038
|
-
// Case 1: Complete paste in single chunk (most common for small/medium pastes)
|
|
1039
|
-
if (hasStart && hasEnd) {
|
|
1040
|
-
const clean = stripPasteSequences(data);
|
|
1041
|
-
if (clean) _pasteLines.push(...clean.split('\n'));
|
|
1042
|
-
return _completePaste();
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// Case 2: Paste start (multi-chunk paste begins)
|
|
1046
|
-
if (hasStart) {
|
|
1047
|
-
_pasteActive = true;
|
|
1048
|
-
_pasteLines = [];
|
|
1049
|
-
const clean = stripPasteSequences(data);
|
|
1050
|
-
if (clean) _pasteLines.push(...clean.split('\n'));
|
|
1051
|
-
return true;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// Case 3: Paste end (multi-chunk paste completes)
|
|
1055
|
-
if (hasEnd) {
|
|
1056
|
-
const clean = stripPasteSequences(data);
|
|
1057
|
-
if (clean) _pasteLines.push(...clean.split('\n'));
|
|
1058
|
-
return _completePaste();
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Case 4: Middle of multi-chunk paste
|
|
1062
|
-
if (_pasteActive) {
|
|
1063
|
-
const clean = stripPasteSequences(data);
|
|
1064
|
-
if (clean) _pasteLines.push(...clean.split('\n'));
|
|
1065
|
-
return true;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// Normal data — pass through
|
|
1069
|
-
return origEmit.call(process.stdin, event, ...args);
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// ─── Inline slash-command suggestions (live while typing) ───
|
|
1074
|
-
let _sugN = 0;
|
|
1075
|
-
|
|
1076
|
-
function _clearSug() {
|
|
1077
|
-
if (_sugN > 0) {
|
|
1078
|
-
// Use relative cursor movement (scroll-safe, unlike \x1b[s/\x1b[u])
|
|
1079
|
-
let s = '';
|
|
1080
|
-
for (let i = 0; i < _sugN; i++) s += '\x1b[1B\x1b[2K';
|
|
1081
|
-
s += `\x1b[${_sugN}A`;
|
|
1082
|
-
process.stdout.write(s);
|
|
1083
|
-
_sugN = 0;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
function _showSug(line) {
|
|
1088
|
-
const hits = [...SLASH_COMMANDS, ...getSkillCommands()].filter((c) => c.cmd.startsWith(line));
|
|
1089
|
-
if (!hits.length || (hits.length === 1 && hits[0].cmd === line)) return;
|
|
1090
|
-
const maxShow = 10;
|
|
1091
|
-
const show = hits.slice(0, maxShow);
|
|
1092
|
-
const padLen = Math.max(...show.map((c) => c.cmd.length));
|
|
1093
|
-
let buf = '';
|
|
1094
|
-
for (const { cmd, desc } of show) {
|
|
1095
|
-
const typed = cmd.substring(0, line.length);
|
|
1096
|
-
const rest = cmd.substring(line.length);
|
|
1097
|
-
const gap = ' '.repeat(Math.max(0, padLen - cmd.length + 2));
|
|
1098
|
-
buf += `\n\x1b[2K ${C.cyan}${typed}${C.reset}${C.dim}${rest}${gap}${desc}${C.reset}`;
|
|
1099
|
-
}
|
|
1100
|
-
_sugN = show.length;
|
|
1101
|
-
if (hits.length > maxShow) {
|
|
1102
|
-
buf += `\n\x1b[2K ${C.dim}… +${hits.length - maxShow} more${C.reset}`;
|
|
1103
|
-
_sugN++;
|
|
1104
|
-
}
|
|
1105
|
-
// Move back up using relative movement (scroll-safe)
|
|
1106
|
-
// Then restore cursor column (prompt length + cursor position)
|
|
1107
|
-
const promptVisible = rl._prompt.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
1108
|
-
buf += `\x1b[${_sugN}A\x1b[${promptVisible + rl.cursor + 1}G`;
|
|
1109
|
-
process.stdout.write(buf);
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
if (process.stdin.isTTY) {
|
|
1113
|
-
process.stdin.on('keypress', (str, key) => {
|
|
1114
|
-
_clearSug();
|
|
1115
|
-
if (key && (key.name === 'tab' || key.name === 'return')) return;
|
|
1116
|
-
setImmediate(() => {
|
|
1117
|
-
if (rl.line && rl.line.startsWith('/')) {
|
|
1118
|
-
_showSug(rl.line);
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// ─── Multi-line input state ──────────────────────────────────
|
|
1125
|
-
let multiLineBuffer = null; // null = not in multi-line mode
|
|
1126
|
-
const MULTI_LINE_PROMPT = `${C.dim}...${C.reset} `;
|
|
1127
|
-
|
|
1128
|
-
rl.setPrompt(getPrompt());
|
|
1129
|
-
rl.prompt();
|
|
1130
|
-
|
|
1131
|
-
rl.on('line', async (line) => {
|
|
1132
|
-
_clearSug();
|
|
1133
|
-
|
|
1134
|
-
// Ignore input while already processing (prevents duplicate submissions)
|
|
1135
|
-
if (_processing) {
|
|
1136
|
-
_pendingPaste = null;
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// Intercept pasted content (stored by paste handler, submitted with Enter)
|
|
1141
|
-
if (_pendingPaste !== null) {
|
|
1142
|
-
line = _pendingPaste;
|
|
1143
|
-
_pendingPaste = null;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Multi-line mode handling
|
|
1147
|
-
if (multiLineBuffer !== null) {
|
|
1148
|
-
// """ mode: wait for closing """
|
|
1149
|
-
if (multiLineBuffer._mode === 'triple') {
|
|
1150
|
-
if (line.trim() === '"""') {
|
|
1151
|
-
const input = multiLineBuffer.join('\n').trim();
|
|
1152
|
-
multiLineBuffer = null;
|
|
1153
|
-
if (input) {
|
|
1154
|
-
appendHistory(input.replace(/\n/g, '\\n'));
|
|
1155
|
-
_processing = true;
|
|
1156
|
-
_sigintCount = 0;
|
|
1157
|
-
_abortController = new AbortController();
|
|
1158
|
-
try {
|
|
1159
|
-
await processInput(input);
|
|
1160
|
-
} catch (err) {
|
|
1161
|
-
if (!_abortController?.signal?.aborted) {
|
|
1162
|
-
const userMessage = err.message?.split('\n')[0] || 'An unexpected error occurred';
|
|
1163
|
-
console.log(`${C.red}Error: ${userMessage}${C.reset}`);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
_processing = false;
|
|
1167
|
-
const msgCount = getConversationLength();
|
|
1168
|
-
if (msgCount > 0) {
|
|
1169
|
-
process.stdout.write(`${C.gray}[${msgCount} messages] ${C.reset}`);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
rl.setPrompt(getPrompt());
|
|
1173
|
-
rl.prompt();
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
multiLineBuffer.push(line);
|
|
1177
|
-
rl.setPrompt(MULTI_LINE_PROMPT);
|
|
1178
|
-
rl.prompt();
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// Backslash continuation mode
|
|
1183
|
-
if (line.endsWith('\\')) {
|
|
1184
|
-
multiLineBuffer.push(line.slice(0, -1));
|
|
1185
|
-
} else {
|
|
1186
|
-
multiLineBuffer.push(line);
|
|
1187
|
-
const input = multiLineBuffer.join('\n').trim();
|
|
1188
|
-
multiLineBuffer = null;
|
|
1189
|
-
if (input) {
|
|
1190
|
-
appendHistory(input.replace(/\n/g, '\\n'));
|
|
1191
|
-
_processing = true;
|
|
1192
|
-
_sigintCount = 0;
|
|
1193
|
-
_abortController = new AbortController();
|
|
1194
|
-
try {
|
|
1195
|
-
await processInput(input);
|
|
1196
|
-
} catch (err) {
|
|
1197
|
-
if (!_abortController?.signal?.aborted) {
|
|
1198
|
-
const userMessage = err.message?.split('\n')[0] || 'An unexpected error occurred';
|
|
1199
|
-
console.log(`${C.red}Error: ${userMessage}${C.reset}`);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
_processing = false;
|
|
1203
|
-
const msgCount = getConversationLength();
|
|
1204
|
-
if (msgCount > 0) {
|
|
1205
|
-
process.stdout.write(`${C.gray}[${msgCount} messages] ${C.reset}`);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
rl.setPrompt(getPrompt());
|
|
1209
|
-
rl.prompt();
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
rl.setPrompt(MULTI_LINE_PROMPT);
|
|
1213
|
-
rl.prompt();
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
// Check for multi-line start with """
|
|
1218
|
-
if (line.trim() === '"""' || line.trim().startsWith('"""')) {
|
|
1219
|
-
const after = line.trim().substring(3);
|
|
1220
|
-
multiLineBuffer = after ? [after] : [];
|
|
1221
|
-
multiLineBuffer._mode = 'triple';
|
|
1222
|
-
rl.setPrompt(MULTI_LINE_PROMPT);
|
|
1223
|
-
rl.prompt();
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// Backslash continuation
|
|
1228
|
-
if (line.endsWith('\\')) {
|
|
1229
|
-
multiLineBuffer = [line.slice(0, -1)];
|
|
1230
|
-
multiLineBuffer._mode = 'backslash';
|
|
1231
|
-
rl.setPrompt(MULTI_LINE_PROMPT);
|
|
1232
|
-
rl.prompt();
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
const input = line.trim();
|
|
1237
|
-
if (!input) {
|
|
1238
|
-
rl.setPrompt(getPrompt());
|
|
1239
|
-
rl.prompt();
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
// Persist history
|
|
1244
|
-
appendHistory(input);
|
|
1245
|
-
|
|
1246
|
-
// Slash commands
|
|
1247
|
-
if (input === '/') {
|
|
1248
|
-
showCommandList();
|
|
1249
|
-
rl.setPrompt(getPrompt());
|
|
1250
|
-
rl.prompt();
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
if (input.startsWith('/')) {
|
|
1254
|
-
await handleSlashCommand(input, rl);
|
|
1255
|
-
rl.setPrompt(getPrompt());
|
|
1256
|
-
rl.prompt();
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Process through agent
|
|
1261
|
-
_processing = true;
|
|
1262
|
-
_sigintCount = 0;
|
|
1263
|
-
_abortController = new AbortController();
|
|
1264
|
-
try {
|
|
1265
|
-
await processInput(input);
|
|
1266
|
-
} catch (err) {
|
|
1267
|
-
if (!_abortController?.signal?.aborted) {
|
|
1268
|
-
const userMessage = err.message?.split('\n')[0] || 'An unexpected error occurred';
|
|
1269
|
-
console.log(`${C.red}Error: ${userMessage}${C.reset}`);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
_processing = false;
|
|
1273
|
-
|
|
1274
|
-
const msgCount = getConversationLength();
|
|
1275
|
-
if (msgCount > 0) {
|
|
1276
|
-
process.stdout.write(`${C.gray}[${msgCount} messages] ${C.reset}`);
|
|
1277
|
-
}
|
|
1278
|
-
rl.setPrompt(getPrompt());
|
|
1279
|
-
rl.prompt();
|
|
1280
|
-
});
|
|
1281
|
-
|
|
1282
|
-
rl.on('close', () => {
|
|
1283
|
-
if (process.stdin.isTTY) process.stdout.write('\x1b[?2004l'); // disable bracketed paste
|
|
1284
|
-
console.log(`\n${C.gray}Bye!${C.reset}`);
|
|
1285
|
-
process.exit(0);
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
module.exports = { startREPL, getPrompt, loadHistory, appendHistory, getHistoryPath, HISTORY_MAX, showCommandList, completer, completeFilePath, handleSlashCommand, showProviders, showHelp, renderBar, hasPasteStart, hasPasteEnd, stripPasteSequences, getAbortSignal };
|