ineedcodes 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +82 -84
- package/package.json +25 -16
- package/src/agent.js +106 -0
- package/src/cli.js +93 -0
- package/src/config.js +39 -0
- package/src/provider.js +59 -0
- package/src/session.js +156 -0
- package/src/tools.js +185 -0
- package/src/ui.js +74 -0
- package/src/wizard.js +111 -0
- package/agent.mjs +0 -244
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,122 +1,120 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="https://raw.githubusercontent.com/ineedcodes/ineedcodes/main/assets/logo.svg" alt="ineed" width="320">
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Your terminal, now autonomous.**
|
|
6
|
+
|
|
7
|
+
You say what you want. ineed reads the files, writes the code, runs the commands, checks the output, and fixes what breaks.
|
|
6
8
|
|
|
7
9
|
[](https://www.npmjs.com/package/ineedcodes)
|
|
8
10
|
[](LICENSE)
|
|
9
|
-
[ · [ineed.web.id](https://ineed.web.id) · [Contributing](CONTRIBUTING.md)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](https://github.com/ineedcodes/ineedcodes)
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## What is ineed?
|
|
19
|
-
|
|
20
|
-
A CLI AI agent. You say what you want in plain language. It does the work:
|
|
21
|
-
reads code, runs commands, edits files, uses Git, runs tests, and shows you
|
|
22
|
-
real proof that it worked.
|
|
16
|
+
## Install
|
|
23
17
|
|
|
24
18
|
```bash
|
|
25
|
-
|
|
19
|
+
npm install -g ineedcodes
|
|
26
20
|
```
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
hidden, and success is never claimed without evidence. Watch how it works,
|
|
30
|
-
learn the workflow: that is vibecoding.
|
|
22
|
+
Works on Linux, macOS, and Windows (PowerShell and cmd). Needs Node.js 20 or newer.
|
|
31
23
|
|
|
32
|
-
##
|
|
24
|
+
## First open
|
|
33
25
|
|
|
34
|
-
```bash
|
|
35
|
-
npm install -g ineedcodes
|
|
36
26
|
```
|
|
27
|
+
$ ineed
|
|
37
28
|
|
|
38
|
-
|
|
29
|
+
Welcome to ineed - let's connect you to an AI provider. You only do this once.
|
|
30
|
+
Any OpenAI-compatible API works: OpenAI, OmniRoute, LM Studio, Ollama, vLLM, and more.
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
1. API base URL (example: https://api.openai.com/v1): https://api.openai.com/v1
|
|
33
|
+
2. API key (input hidden): ********
|
|
34
|
+
Connected. 41 models available.
|
|
35
|
+
3. Model id (Enter = gpt-4o-mini):
|
|
36
|
+
Works. Replied: OK
|
|
37
|
+
Saved to ~/.ineedcodes/config.json. You will not be asked again.
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
a model from a live list. Saved once in `~/.ineedcodes/config.json`. Any
|
|
48
|
-
OpenAI-compatible API works: OpenAI, iNeed API, OmniRoute, LM Studio,
|
|
49
|
-
Ollama, vLLM, and more.
|
|
40
|
+
Pick your provider, paste your key, choose a model. Done once, never asked again.
|
|
50
41
|
|
|
51
|
-
## Use
|
|
42
|
+
## Use
|
|
52
43
|
|
|
53
44
|
```bash
|
|
54
|
-
ineed
|
|
55
|
-
ineed "
|
|
56
|
-
ineed --resume # continue your last session
|
|
45
|
+
ineed # session
|
|
46
|
+
ineed "fix the build errors" # one-shot task
|
|
57
47
|
```
|
|
58
48
|
|
|
59
|
-
Then just talk
|
|
49
|
+
Then just talk.
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
ineed › fix the failing tests and tell me what was wrong
|
|
53
|
+
ineed › build me a telegram bot that echoes messages, test it, then commit
|
|
54
|
+
ineed › explain this repository like I am a beginner
|
|
55
|
+
```
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
- *Get this GitHub project running.*
|
|
63
|
-
- *Why is this API failing? Fix it and prove it.*
|
|
64
|
-
- *Make this README sound human.*
|
|
57
|
+
The agent decides what it needs: list files, read code, search, edit, run shell commands, verify the result, and report back with evidence.
|
|
65
58
|
|
|
66
|
-
##
|
|
59
|
+
## Session shortcuts
|
|
67
60
|
|
|
68
|
-
|
|
|
61
|
+
| command | what it does |
|
|
69
62
|
|---|---|
|
|
70
|
-
|
|
|
71
|
-
| `/plan`
|
|
72
|
-
| `/
|
|
73
|
-
| `/
|
|
74
|
-
| `/
|
|
75
|
-
| `/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
> New to AI coding? Free tutorials and a curated list of free AI resources:
|
|
82
|
-
> [ineed.web.id](https://ineed.web.id). No signup, no paywall.
|
|
83
|
-
|
|
84
|
-
## What is inside
|
|
85
|
-
|
|
86
|
-
- **Agent runtime**: an iterative loop: reason, call a tool, observe the real
|
|
87
|
-
result, adapt, verify. Plan mode (read only) and build mode. Reasoning
|
|
88
|
-
effort from low to high.
|
|
89
|
-
- **Tools**: filesystem, search (ripgrep with fallback), shell, Git, web, and
|
|
90
|
-
a native MCP client. Tools take JSON, return JSON.
|
|
91
|
-
- **Memory**: [ICM](https://github.com/rtk-ai/icm), one database per project.
|
|
92
|
-
Conventions, solved errors, and decisions survive across sessions.
|
|
93
|
-
- **Skills**: portable `SKILL.md` files. Built-ins: `boost` (isolated worktree
|
|
94
|
-
plus independent verification) and `humanizer` (natural prose rewriting).
|
|
95
|
-
- **Permissions**: `ask`, `workspace`, or `autonomous`. Destructive, remote,
|
|
96
|
-
and secret operations always need approval.
|
|
97
|
-
- **Secrets**: `.env` files, SSH keys, and tokens never reach model context.
|
|
98
|
-
- **Providers**: any OpenAI-compatible API. No lock-in.
|
|
99
|
-
|
|
100
|
-
## Project layout
|
|
63
|
+
| `/model` | pick a model from your provider |
|
|
64
|
+
| `/plan` | read-only mode, the agent suggests but changes nothing |
|
|
65
|
+
| `/build` | default mode, the agent makes real changes |
|
|
66
|
+
| `/reason` | toggle reasoning effort low/high |
|
|
67
|
+
| `/clear` | forget the current conversation |
|
|
68
|
+
| `/setup` | redo provider setup |
|
|
69
|
+
| `/exit` | quit |
|
|
70
|
+
|
|
71
|
+
Shortcuts are optional. Normal language always works.
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
101
74
|
|
|
102
75
|
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
76
|
+
your words
|
|
77
|
+
|
|
|
78
|
+
objective -> recall context -> pick tool -> real action -> observe result
|
|
79
|
+
| |
|
|
80
|
+
+------------- repeat until verified ----------------------+
|
|
81
|
+
|
|
|
82
|
+
report with evidence
|
|
107
83
|
```
|
|
108
84
|
|
|
109
|
-
|
|
85
|
+
Not a chatbot that prints code. A loop that does the work, checks the results, and shows you the evidence.
|
|
110
86
|
|
|
111
|
-
|
|
112
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
87
|
+
### Tools
|
|
113
88
|
|
|
114
|
-
|
|
89
|
+
| tool | does |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `list_files` | map the project |
|
|
92
|
+
| `read_file` | read code and configs |
|
|
93
|
+
| `search_text` | find anything, fast |
|
|
94
|
+
| `write_file` | create files |
|
|
95
|
+
| `edit_file` | targeted patch, not a rewrite |
|
|
96
|
+
| `delete_file` | remove a file |
|
|
97
|
+
| `shell` | build, test, install, git, anything |
|
|
115
98
|
|
|
116
|
-
|
|
99
|
+
### Safety
|
|
117
100
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
- Secrets (.env, ssh keys, pem files) never enter the model context.
|
|
102
|
+
- Destructive shell commands are refused.
|
|
103
|
+
- Everything is jailed to your working directory.
|
|
104
|
+
- Plan mode lets you preview intent before any change.
|
|
121
105
|
|
|
122
|
-
|
|
106
|
+
## Works with any provider
|
|
107
|
+
|
|
108
|
+
OpenAI, iNeed API, OmniRoute, LM Studio, Ollama, vLLM, Groq, Together, OpenRouter, or anything that speaks the OpenAI format. You bring the key, ineed brings the agent.
|
|
109
|
+
|
|
110
|
+
## For beginners, by a beginner's journey
|
|
111
|
+
|
|
112
|
+
This project exists so anyone can start vibecoding: describe what you want, watch the agent work, learn from what it does. Free tutorials and AI resources live at [ineed.web.id](https://ineed.web.id), the product home is [ineed.codes](https://ineed.codes).
|
|
113
|
+
|
|
114
|
+
## Contributing
|
|
115
|
+
|
|
116
|
+
Issues and PRs are welcome at [github.com/ineedcodes/ineedcodes](https://github.com/ineedcodes/ineedcodes).
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
[MIT](LICENSE) - 0xyas
|
package/package.json
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ineedcodes",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Your terminal, now autonomous. Just say what you want, ineed does the rest.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"ineed": "
|
|
8
|
+
"ineed": "src/cli.js"
|
|
9
9
|
},
|
|
10
|
-
"
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
11
15
|
"engines": {
|
|
12
16
|
"node": ">=20.0.0"
|
|
13
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test/smoke.mjs && node test/pack.mjs"
|
|
20
|
+
},
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "0xyas",
|
|
23
|
+
"email": "arkhanalghozali18@gmail.com",
|
|
24
|
+
"url": "https://ineed.codes"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://ineed.codes",
|
|
14
27
|
"repository": {
|
|
15
|
-
"type": "git"
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/ineedcodes/ineedcodes.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/ineedcodes/ineedcodes/issues"
|
|
16
33
|
},
|
|
17
34
|
"keywords": [
|
|
18
35
|
"ai",
|
|
@@ -21,16 +38,8 @@
|
|
|
21
38
|
"terminal",
|
|
22
39
|
"autonomous",
|
|
23
40
|
"coding",
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"@types/node": "^22.10.2",
|
|
28
|
-
"esbuild": "^0.28.2",
|
|
29
|
-
"typescript": "^5.6.3"
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"agent.mjs",
|
|
33
|
-
"README.md",
|
|
34
|
-
"LICENSE"
|
|
41
|
+
"vibecoding",
|
|
42
|
+
"llm",
|
|
43
|
+
"openai-compatible"
|
|
35
44
|
]
|
|
36
45
|
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// agent.js: the loop. objective -> reason -> tool call -> observe real result -> repeat -> verify -> report.
|
|
2
|
+
|
|
3
|
+
import { chat } from './provider.js';
|
|
4
|
+
import { TOOLS, runTool, shellRun, isDestructive } from './tools.js';
|
|
5
|
+
import { trunc, gray, cyan, dim } from './ui.js';
|
|
6
|
+
|
|
7
|
+
export const MAX_STEPS = 30;
|
|
8
|
+
export const MAX_HISTORY_CHARS = 30_000;
|
|
9
|
+
export const MAX_TURNS = 40;
|
|
10
|
+
|
|
11
|
+
const SYSTEM = `You are ineed, an autonomous terminal agent on the user's machine.
|
|
12
|
+
Rules:
|
|
13
|
+
- Use the tools to do real work. Never invent output. Every success claim needs evidence from a tool result.
|
|
14
|
+
- Prefer targeted edits (edit_file) over full rewrites (write_file). Work only inside the current folder.
|
|
15
|
+
- Never push to remotes or delete data without being asked.
|
|
16
|
+
- Destructive commands are blocked. Ask the user to run those themselves.
|
|
17
|
+
- When the objective is done, verify it (run the tests, read the file back, whatever proves it), then reply with the final result in this shape:
|
|
18
|
+
What changed, what you ran, the evidence you saw.`;
|
|
19
|
+
|
|
20
|
+
export function trimHistory(history) {
|
|
21
|
+
let start = history.length, total = 0;
|
|
22
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
23
|
+
total += (history[i].content?.length ?? 0) + 24;
|
|
24
|
+
if (total > MAX_HISTORY_CHARS) break;
|
|
25
|
+
start = i;
|
|
26
|
+
}
|
|
27
|
+
return history.slice(start);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runObjective(cfg, objective, cwd, history, hooks = {}) {
|
|
31
|
+
const ctrl = new AbortController();
|
|
32
|
+
hooks.onRunStart?.(ctrl);
|
|
33
|
+
const plan = cfg.mode === 'plan';
|
|
34
|
+
const tools = plan ? TOOLS.filter(t => t.allowedInPlan) : TOOLS;
|
|
35
|
+
const messages = [
|
|
36
|
+
{
|
|
37
|
+
role: 'system',
|
|
38
|
+
content: `${SYSTEM}\nWorking directory: ${cwd}\nMode: ${plan ? 'plan (read only, suggest what to change, do not change anything)' : 'build'}`
|
|
39
|
+
},
|
|
40
|
+
...trimHistory(history),
|
|
41
|
+
{ role: 'user', content: objective }
|
|
42
|
+
];
|
|
43
|
+
const changed = new Set();
|
|
44
|
+
const ran = [];
|
|
45
|
+
let answer = '';
|
|
46
|
+
let lastShown = '';
|
|
47
|
+
try {
|
|
48
|
+
for (let step = 0; step < MAX_STEPS; step++) {
|
|
49
|
+
if (ctrl.signal.aborted) break;
|
|
50
|
+
let msg;
|
|
51
|
+
try {
|
|
52
|
+
msg = await chat(cfg, messages, tools, ctrl.signal);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (ctrl.signal.aborted) break;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
messages.push(msg);
|
|
58
|
+
if (msg.content && msg.content !== lastShown) {
|
|
59
|
+
hooks.onText?.(msg.content);
|
|
60
|
+
lastShown = msg.content;
|
|
61
|
+
answer = msg.content;
|
|
62
|
+
}
|
|
63
|
+
const calls = msg.tool_calls ?? [];
|
|
64
|
+
if (calls.length === 0) {
|
|
65
|
+
return { answer, changed: [...changed], ran, aborted: false };
|
|
66
|
+
}
|
|
67
|
+
for (const call of calls) {
|
|
68
|
+
let input = {};
|
|
69
|
+
try { input = JSON.parse(call.function?.arguments || '{}'); } catch {}
|
|
70
|
+
hooks.onTool?.(call.function?.name, input);
|
|
71
|
+
let result;
|
|
72
|
+
if (call.function?.name === 'shell') {
|
|
73
|
+
if (plan) result = { output: 'Refused: plan mode is read only. Switch to build mode with /build.' };
|
|
74
|
+
else if (isDestructive(String(input.command ?? ''))) {
|
|
75
|
+
result = { output: 'Refused: that command is destructive. Run it yourself if you are sure.' };
|
|
76
|
+
} else {
|
|
77
|
+
result = await shellRun(String(input.command ?? ''), cwd, ctrl.signal);
|
|
78
|
+
ran.push(String(input.command ?? '').slice(0, 120));
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (plan && !TOOLS.find(t => t.name === call.function?.name)?.allowedInPlan) {
|
|
82
|
+
result = { output: 'Refused: plan mode is read only. Switch to build mode with /build.' };
|
|
83
|
+
} else {
|
|
84
|
+
result = runTool(call.function?.name, input, cwd);
|
|
85
|
+
if (!plan && ['write_file', 'edit_file', 'delete_file'].includes(call.function?.name)
|
|
86
|
+
&& !/^(Refused|Error)/.test(String(result.output))) {
|
|
87
|
+
changed.add(String(input.path ?? ''));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
hooks.onResult?.(result.output);
|
|
92
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: String(result.output).slice(0, 20_000) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
hooks.onRunEnd?.();
|
|
97
|
+
}
|
|
98
|
+
const stopped = ctrl.signal.aborted;
|
|
99
|
+
return { answer, changed: [...changed], ran, aborted: true, stopped };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function pushTurn(history, objective, result) {
|
|
103
|
+
history.push({ role: 'user', content: objective }, { role: 'assistant', content: result.answer });
|
|
104
|
+
if (history.length > MAX_TURNS * 2) history = history.slice(-MAX_TURNS * 2);
|
|
105
|
+
return history;
|
|
106
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ineed: your terminal, now autonomous.
|
|
3
|
+
// First open asks for provider setup. After that, just say what you want.
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const [MAJOR] = process.versions.node.split('.').map(Number);
|
|
9
|
+
if (!(MAJOR >= 20)) {
|
|
10
|
+
console.error(`ineed needs Node.js 20 or newer. You have ${process.versions.node}.`);
|
|
11
|
+
console.error('Install a newer Node from https://nodejs.org and try again.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { loadConfig } = await import('./config.js');
|
|
16
|
+
const { VERSION, bold, dim, red, green, yellow } = await import('./ui.js');
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
|
|
20
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
21
|
+
console.log(`ineed ${VERSION}`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
26
|
+
console.log(`
|
|
27
|
+
${bold('ineed')} ${dim(`v${VERSION}`)} - your terminal, now autonomous
|
|
28
|
+
|
|
29
|
+
${green('ineed')} interactive session (first open: setup)
|
|
30
|
+
${green('ineed "fix the build errors"')} one-shot task
|
|
31
|
+
${green('ineed --reset')} redo provider setup
|
|
32
|
+
${green('ineed --version')} show version
|
|
33
|
+
|
|
34
|
+
Inside a session, ${dim('/help')} lists the shortcuts. Or just talk to it.
|
|
35
|
+
`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// internal: one-shot worker. Spawned by the branch below, never run by users.
|
|
40
|
+
if (args[0] === '--child') {
|
|
41
|
+
const task = args.slice(1).join(' ');
|
|
42
|
+
const { runObjective } = await import('./agent.js');
|
|
43
|
+
const cfg = loadConfig();
|
|
44
|
+
if (!cfg) {
|
|
45
|
+
console.error(red('No provider configured. Run ') + bold('ineed') + red(' first to set one up.'));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const res = await runObjective(cfg, task, process.cwd(), []);
|
|
50
|
+
if (res.aborted) {
|
|
51
|
+
console.log('\n' + yellow('Stopped.') + dim(' Task did not finish (step limit or Ctrl+C). Re-run to continue.'));
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
console.log('\n' + bold(green('Done')) + (res.changed?.length ? dim(' changed: ' + res.changed.join(', ')) : ''));
|
|
55
|
+
if (res.answer) console.log(' ' + res.answer.split('\n').slice(0, 14).join('\n '));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(red('Error: ' + err.message));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// one-shot task: keep this process alive as the parent, run the worker as a child
|
|
64
|
+
if (args.length > 0 && args[0] !== '--reset') {
|
|
65
|
+
const child = spawn(process.execPath, [new URL(import.meta.url).pathname, '--child', ...args], { stdio: 'inherit' });
|
|
66
|
+
child.on('exit', code => process.exit(code ?? 1));
|
|
67
|
+
} else {
|
|
68
|
+
// interactive session or setup
|
|
69
|
+
const readline = await import('node:readline');
|
|
70
|
+
const { makeInput } = await import('./ui.js');
|
|
71
|
+
const { wizard } = await import('./wizard.js');
|
|
72
|
+
const { startSession } = await import('./session.js');
|
|
73
|
+
const { clearConfig } = await import('./config.js');
|
|
74
|
+
|
|
75
|
+
if (args[0] === '--reset') {
|
|
76
|
+
clearConfig();
|
|
77
|
+
console.log(dim('Setup cleared. Let\'s set it up again.'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let cfg = loadConfig();
|
|
81
|
+
if (!cfg) {
|
|
82
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
83
|
+
const ask = makeInput(rl);
|
|
84
|
+
try {
|
|
85
|
+
cfg = await wizard(ask);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.aborted) { console.log(dim('\nSetup aborted. Run `ineed` to try again.')); process.exit(1); }
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
rl.close();
|
|
91
|
+
}
|
|
92
|
+
await startSession(cfg);
|
|
93
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// config.js: load, validate, save provider config. Secrets never get printed here.
|
|
2
|
+
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const CONFIG_DIR = process.env.INEED_CONFIG_DIR || path.join(os.homedir(), '.ineedcodes');
|
|
8
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
export function loadConfig() {
|
|
11
|
+
try {
|
|
12
|
+
const c = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
13
|
+
if (c && typeof c.baseUrl === 'string' && typeof c.apiKey === 'string' && typeof c.model === 'string'
|
|
14
|
+
&& c.baseUrl && c.model) return normalize(c);
|
|
15
|
+
} catch {}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalize(c) {
|
|
20
|
+
return {
|
|
21
|
+
baseUrl: String(c.baseUrl).replace(/\/+$/, ''),
|
|
22
|
+
apiKey: String(c.apiKey ?? ''),
|
|
23
|
+
model: String(c.model),
|
|
24
|
+
reasoning: c.reasoning === 'high' ? 'high' : 'low',
|
|
25
|
+
mode: c.mode === 'plan' ? 'plan' : 'build'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveConfig(c) {
|
|
30
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
31
|
+
const clean = normalize(c);
|
|
32
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(clean, null, 2) + '\n', { mode: 0o600 });
|
|
33
|
+
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {}
|
|
34
|
+
return clean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearConfig() {
|
|
38
|
+
try { fs.unlinkSync(CONFIG_FILE); } catch {}
|
|
39
|
+
}
|
package/src/provider.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// provider.js: OpenAI-compatible chat + model listing. Timeout and cancellation built in.
|
|
2
|
+
|
|
3
|
+
const HTTP_TIMEOUT = 120_000;
|
|
4
|
+
|
|
5
|
+
async function request(url, opts, signal) {
|
|
6
|
+
const ctrl = new AbortController();
|
|
7
|
+
let timedOut = false;
|
|
8
|
+
const timer = setTimeout(() => { timedOut = true; ctrl.abort(); }, HTTP_TIMEOUT);
|
|
9
|
+
const relay = () => ctrl.abort();
|
|
10
|
+
signal?.addEventListener('abort', relay, { once: true });
|
|
11
|
+
try {
|
|
12
|
+
return await fetch(url, { ...opts, signal: ctrl.signal });
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (signal?.aborted) { const e = new Error('stopped by user'); e.stopped = true; throw e; }
|
|
15
|
+
if (timedOut) throw new Error('request timed out after 120s');
|
|
16
|
+
throw new Error(`cannot reach ${url}: ${err.message}`);
|
|
17
|
+
} finally {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
signal?.removeEventListener('abort', relay);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function fetchModels(cfg, signal) {
|
|
24
|
+
const headers = { accept: 'application/json' };
|
|
25
|
+
if (cfg.apiKey) headers.authorization = `Bearer ${cfg.apiKey}`;
|
|
26
|
+
const res = await request(`${cfg.baseUrl}/models`, { headers }, signal);
|
|
27
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} on GET /models`);
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
const ids = (data.data ?? []).map(m => m.id).filter(Boolean);
|
|
30
|
+
return [...new Set(ids)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function chat(cfg, messages, tools, signal) {
|
|
34
|
+
const body = { model: cfg.model, messages, temperature: 0.2 };
|
|
35
|
+
if (cfg.reasoning === 'high') body.reasoning_effort = 'high';
|
|
36
|
+
if (tools?.length) body.tools = tools.map(t => ({
|
|
37
|
+
type: 'function',
|
|
38
|
+
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
39
|
+
}));
|
|
40
|
+
const headers = { 'content-type': 'application/json' };
|
|
41
|
+
if (cfg.apiKey) headers.authorization = `Bearer ${cfg.apiKey}`;
|
|
42
|
+
const res = await request(`${cfg.baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body) }, signal);
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
// provider does not know reasoning_effort: retry once without it
|
|
46
|
+
if (body.reasoning_effort && (res.status === 400 || res.status === 422)) {
|
|
47
|
+
delete body.reasoning_effort;
|
|
48
|
+
const retry = await request(`${cfg.baseUrl}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(body) }, signal);
|
|
49
|
+
if (!retry.ok) throw new Error(`HTTP ${retry.status}: ${(await retry.text()).slice(0, 200)}`);
|
|
50
|
+
return parseMessage(await retry.json());
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
53
|
+
}
|
|
54
|
+
return parseMessage(await res.json());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseMessage(json) {
|
|
58
|
+
return json.choices?.[0]?.message ?? { role: 'assistant', content: '', tool_calls: [] };
|
|
59
|
+
}
|