ineedcodes 1.0.1 → 1.1.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.
- package/README.md +35 -2
- package/package.json +1 -1
- package/src/agent.js +186 -12
- package/src/cli.js +11 -5
- package/src/config.js +5 -1
- package/src/mcp.js +203 -0
- package/src/memory.js +61 -0
- package/src/provider.js +21 -1
- package/src/session.js +362 -99
- package/src/tools.js +20 -1
- package/src/ui.js +71 -8
- package/src/wizard.js +11 -2
package/src/provider.js
CHANGED
|
@@ -39,7 +39,27 @@ export async function chat(cfg, messages, tools, signal) {
|
|
|
39
39
|
}));
|
|
40
40
|
const headers = { 'content-type': 'application/json' };
|
|
41
41
|
if (cfg.apiKey) headers.authorization = `Bearer ${cfg.apiKey}`;
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
// transient failures (network errors, 429, 5xx) get retries with backoff; other HTTP answers do not
|
|
44
|
+
const ATTEMPTS = 4;
|
|
45
|
+
let res;
|
|
46
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
47
|
+
let threw = null;
|
|
48
|
+
try {
|
|
49
|
+
res = await request(`${cfg.baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body) }, signal);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
threw = err;
|
|
52
|
+
}
|
|
53
|
+
const retryable = threw || [429, 500, 502, 503, 504].includes(res?.status);
|
|
54
|
+
if (!retryable || attempt === ATTEMPTS || signal?.aborted) {
|
|
55
|
+
if (threw) throw threw;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (threw || res) {
|
|
59
|
+
const delay = Math.min(15_000, 1500 * attempt * attempt);
|
|
60
|
+
await new Promise(r => setTimeout(r, delay));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
if (!res.ok) {
|
|
44
64
|
const text = await res.text();
|
|
45
65
|
// provider does not know reasoning_effort: retry once without it
|
package/src/session.js
CHANGED
|
@@ -1,156 +1,419 @@
|
|
|
1
|
-
// session.js: interactive
|
|
1
|
+
// session.js: interactive chat TUI. Fixed logo header, scrolling chat area, fixed status + input footer.
|
|
2
|
+
// Falls back to a plain REPL (same commands) when stdout is not a TTY, so tests and pipes keep working.
|
|
2
3
|
|
|
3
4
|
import * as readline from 'node:readline';
|
|
4
|
-
import {
|
|
5
|
+
import { clearConfig, normalize, saveConfig } from './config.js';
|
|
5
6
|
import { runObjective, pushTurn } from './agent.js';
|
|
6
7
|
import { fetchModels } from './provider.js';
|
|
7
|
-
import { makeInput, bold, dim, red, green, yellow, cyan, gray, trunc, BANNER } from './ui.js';
|
|
8
|
+
import { makeInput, bold, dim, red, green, yellow, cyan, gray, trunc, BANNER, logo, box, startSpinner, VERSION, RULE, userBubble, screen } from './ui.js';
|
|
8
9
|
import { wizard } from './wizard.js';
|
|
10
|
+
import { getMemoryProvider, ICMAdapter } from './memory.js';
|
|
11
|
+
import { mcpConfigured } from './mcp.js';
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const plain = s => String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
14
|
+
|
|
15
|
+
function wrapLines(text, width) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const raw of String(text).split('\n')) {
|
|
18
|
+
let line = raw;
|
|
19
|
+
if (line === '') { out.push(''); continue; }
|
|
20
|
+
while (plain(line).length > width) {
|
|
21
|
+
let vis = 0, i = 0;
|
|
22
|
+
while (i < line.length && vis < width) {
|
|
23
|
+
if (line[i] === '\x1b') { while (i < line.length && line[i] !== 'm') i++; }
|
|
24
|
+
else vis++;
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
out.push(line.slice(0, i));
|
|
28
|
+
line = line.slice(i);
|
|
29
|
+
}
|
|
30
|
+
out.push(line);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function startSession(cfg, { fresh = false } = {}) {
|
|
36
|
+
const state = normalize(cfg);
|
|
15
37
|
let history = [];
|
|
16
38
|
let busy = false;
|
|
17
39
|
let activeRun = null;
|
|
18
40
|
let mode = state.mode;
|
|
19
|
-
let pendingLines = [];
|
|
20
41
|
let lastSigint = 0;
|
|
21
42
|
let closed = false;
|
|
43
|
+
const approved = new Set(); // session-wide "always allow" grants
|
|
44
|
+
const pendingLines = [];
|
|
22
45
|
|
|
23
|
-
|
|
24
|
-
if (activeRun) { activeRun.abort(); console.log(dim('\nStopping current task... press Ctrl+C again to force exit.')); return; }
|
|
25
|
-
const now = Date.now();
|
|
26
|
-
if (now - lastSigint < 3000) { console.log(dim('\nGoodbye.')); process.exit(0); }
|
|
27
|
-
lastSigint = now;
|
|
28
|
-
console.log(dim('\n(Ctrl+C again to exit)'));
|
|
29
|
-
rl.prompt();
|
|
30
|
-
});
|
|
46
|
+
const TUI = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
// ONE readline, ONE line dispatcher for the whole session
|
|
49
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
+
let handleRef = null;
|
|
51
|
+
const ask = makeInput(rl, l => handleRef?.(l));
|
|
36
52
|
|
|
37
|
-
const
|
|
38
|
-
console.log(BANNER() + dim(` · ${state.model} · ${process.cwd()}`));
|
|
39
|
-
console.log(dim('Just say what you want. /help shows shortcuts. Ctrl+C twice exits.'));
|
|
40
|
-
};
|
|
53
|
+
const say = TUI ? lines => tuiPrint(lines) : (lines => console.log(lines));
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(' ' + cyan('/reset') + ' clear saved config');
|
|
49
|
-
console.log(' ' + cyan('/clear') + ' forget this session\'s conversation');
|
|
50
|
-
console.log(' ' + cyan('/exit') + ' quit');
|
|
51
|
-
console.log(dim(' Everything else you type is a task, in normal language.'));
|
|
52
|
-
};
|
|
55
|
+
function doExit() {
|
|
56
|
+
closed = true;
|
|
57
|
+
if (TUI) { screen.resetRegion(); screen.exit(); }
|
|
58
|
+
console.log(dim('Goodbye.'));
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
console.log(dim(' Fetching models...'));
|
|
62
|
+
async function pickModel() {
|
|
56
63
|
let models = [];
|
|
57
|
-
try { models = await fetchModels(state); } catch (err) {
|
|
64
|
+
try { models = await fetchModels(state); } catch (err) { say(red(' ' + err.message)); return; }
|
|
58
65
|
if (models.length === 0) {
|
|
59
|
-
|
|
60
|
-
if (m) { state = normalize({ ...state, model: m }); console.log(green(' Model: ' + state.model)); }
|
|
66
|
+
say(yellow(' Server sent no list. Change model by editing ~/.ineedcodes/config.json'));
|
|
61
67
|
return;
|
|
62
68
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
list.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
|
|
69
|
+
const list = models.slice(0, 5);
|
|
70
|
+
say(dim(` ${models.length} models available, showing ${list.length}. Type a number, or a full model id.`));
|
|
71
|
+
list.forEach((m, i) => say(` ${i + 1}. ${m}`));
|
|
67
72
|
const pick = await ask(' Model: ');
|
|
68
73
|
if (!pick) return;
|
|
69
74
|
const idx = Number(pick);
|
|
70
|
-
if (Number.isInteger(idx) && idx >= 1 && idx <= list.length) state
|
|
71
|
-
else state
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= list.length) Object.assign(state, normalize({ ...state, model: list[idx - 1] }));
|
|
76
|
+
else Object.assign(state, normalize({ ...state, model: pick }));
|
|
77
|
+
say(green(' Model: ' + state.model));
|
|
78
|
+
if (TUI) drawStatus();
|
|
79
|
+
}
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
async function runTask(input) {
|
|
76
82
|
busy = true;
|
|
83
|
+
let lastStreamed = '';
|
|
84
|
+
if (TUI) tuiUserLine(input);
|
|
85
|
+
let spinner = null;
|
|
86
|
+
const stopSpinner = () => { spinner?.stop(); spinner = null; };
|
|
77
87
|
try {
|
|
78
88
|
const res = await runObjective(state, input, process.cwd(), history, {
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
onMemoryStart: () => { stopSpinner(); spinner = startSpinner('recalling memory'); },
|
|
90
|
+
onMemoryEnd: () => stopSpinner(),
|
|
91
|
+
onThinkingStart: () => { stopSpinner(); spinner = startSpinner('thinking'); },
|
|
92
|
+
onThinkingEnd: () => stopSpinner(),
|
|
93
|
+
onWorkStart: label => { stopSpinner(); spinner = startSpinner(label || 'working'); },
|
|
94
|
+
onWorkEnd: () => stopSpinner(),
|
|
95
|
+
onTool: (name, input2) => { stopSpinner(); say(cyan(' ● ' + name) + gray(' ' + trunc(JSON.stringify(input2), 90))); },
|
|
96
|
+
onResult: out => { say(gray(' ' + trunc(out, 110))); },
|
|
97
|
+
onText: t => { stopSpinner(); lastStreamed = t; },
|
|
98
|
+
onTodos: list => {
|
|
99
|
+
stopSpinner();
|
|
100
|
+
const mark = s => s === 'completed' ? green('✔') : s === 'in_progress' ? cyan('▸') : dim('○');
|
|
101
|
+
say(box([bold('To-do'), ...list.map(t => ' ' + mark(t.status) + ' ' + t.content)]));
|
|
102
|
+
},
|
|
103
|
+
onAgentStart: (id, input) => { stopSpinner(); say(cyan(' ◆ spawn ' + id) + gray(` role=${input.role ?? '?'} task=${trunc(String(input.objective ?? ''), 70)}`)); },
|
|
104
|
+
onAgentEnd: (id, r) => { stopSpinner(); say((r.status === 'completed' ? green(' ◆ ' + id + ' done') : yellow(' ◆ ' + id + ' ' + r.status)) + gray(' ' + trunc(String(r.summary ?? '').replaceAll('\n', ' '), 90))); },
|
|
105
|
+
onMCP: names => { if (names.length) say(dim(' MCP tools available: ' + names.join(', '))); },
|
|
106
|
+
onMCPResult: (name, out) => { say(gray(' mcp result: ' + trunc(out, 100))); },
|
|
107
|
+
onApprove: async (cat, name, input2) => {
|
|
108
|
+
stopSpinner();
|
|
109
|
+
say(yellow(' ⚠ approval needed') + ' ' + cyan(name) + gray(' ' + trunc(JSON.stringify(input2), 80)));
|
|
110
|
+
const a = await ask(' [y] once · [a] always for ' + cat + ' · [n] no: ');
|
|
111
|
+
const c = a.trim().toLowerCase();
|
|
112
|
+
if (c === 'a' || c === 'always') { approved.add(cat); say(dim(' always allowed for this session.')); return 'always'; }
|
|
113
|
+
if (c === 'y' || c === 'yes') return true;
|
|
114
|
+
say(dim(' denied.'));
|
|
115
|
+
return false;
|
|
81
116
|
},
|
|
82
|
-
|
|
83
|
-
onText: t => { console.log(' ' + dim(trunc(t, 300))); },
|
|
117
|
+
approved,
|
|
84
118
|
onRunStart: c => { activeRun = c; },
|
|
85
|
-
onRunEnd: () => { activeRun = null; }
|
|
119
|
+
onRunEnd: () => { activeRun = null; stopSpinner(); }
|
|
86
120
|
});
|
|
87
121
|
history = pushTurn(history, input, res);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
122
|
+
stopSpinner();
|
|
123
|
+
if (res.aborted) {
|
|
124
|
+
say(yellow(' ■ Stopped') + dim(' - partly done. Ask me to continue.'));
|
|
125
|
+
} else {
|
|
126
|
+
const rows = [green(bold('■ Done'))];
|
|
127
|
+
if (res.changed?.length) rows.push(dim(' files: ') + res.changed.join(', '));
|
|
128
|
+
if (res.answer) String(res.answer).split('\n').slice(0, 14).forEach(l => rows.push(' ' + l));
|
|
129
|
+
else if (!res.changed?.length) rows.push(dim(' (no output)'));
|
|
130
|
+
say(box(rows));
|
|
93
131
|
}
|
|
94
132
|
} catch (err) {
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
stopSpinner();
|
|
134
|
+
history = pushTurn(history, input, { answer: '(task failed: ' + err.message + ')' });
|
|
135
|
+
say(red(' ✗ ' + err.message) + dim(' context kept.'));
|
|
97
136
|
} finally {
|
|
98
137
|
busy = false;
|
|
99
138
|
activeRun = null;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
drain();
|
|
103
|
-
}
|
|
139
|
+
stopSpinner();
|
|
140
|
+
await afterTask();
|
|
104
141
|
}
|
|
105
|
-
}
|
|
142
|
+
}
|
|
106
143
|
|
|
107
|
-
|
|
144
|
+
async function afterTask() {
|
|
145
|
+
if (closed) return;
|
|
146
|
+
if (TUI) { drawStatus(); scrollRegion(); return; }
|
|
147
|
+
plainPrompt();
|
|
108
148
|
while (!busy && !closed) {
|
|
109
149
|
const next = pendingLines.shift();
|
|
110
150
|
if (!next) break;
|
|
111
|
-
handle(next);
|
|
151
|
+
await handle(next);
|
|
112
152
|
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const commands = {
|
|
156
|
+
'/help': () => {
|
|
157
|
+
say(' ' + cyan('/model') + ' pick a model from your provider');
|
|
158
|
+
say(' ' + cyan('/plan') + ' plan mode: read only');
|
|
159
|
+
say(' ' + cyan('/build') + ' build mode: real changes (default)');
|
|
160
|
+
say(' ' + cyan('/reason') + ' toggle reasoning low/high');
|
|
161
|
+
say(' ' + cyan('/perm') + ' permissions: /perm auto | /perm safe | /perm');
|
|
162
|
+
say(' ' + cyan('/config') + ' show provider config (key hidden)');
|
|
163
|
+
say(' ' + cyan('/memory') + ' memory status, /memory on|off to toggle');
|
|
164
|
+
say(' ' + cyan('/mcp') + ' list MCP servers and their tools');
|
|
165
|
+
say(' ' + cyan('/clear') + ' forget this conversation');
|
|
166
|
+
say(' ' + cyan('/setup') + ' redo provider setup');
|
|
167
|
+
say(' ' + cyan('/reset') + ' clear saved config');
|
|
168
|
+
say(' ' + cyan('/exit') + ' quit');
|
|
169
|
+
},
|
|
170
|
+
'/config': () => {
|
|
171
|
+
say(box([
|
|
172
|
+
bold('Provider config') + dim(' ~/.ineedcodes/config.json'),
|
|
173
|
+
dim('base URL') + ' ' + state.baseUrl,
|
|
174
|
+
dim('model') + ' ' + state.model,
|
|
175
|
+
dim('reasoning') + ' ' + state.reasoning,
|
|
176
|
+
dim('mode') + ' ' + mode,
|
|
177
|
+
dim('memory') + ' ' + (state.memory === false ? 'off' : 'on (icm)'),
|
|
178
|
+
dim('edit perm') + ' ' + state.permEdit,
|
|
179
|
+
dim('shell perm') + ' ' + state.permShell,
|
|
180
|
+
dim('API key') + ' saved, hidden'
|
|
181
|
+
]));
|
|
182
|
+
},
|
|
183
|
+
'/clear': () => { history = []; say(dim('Conversation forgotten.')); }
|
|
113
184
|
};
|
|
114
185
|
|
|
115
|
-
|
|
116
|
-
if (!input)
|
|
186
|
+
async function handle(input) {
|
|
187
|
+
if (!input) return;
|
|
117
188
|
if (busy) { pendingLines.push(input); return; }
|
|
118
|
-
if (['/exit', '/quit', 'exit', 'quit'].includes(input))
|
|
119
|
-
if (input === '/help' || input === '?')
|
|
120
|
-
if (input === '/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
189
|
+
if (['/exit', '/quit', 'exit', 'quit'].includes(input)) return doExit();
|
|
190
|
+
if (input === '/help' || input === '?') return commands['/help']();
|
|
191
|
+
if (input === '/config') return commands['/config']();
|
|
192
|
+
if (input === '/clear') return commands['/clear']();
|
|
193
|
+
if (input === '/model') { busy = true; try { await pickModel(); } finally { busy = false; } return afterTask(); }
|
|
194
|
+
if (input === '/plan') { mode = 'plan'; Object.assign(state, normalize({ ...state, mode })); say(yellow('Plan mode: read only.')); if (TUI) drawStatus(); return; }
|
|
195
|
+
if (input === '/build') { mode = 'build'; Object.assign(state, normalize({ ...state, mode })); say(green('Build mode: real changes.')); if (TUI) drawStatus(); return; }
|
|
196
|
+
if (input === '/reason') {
|
|
197
|
+
Object.assign(state, normalize({ ...state, reasoning: state.reasoning === 'high' ? 'low' : 'high' }));
|
|
198
|
+
say(dim('Reasoning effort: ' + state.reasoning));
|
|
199
|
+
if (TUI) drawStatus();
|
|
129
200
|
return;
|
|
130
201
|
}
|
|
131
|
-
if (input === '/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
202
|
+
if (input === '/perm' || input.startsWith('/perm ')) {
|
|
203
|
+
const arg = input.slice(5).trim();
|
|
204
|
+
const apply = (pe, ps, msg) => {
|
|
205
|
+
Object.assign(state, normalize({ ...state, permEdit: pe, permShell: ps }));
|
|
206
|
+
saveConfig(state);
|
|
207
|
+
say(green(msg));
|
|
208
|
+
if (TUI) drawStatus();
|
|
209
|
+
};
|
|
210
|
+
if (arg === 'auto') return apply('allow', 'allow', 'Permissions: edits and shell run without asking.');
|
|
211
|
+
if (arg === 'safe') return apply('ask', 'ask', 'Permissions: edits and shell ask first.');
|
|
212
|
+
const parts = arg.split(/\s+/);
|
|
213
|
+
if (parts[0] === 'edit' && ['allow', 'ask'].includes(parts[1])) {
|
|
214
|
+
return apply(parts[1], state.permShell, `Edit permission: ${parts[1]}.`);
|
|
215
|
+
}
|
|
216
|
+
if (parts[0] === 'shell' && ['allow', 'ask'].includes(parts[1])) {
|
|
217
|
+
return apply(state.permEdit, parts[1], `Shell permission: ${parts[1]}.`);
|
|
218
|
+
}
|
|
219
|
+
say(box([
|
|
220
|
+
bold('Permissions'),
|
|
221
|
+
dim('edit') + ' ' + state.permEdit + dim(' (write_file, edit_file, delete_file)'),
|
|
222
|
+
dim('shell') + ' ' + state.permShell,
|
|
223
|
+
dim('granted this session') + ' ' + ([...approved].join(', ') || 'none'),
|
|
224
|
+
'',
|
|
225
|
+
dim('/perm auto') + ' never ask (saved)',
|
|
226
|
+
dim('/perm safe') + ' ask for edits and shell (saved)',
|
|
227
|
+
dim('/perm edit allow|ask /perm shell allow|ask')
|
|
228
|
+
]));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (input === '/memory' || input.startsWith('/memory ')) {
|
|
232
|
+
const arg = input.split(/\s+/)[1];
|
|
233
|
+
if (arg === 'on' || arg === 'off') {
|
|
234
|
+
Object.assign(state, normalize({ ...state, memory: arg === 'on' }));
|
|
235
|
+
saveConfig(state);
|
|
236
|
+
say(arg === 'on'
|
|
237
|
+
? green('Memory: on') + dim(' - durable facts are recalled before tasks and stored after real work.')
|
|
238
|
+
: yellow('Memory: off') + dim(' - nothing is recalled or stored.'));
|
|
239
|
+
if (TUI) drawStatus();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
busy = true;
|
|
243
|
+
const provider = getMemoryProvider(state);
|
|
244
|
+
if (!provider) { say(yellow('Memory is off.') + dim(' Turn it on with /memory on')); busy = false; return afterTask(); }
|
|
245
|
+
const ok = await ICMAdapter.available();
|
|
246
|
+
say(ok ? green('Memory: on') + dim(` via ${provider.name}.`) : yellow('Memory: provider not installed.') + dim(' Install icm to enable.'));
|
|
247
|
+
busy = false;
|
|
248
|
+
return afterTask();
|
|
249
|
+
}
|
|
250
|
+
if (input === '/mcp' || input === '/mcp reload') {
|
|
251
|
+
if (!mcpConfigured()) {
|
|
252
|
+
say(yellow('No MCP servers configured.') + dim(' Add them to ~/.ineedcodes/mcp.json, e.g.: {"context7":{"command":"npx","args":["-y","@upstash/context7-mcp"]}}'));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
busy = true;
|
|
256
|
+
try {
|
|
257
|
+
const { McpManager } = await import('./mcp.js');
|
|
258
|
+
const mgr = new McpManager();
|
|
259
|
+
const errors = await mgr.loadFromConfig();
|
|
260
|
+
for (const e of errors) say(red(' ✗ ' + e));
|
|
261
|
+
const tools = await mgr.allTools();
|
|
262
|
+
if (tools.length) {
|
|
263
|
+
say(green(` ${mgr.servers.size} MCP server(s), ${tools.length} tool(s):`));
|
|
264
|
+
for (const t of tools) say(' ' + cyan(t.name) + gray(' ' + trunc(t.description, 90)));
|
|
265
|
+
} else if (!errors.length) {
|
|
266
|
+
say(yellow(' Servers connected but exposed no tools.'));
|
|
267
|
+
}
|
|
268
|
+
mgr.killAll();
|
|
269
|
+
} catch (err) { say(red(' ✗ ' + err.message)); }
|
|
270
|
+
busy = false;
|
|
271
|
+
return afterTask();
|
|
137
272
|
}
|
|
138
|
-
if (input === '/clear') { history = []; console.log(dim('Conversation forgotten.')); prompt(); return; }
|
|
139
273
|
if (input === '/setup' || input === '/reset') {
|
|
140
274
|
clearConfig();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
275
|
+
say(dim('Config cleared. Running setup...'));
|
|
276
|
+
busy = true;
|
|
277
|
+
try {
|
|
278
|
+
const c = await wizard(ask, { fromCommand: true });
|
|
279
|
+
Object.assign(state, normalize(c));
|
|
144
280
|
mode = state.mode;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
281
|
+
say(green('Ready. ' + state.model));
|
|
282
|
+
if (TUI) { drawHeader(); drawStatus(); }
|
|
283
|
+
} catch { return doExit(); }
|
|
284
|
+
busy = false;
|
|
285
|
+
return afterTask();
|
|
286
|
+
}
|
|
287
|
+
return runTask(input);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const plainPrompt = () => {
|
|
291
|
+
rl.setPrompt(`\n[${mode}/${state.reasoning}] ${bold(green('ineed'))} ${green('❯')} `);
|
|
292
|
+
rl.prompt();
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const printWelcome = () => {
|
|
296
|
+
const lines = fresh
|
|
297
|
+
? [
|
|
298
|
+
green(bold('Welcome to ineed!')),
|
|
299
|
+
' You are all set: any OpenAI-compatible provider, any folder.',
|
|
300
|
+
' Type what you want in normal language, for example:',
|
|
301
|
+
dim(' "buatkan landing page beranimasi di folder ini"'),
|
|
302
|
+
dim(' "fix the failing tests and tell me what was wrong"'),
|
|
303
|
+
dim(' "explain this repository like I am a beginner"'),
|
|
304
|
+
' ' + dim('Helpers: /help commands · /perm auto or safe · /model · /memory on|off')
|
|
305
|
+
]
|
|
306
|
+
: [
|
|
307
|
+
dim('Type what you want. /help for commands.')
|
|
308
|
+
+ (state.permEdit === 'ask' || state.permShell === 'ask' ? dim(' Edits and shell ask first: /perm auto to relax.') : '')
|
|
309
|
+
];
|
|
310
|
+
for (const l of lines) say(l);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// ── plain REPL mode (pipes, tests, NO_COLOR) ──
|
|
314
|
+
if (!TUI) {
|
|
315
|
+
rl.on('SIGINT', () => {
|
|
316
|
+
if (activeRun) { activeRun.abort(); console.log(dim('\nStopping...')); return; }
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
if (now - lastSigint < 3000) return doExit();
|
|
319
|
+
lastSigint = now;
|
|
320
|
+
console.log(dim('\n(Ctrl+C again to exit)'));
|
|
321
|
+
rl.prompt();
|
|
322
|
+
});
|
|
323
|
+
handleRef = handle;
|
|
324
|
+
console.log(logo());
|
|
325
|
+
console.log(BANNER() + dim(` · ${state.model} · ${process.cwd()}`));
|
|
326
|
+
printWelcome();
|
|
327
|
+
plainPrompt();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── full TUI mode ──
|
|
332
|
+
const chatLines = []; // completed chat lines (ANSI strings)
|
|
333
|
+
let chatTop = 0, chatBot = 0; // scroll region rows
|
|
334
|
+
let statusRow = 0;
|
|
335
|
+
|
|
336
|
+
function drawHeader() {
|
|
337
|
+
const rows = 6;
|
|
338
|
+
const lines = logo().split('\n');
|
|
339
|
+
for (let i = 0; i < rows; i++) {
|
|
340
|
+
screen.at(i + 1, 1);
|
|
341
|
+
screen.clearLine();
|
|
342
|
+
process.stdout.write(lines[i] ?? '');
|
|
149
343
|
}
|
|
150
|
-
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function drawStatus() {
|
|
347
|
+
const cols = process.stdout.columns || 80;
|
|
348
|
+
const left = ` ${bold(green('ineed'))} ${dim(`v${VERSION}`)}`;
|
|
349
|
+
const mid = ` ${dim(state.model)}`;
|
|
350
|
+
const right = ` ${mode === 'plan' ? yellow('plan') : green('build')} ${dim('/')} ${dim(state.reasoning)} ${dim('/')} ${state.memory === false ? dim('mem:off') : dim('mem:on')} `;
|
|
351
|
+
screen.at(statusRow, 1);
|
|
352
|
+
screen.clearLine();
|
|
353
|
+
process.stdout.write(dim('─'.repeat(Math.max(0, cols - plain(left).length - plain(mid).length - plain(right).length))) + left + mid + right);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function scrollRegion() {
|
|
357
|
+
screen.region(chatTop, chatBot);
|
|
358
|
+
screen.at(chatBot, 1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function redrawChat() {
|
|
362
|
+
for (let i = chatTop; i <= chatBot; i++) {
|
|
363
|
+
screen.at(i, 1);
|
|
364
|
+
screen.clearLine();
|
|
365
|
+
}
|
|
366
|
+
screen.at(chatTop, 1);
|
|
367
|
+
const vis = chatBot - chatTop + 1;
|
|
368
|
+
const show = chatLines.slice(-vis);
|
|
369
|
+
process.stdout.write(show.join('\r\n'));
|
|
370
|
+
scrollRegion();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function tuiPrint(text) {
|
|
374
|
+
for (const l of wrapLines(String(text), Math.max(10, (process.stdout.columns || 80) - 4))) chatLines.push(l);
|
|
375
|
+
redrawChat();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function tuiUserLine(input) {
|
|
379
|
+
for (const l of wrapLines(userBubble(input), Math.max(10, (process.stdout.columns || 80) - 4))) chatLines.push(l);
|
|
380
|
+
redrawChat();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const layout = () => {
|
|
384
|
+
const rows = process.stdout.rows || 24;
|
|
385
|
+
const headerRows = 6;
|
|
386
|
+
statusRow = rows - 1;
|
|
387
|
+
chatTop = headerRows + 1;
|
|
388
|
+
chatBot = statusRow - 1;
|
|
389
|
+
screen.at(1, 1);
|
|
390
|
+
process.stdout.write('\x1b[2J');
|
|
391
|
+
drawHeader();
|
|
392
|
+
redrawChat();
|
|
393
|
+
drawStatus();
|
|
394
|
+
scrollRegion();
|
|
151
395
|
};
|
|
152
396
|
|
|
397
|
+
rl.on('resize', layout);
|
|
398
|
+
|
|
399
|
+
// keep Ctrl+Z from suspending us in raw mode: swallow the key, tell the user
|
|
400
|
+
const origWrite = rl.write.bind(rl);
|
|
401
|
+
rl.write = (d, key) => {
|
|
402
|
+
if (key && key.ctrl && key.name === 'z') { tuiPrint(dim(' (Ctrl+Z is disabled here. Use /exit, or Ctrl+C twice.)')); return; }
|
|
403
|
+
return origWrite(d, key);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
rl.on('SIGINT', () => {
|
|
407
|
+
if (activeRun) { activeRun.abort(); return; }
|
|
408
|
+
const now = Date.now();
|
|
409
|
+
if (now - lastSigint < 3000) return doExit();
|
|
410
|
+
lastSigint = now;
|
|
411
|
+
});
|
|
412
|
+
|
|
153
413
|
handleRef = handle;
|
|
154
|
-
|
|
155
|
-
|
|
414
|
+
|
|
415
|
+
screen.enter();
|
|
416
|
+
layout();
|
|
417
|
+
printWelcome();
|
|
418
|
+
scrollRegion();
|
|
156
419
|
}
|
package/src/tools.js
CHANGED
|
@@ -55,6 +55,25 @@ export const TOOLS = [
|
|
|
55
55
|
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
56
56
|
allowedInPlan: false
|
|
57
57
|
},
|
|
58
|
+
{
|
|
59
|
+
name: 'todo',
|
|
60
|
+
description: 'Maintain a visible task checklist for multi-step objectives. Replace the whole list each time.',
|
|
61
|
+
parameters: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
todos: {
|
|
65
|
+
type: 'array',
|
|
66
|
+
items: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: { content: { type: 'string' }, status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] } },
|
|
69
|
+
required: ['content', 'status']
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
required: ['todos']
|
|
74
|
+
},
|
|
75
|
+
allowedInPlan: true
|
|
76
|
+
},
|
|
58
77
|
{
|
|
59
78
|
name: 'shell',
|
|
60
79
|
description: 'Run a shell command in the working directory. Returns exit code with stdout and stderr.',
|
|
@@ -76,7 +95,7 @@ export function runTool(name, input, cwd) {
|
|
|
76
95
|
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
77
96
|
for (const e of entries) {
|
|
78
97
|
if (skip.has(e.name)) continue;
|
|
79
|
-
out.push(
|
|
98
|
+
out.push(e.isDirectory() ? e.name + '/' : e.name);
|
|
80
99
|
if (e.isDirectory()) walk(path.join(d, e.name), depth + 1);
|
|
81
100
|
}
|
|
82
101
|
})(abs, 0);
|