ineedcodes 0.2.0 → 1.0.1

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/agent.mjs DELETED
@@ -1,244 +0,0 @@
1
- #!/usr/bin/env node
2
- // ineed — minimal, self-contained terminal AI agent. No dependencies.
3
-
4
- import * as fs from 'node:fs';
5
- import * as os from 'node:os';
6
- import * as path from 'node:path';
7
- import { spawn } from 'node:child_process';
8
- import * as readline from 'node:readline';
9
-
10
- const VERSION = '0.2.0';
11
- const CONFIG_DIR = path.join(os.homedir(), '.ineedcodes');
12
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
13
- const A = (c, t) => process.stdout.isTTY ? `\x1b[${c}m${t}\x1b[0m` : t;
14
- const bold = t => A('1', t), dim = t => A('2', t), green = t => A('32', t);
15
- const red = t => A('31', t), yellow = t => A('33', t), cyan = t => A('36', t), gray = t => A('90', t);
16
- const trunc = (s, n = 120) => { const o = s.replaceAll('\n', ' '); return o.length > n ? o.slice(0, n - 1) + '…' : o; };
17
-
18
- /* ── config ─────────────────────────────────────────────── */
19
- function loadConfig() {
20
- try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return null; }
21
- }
22
- function saveConfig(cfg) {
23
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
24
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
25
- }
26
-
27
- /* ── provider API (OpenAI-compatible) ───────────────────── */
28
- async function chat(cfg, messages, tools) {
29
- const body = { model: cfg.model, messages, temperature: 0.2 };
30
- if (tools?.length) body.tools = tools.map(t => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters } }));
31
- const res = await fetch(cfg.baseUrl.replace(/\/+$/, '') + '/chat/completions', {
32
- method: 'POST',
33
- headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.apiKey}` },
34
- body: JSON.stringify(body)
35
- });
36
- if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
37
- return (await res.json()).choices?.[0]?.message ?? { role: 'assistant', content: '', tool_calls: [] };
38
- }
39
-
40
- /* ── tools ──────────────────────────────────────────────── */
41
- const TOOLS = [
42
- { name: 'read_file', description: 'Read a text file (UTF-8).', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
43
- { name: 'write_file', description: 'Create or overwrite a file.', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
44
- { name: 'list_files', description: 'List files in a directory (recursive, skips node_modules/.git/dist).', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
45
- { name: 'shell', description: 'Run a shell command and capture stdout, stderr, exit code.', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } }
46
- ];
47
-
48
- function runTool(name, input, cwd) {
49
- try {
50
- if (name === 'read_file') {
51
- const p = path.resolve(cwd, input.path);
52
- if (path.basename(p).startsWith('.env') || /(^|\/)\.ssh\//.test(p)) return { ok: false, output: 'Refused: looks like a secret file.' };
53
- return { ok: true, output: fs.readFileSync(p, 'utf8').slice(0, 60_000) };
54
- }
55
- if (name === 'write_file') {
56
- const p = path.resolve(cwd, input.path);
57
- fs.mkdirSync(path.dirname(p), { recursive: true });
58
- fs.writeFileSync(p, input.content);
59
- return { ok: true, output: `Wrote ${p} (${input.content.length} bytes).` };
60
- }
61
- if (name === 'list_files') {
62
- const out = [];
63
- const skip = new Set(['node_modules', '.git', 'dist', 'build', '.next']);
64
- (function walk(d, depth) {
65
- if (out.length > 400 || depth > 4) return;
66
- for (const e of fs.readdirSync(d, { withFileTypes: true })) {
67
- if (skip.has(e.name)) continue;
68
- out.push((e.isDirectory() ? e.name + '/' : e.name));
69
- if (e.isDirectory()) walk(path.join(d, e.name), depth + 1);
70
- }
71
- })(path.resolve(cwd, input.path || '.'), 0);
72
- return { ok: true, output: out.join('\n') };
73
- }
74
- if (name === 'shell') {
75
- return { ok: true, output: 'RUNNING' }; // replaced by async shellRun
76
- }
77
- return { ok: false, output: `Unknown tool: ${name}` };
78
- } catch (err) {
79
- return { ok: false, output: `Error: ${err.message}` };
80
- }
81
- }
82
-
83
- function shellRun(command, cwd) {
84
- return new Promise(resolve => {
85
- const child = spawn(command, { cwd, shell: true, env: { ...process.env, NO_COLOR: '1' } });
86
- let out = '';
87
- const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} out += '\n[timeout after 120s]'; }, 120_000);
88
- child.stdout.on('data', c => { out += c.toString(); if (out.length > 100_000) child.kill('SIGKILL'); });
89
- child.stderr.on('data', c => { out += c.toString(); });
90
- child.on('close', code => { clearTimeout(timer); resolve({ ok: code === 0, output: `exit code: ${code}\n${out.slice(0, 20_000)}` }); });
91
- child.on('error', err => { clearTimeout(timer); resolve({ ok: false, output: `spawn error: ${err.message}` }); });
92
- });
93
- }
94
-
95
- /* ── agent loop ─────────────────────────────────────────── */
96
- const SYSTEM = `You are ineed, an autonomous terminal agent on the user's machine.
97
- Rules:
98
- - Use the tools to do real work. Never invent command output; success claims need evidence.
99
- - Prefer targeted edits over full rewrites. Work only inside the current folder.
100
- - Never push to remotes or delete data without being asked.
101
- - When the objective is met, reply with the final result: what changed, what you ran, and the evidence.`;
102
-
103
- async function runObjective(cfg, objective, cwd, history) {
104
- const messages = [
105
- { role: 'system', content: `${SYSTEM}\nWorking directory: ${cwd}` },
106
- ...history,
107
- { role: 'user', content: objective }
108
- ];
109
- let answer = '';
110
-
111
- for (let step = 0; step < 30; step++) {
112
- const msg = await chat(cfg, messages, TOOLS);
113
- messages.push(msg);
114
-
115
- if (msg.content) { console.log(' ' + dim(trunc(msg.content, 200))); answer = msg.content; }
116
- const calls = msg.tool_calls ?? [];
117
- if (calls.length === 0) return { answer, history: [{ role: 'user', content: objective }, { role: 'assistant', content: answer }] };
118
-
119
- for (const call of calls) {
120
- const input = JSON.parse(call.function?.arguments || '{}');
121
- console.log(' ' + cyan('▸ ' + call.function?.name) + ' ' + gray(trunc(call.function?.arguments || '', 90)));
122
- let result;
123
- if (call.function?.name === 'shell') result = await shellRun(input.command, cwd);
124
- else result = runTool(call.function?.name, input, cwd);
125
- console.log(' ' + (result.ok ? green('◂ ok') : red('◂ fail')) + ' ' + gray(trunc(result.output, 100)));
126
- messages.push({ role: 'tool', tool_call_id: call.id, content: result.output.slice(0, 20_000) });
127
- }
128
- }
129
- return { answer: answer || 'Stopped: too many steps.', history: [{ role: 'user', content: objective }, { role: 'assistant', content: answer }] };
130
- }
131
-
132
- /* ── setup wizard ───────────────────────────────────────── */
133
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
134
- let askQueue = [];
135
- let askPending = null;
136
- let wizardMode = true;
137
- let replHandler = null;
138
- rl.on('line', l => {
139
- const line = l.trim();
140
- if (askPending) { const r = askPending; askPending = null; r(line); return; }
141
- if (wizardMode) { askQueue.push(line); return; }
142
- if (replHandler) replHandler(line);
143
- });
144
- rl.on('close', () => { if (askPending) { const r = askPending; askPending = null; r(''); } });
145
- const ask = q => new Promise(res => {
146
- process.stdout.write(q + ' ');
147
- if (askQueue.length > 0) res(askQueue.shift());
148
- else askPending = res;
149
- });
150
-
151
- async function wizard() {
152
- console.log('\n' + bold('Welcome to ineed!') + ' Let me connect to your AI provider. One time only.\n');
153
- const baseUrl = await ask(cyan('1. API base URL (e.g. https://api.openai.com/v1):'));
154
- const apiKey = await ask(cyan('2. API key:'));
155
- if (!baseUrl || !apiKey) { console.log(red('Both are required. Run `ineed` again.')); process.exit(1); }
156
- console.log(dim('\nChecking…'));
157
- let models = [];
158
- try {
159
- const res = await fetch(baseUrl.replace(/\/+$/, '') + '/models', { headers: { authorization: `Bearer ${apiKey}` } });
160
- if (res.ok) models = (await res.json()).data?.map(m => m.id).filter(Boolean) ?? [];
161
- } catch {}
162
- if (models.length > 0) console.log(green(`✓ Connected. ${models.length} models found.`));
163
- else console.log(yellow('⚠ Could not list models (some servers hide them). Type a model id from the provider docs.'));
164
- const model = await ask(cyan(`3. Model id ${models.length ? '(e.g. ' + (models.find(m => /gpt-4o-mini|fast|flash/i.test(m)) ?? models[0]) + ')' : ''}:`));
165
- if (!model) { console.log(red('Model is required. Run `ineed` again.')); process.exit(1); }
166
-
167
- console.log(dim('\nSending a test message…'));
168
- try {
169
- const m = await chat({ baseUrl, apiKey, model }, [{ role: 'user', content: 'Reply with exactly: OK' }]);
170
- console.log(green('✓ Works!') + dim(` model replied: ${trunc(m.content ?? '', 40)}`));
171
- } catch (err) {
172
- console.log(yellow(`⚠ Test failed: ${err.message}`));
173
- const keep = await ask(yellow('Save anyway? [y/N]:'));
174
- if (!/^y/i.test(keep)) { console.log(dim('Not saved. Run `ineed` to retry.')); process.exit(1); }
175
- }
176
- saveConfig({ baseUrl, apiKey, model });
177
- console.log(green('\nSaved to ' + CONFIG_FILE));
178
- console.log('All set! Type your goal below.\n');
179
- }
180
-
181
- /* ── main ───────────────────────────────────────────────── */
182
- const args = process.argv.slice(2);
183
- if (args[0] === '--version' || args[0] === '-v') { console.log(`ineed ${VERSION}`); process.exit(0); }
184
- if (args[0] === '--help' || args[0] === '-h') {
185
- console.log(`
186
- ${bold('ineed')} v${VERSION} — your terminal, now autonomous
187
-
188
- ${cyan('ineed')} interactive session here
189
- ${cyan('ineed "do something"')} one-shot task
190
- ${cyan('ineed --reset')} forget saved config and run the wizard again
191
- `);
192
- process.exit(0);
193
- }
194
-
195
- const cwd = process.cwd();
196
- let cfg = loadConfig();
197
- if (args[0] === '--reset') { try { fs.unlinkSync(CONFIG_FILE); } catch {} cfg = null; }
198
-
199
- if (!cfg) { await wizard(); cfg = loadConfig(); }
200
-
201
- if (args.length) {
202
- const { answer } = await runObjective(cfg, args.join(' '), cwd, []);
203
- console.log('\n' + bold(green('Done')) + (answer ? '\n ' + trunc(answer, 500) : ''));
204
- process.exit(0);
205
- }
206
-
207
- console.log(bold(green('ineed')) + dim(` v${VERSION} · ${cfg.model} · ${cwd}`));
208
- console.log(dim('Say what you want. /help for commands. Ctrl+C twice to exit.\n'));
209
-
210
- // main REPL reuses the shared rl above
211
- rl.setPrompt('\n' + bold(green('ineed')) + dim(' ❯ '));
212
- let lastAnswer = '';
213
- const history = [];
214
-
215
- const handleLine = async input => {
216
- if (!input) { rl.prompt(); return; }
217
- if (input === '/exit' || input === '/quit' || input === 'exit') { console.log(dim('Goodbye.')); process.exit(0); }
218
- if (input === '/help') {
219
- console.log(' ' + cyan('/model') + ' show or change model ' + cyan('/reset') + ' redo setup ' + cyan('/exit') + ' quit');
220
- rl.prompt(); return;
221
- }
222
- if (input.startsWith('/model')) {
223
- console.log('Model: ' + bold(cfg.model) + dim(' — change with `ineed --reset` or edit ' + CONFIG_FILE));
224
- rl.prompt(); return;
225
- }
226
- if (input === '/reset') { try { fs.unlinkSync(CONFIG_FILE); } catch {} console.log(dim('Config cleared. Exiting: run `ineed` to set up again.')); process.exit(0); }
227
-
228
- try {
229
- const { answer, history: h } = await runObjective(cfg, input, cwd, history);
230
- history.push(...h);
231
- lastAnswer = answer;
232
- console.log('\n' + bold(green('Done')) + (answer ? '\n ' + answer.split('\n').slice(0, 12).join('\n ') : ''));
233
- } catch (err) {
234
- console.log(red('\nError: ' + err.message));
235
- }
236
- rl.prompt();
237
- };
238
-
239
- wizardMode = false;
240
- replHandler = handleLine;
241
- const backlog = askQueue.splice(0);
242
- for (const line of backlog) await handleLine(line);
243
- rl.prompt();
244
- rl.on('SIGINT', () => { console.log(dim('\nGoodbye.')); process.exit(0); });