@olahulleberg/infer 0.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 +72 -0
- package/package.json +28 -0
- package/src/cli.js +760 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# infer
|
|
2
|
+
|
|
3
|
+
A tiny, no‑TUI CLI for asking a quick agentic question from your terminal.
|
|
4
|
+
|
|
5
|
+
> Not a replacement for large agentic CLIs. Use those for workflows, long sessions, and heavy automation. Use `infer` for fast, focused answers with light tool use.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add -g @olahulleberg/infer @mariozechner/pi-coding-agent
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
To get the latest pi without reinstalling infer:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add -g @mariozechner/pi-coding-agent
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Local dev:**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun install
|
|
25
|
+
bun link
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
infer "Summarize this repo"
|
|
32
|
+
infer -c "Continue from last session"
|
|
33
|
+
infer --provider openai --model gpt-4o "Explain this error"
|
|
34
|
+
infer config
|
|
35
|
+
infer config --source models.dev
|
|
36
|
+
echo "What files changed?" | infer
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why use it
|
|
40
|
+
|
|
41
|
+
- Minimal surface area and zero TUI overhead
|
|
42
|
+
- Shows tool actions, then prints the final answer
|
|
43
|
+
- Ideal for short, agentic questions in a shell
|
|
44
|
+
|
|
45
|
+
## How it behaves
|
|
46
|
+
|
|
47
|
+
**Sessions**
|
|
48
|
+
- Default: starts fresh and clears previous sessions
|
|
49
|
+
- Continue: `-c` or `-r`
|
|
50
|
+
- Storage: `~/.infer/agent/sessions/last.jsonl`
|
|
51
|
+
|
|
52
|
+
**Bash approval**
|
|
53
|
+
Every `bash` tool call asks for approval:
|
|
54
|
+
- **Accept**: run once
|
|
55
|
+
- **Reject**: block
|
|
56
|
+
- **Dangerous Accept All**: run all future bash commands in this process
|
|
57
|
+
|
|
58
|
+
**Config & auth**
|
|
59
|
+
- Config dir: `~/.infer/agent` (override with `INFER_AGENT_DIR`)
|
|
60
|
+
- API keys: env vars (e.g. `OPENAI_API_KEY`) or `~/.infer/agent/auth.json`
|
|
61
|
+
- Setup: `infer config`
|
|
62
|
+
|
|
63
|
+
## Flags
|
|
64
|
+
|
|
65
|
+
| Flag | Description |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `-c`, `--continue`, `-r`, `--resume` | Continue last session |
|
|
68
|
+
| `-p`, `--provider <name>` | Model provider |
|
|
69
|
+
| `-m`, `--model <id>` | Model id |
|
|
70
|
+
| `--thinking <level>` | off \| minimal \| low \| medium \| high \| xhigh |
|
|
71
|
+
| `--source <local\|models.dev>` | Model source for `infer config` |
|
|
72
|
+
| `-h`, `--help` | Show help |
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@olahulleberg/infer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal Pi-powered CLI for one-shot prompts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"infer": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@clack/prompts": "^1.0.0",
|
|
14
|
+
"@inquirer/select": "^5.0.4"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@mariozechner/pi-coding-agent": ">=0.52.7"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AuthStorage,
|
|
4
|
+
createAgentSession,
|
|
5
|
+
DefaultResourceLoader,
|
|
6
|
+
ModelRegistry,
|
|
7
|
+
SessionManager,
|
|
8
|
+
SettingsManager,
|
|
9
|
+
} from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
autocomplete,
|
|
12
|
+
confirm,
|
|
13
|
+
intro,
|
|
14
|
+
isCancel,
|
|
15
|
+
note,
|
|
16
|
+
outro,
|
|
17
|
+
password,
|
|
18
|
+
select,
|
|
19
|
+
spinner,
|
|
20
|
+
} from "@clack/prompts";
|
|
21
|
+
import inquirerSelect from "@inquirer/select";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { join, resolve } from "path";
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "fs";
|
|
25
|
+
|
|
26
|
+
const VALID_THINKING_LEVELS = new Set([
|
|
27
|
+
"off",
|
|
28
|
+
"minimal",
|
|
29
|
+
"low",
|
|
30
|
+
"medium",
|
|
31
|
+
"high",
|
|
32
|
+
"xhigh",
|
|
33
|
+
]);
|
|
34
|
+
const DIM = "\x1b[90m";
|
|
35
|
+
const RESET = "\x1b[0m";
|
|
36
|
+
|
|
37
|
+
const COMMAND_NAME = "infer";
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const options = {
|
|
40
|
+
command: "prompt",
|
|
41
|
+
continue: false,
|
|
42
|
+
provider: undefined,
|
|
43
|
+
model: undefined,
|
|
44
|
+
thinking: undefined,
|
|
45
|
+
source: undefined,
|
|
46
|
+
help: false,
|
|
47
|
+
version: false,
|
|
48
|
+
};
|
|
49
|
+
const promptParts = [];
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg === "--") {
|
|
54
|
+
promptParts.push(...args.slice(i + 1));
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (arg === "-c" || arg === "--continue" || arg === "-r" || arg === "--resume") {
|
|
58
|
+
options.continue = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg === "-p" || arg === "--provider") {
|
|
62
|
+
options.provider = requireValue(arg, args[i + 1]);
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === "-m" || arg === "--model") {
|
|
67
|
+
options.model = requireValue(arg, args[i + 1]);
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg === "--thinking") {
|
|
72
|
+
options.thinking = requireValue(arg, args[i + 1]);
|
|
73
|
+
i += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--source") {
|
|
77
|
+
options.source = requireValue(arg, args[i + 1]);
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === "config") {
|
|
82
|
+
options.command = "config";
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg === "-h" || arg === "--help") {
|
|
86
|
+
options.help = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (arg === "-v" || arg === "--version") {
|
|
90
|
+
options.version = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (arg.startsWith("-")) {
|
|
94
|
+
fail(`Unknown flag: ${arg}`);
|
|
95
|
+
}
|
|
96
|
+
promptParts.push(arg);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.help) {
|
|
100
|
+
printHelp();
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (options.version) {
|
|
105
|
+
printVersion();
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.thinking && !VALID_THINKING_LEVELS.has(options.thinking)) {
|
|
110
|
+
fail(`Invalid --thinking value: ${options.thinking}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (options.source && options.source !== "local" && options.source !== "models.dev") {
|
|
114
|
+
fail(`Invalid --source value: ${options.source}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.source && options.command !== "config") {
|
|
118
|
+
fail("--source is only valid with the config command.");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const agentDir = resolveAgentDir();
|
|
122
|
+
const sessionDir = join(agentDir, "sessions");
|
|
123
|
+
const sessionFile = join(sessionDir, "last.jsonl");
|
|
124
|
+
|
|
125
|
+
ensureDir(agentDir);
|
|
126
|
+
ensureDir(sessionDir);
|
|
127
|
+
|
|
128
|
+
const settingsManager = SettingsManager.create(process.cwd(), agentDir);
|
|
129
|
+
const authStorage = new AuthStorage(join(agentDir, "auth.json"));
|
|
130
|
+
const modelRegistry = new ModelRegistry(authStorage, join(agentDir, "models.json"));
|
|
131
|
+
|
|
132
|
+
if (options.command === "config") {
|
|
133
|
+
if (promptParts.length > 0) {
|
|
134
|
+
fail("The config command does not accept a prompt.");
|
|
135
|
+
}
|
|
136
|
+
await runConfigurator({
|
|
137
|
+
agentDir,
|
|
138
|
+
settingsManager,
|
|
139
|
+
authStorage,
|
|
140
|
+
modelRegistry,
|
|
141
|
+
source: options.source,
|
|
142
|
+
});
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const prompt = await resolvePrompt(promptParts);
|
|
147
|
+
if (!prompt) {
|
|
148
|
+
printHelp();
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!options.continue) {
|
|
153
|
+
clearSessions(sessionDir);
|
|
154
|
+
}
|
|
155
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
156
|
+
cwd: process.cwd(),
|
|
157
|
+
agentDir,
|
|
158
|
+
settingsManager,
|
|
159
|
+
extensionFactories: [createBashApprovalExtension()],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await resourceLoader.reload();
|
|
163
|
+
|
|
164
|
+
const model = resolveModel({
|
|
165
|
+
provider: options.provider,
|
|
166
|
+
modelId: options.model,
|
|
167
|
+
settingsManager,
|
|
168
|
+
modelRegistry,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const sessionManager = SessionManager.open(sessionFile, sessionDir);
|
|
172
|
+
const { session } = await createAgentSession({
|
|
173
|
+
cwd: process.cwd(),
|
|
174
|
+
agentDir,
|
|
175
|
+
authStorage,
|
|
176
|
+
modelRegistry,
|
|
177
|
+
resourceLoader,
|
|
178
|
+
settingsManager,
|
|
179
|
+
sessionManager,
|
|
180
|
+
model: model ?? undefined,
|
|
181
|
+
thinkingLevel: options.thinking,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
let lastAssistantText = "";
|
|
185
|
+
let printedToolLine = false;
|
|
186
|
+
let suppressBashToolLine = false;
|
|
187
|
+
|
|
188
|
+
session.subscribe((event) => {
|
|
189
|
+
if (event.type === "tool_execution_start") {
|
|
190
|
+
if (event.toolName === "bash" && suppressBashToolLine) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const line = formatToolLine(event.toolName, event.args);
|
|
194
|
+
if (line) {
|
|
195
|
+
printedToolLine = true;
|
|
196
|
+
process.stdout.write(`${line}\n`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (event.type === "message_end") {
|
|
201
|
+
if (isAssistantMessage(event.message)) {
|
|
202
|
+
lastAssistantText = extractText(event.message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await session.prompt(prompt);
|
|
209
|
+
if (printedToolLine) {
|
|
210
|
+
process.stdout.write("\n");
|
|
211
|
+
}
|
|
212
|
+
if (lastAssistantText) {
|
|
213
|
+
process.stdout.write(`${lastAssistantText.trimEnd()}\n`);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
process.stderr.write(`${message}\n`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
} finally {
|
|
220
|
+
session.dispose();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resolveAgentDir() {
|
|
224
|
+
const envDir = process.env.INFER_AGENT_DIR;
|
|
225
|
+
if (envDir) {
|
|
226
|
+
return expandHome(envDir);
|
|
227
|
+
}
|
|
228
|
+
return join(homedir(), ".infer", "agent");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function expandHome(targetPath) {
|
|
232
|
+
if (targetPath === "~") {
|
|
233
|
+
return homedir();
|
|
234
|
+
}
|
|
235
|
+
if (targetPath.startsWith("~/")) {
|
|
236
|
+
return join(homedir(), targetPath.slice(2));
|
|
237
|
+
}
|
|
238
|
+
return resolve(targetPath);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ensureDir(pathname) {
|
|
242
|
+
if (!existsSync(pathname)) {
|
|
243
|
+
mkdirSync(pathname, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function clearSessions(sessionDirPath) {
|
|
248
|
+
if (!existsSync(sessionDirPath)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (const entry of readdirSync(sessionDirPath)) {
|
|
252
|
+
if (entry.endsWith(".jsonl")) {
|
|
253
|
+
unlinkSync(join(sessionDirPath, entry));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveModel({ provider, modelId, settingsManager, modelRegistry }) {
|
|
259
|
+
if (!provider && !modelId) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const resolvedProvider = provider ?? settingsManager.getDefaultProvider();
|
|
264
|
+
if (!resolvedProvider) {
|
|
265
|
+
fail("Missing provider. Use --provider or set defaultProvider in settings.");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const resolvedModelId = modelId ?? settingsManager.getDefaultModel();
|
|
269
|
+
if (!resolvedModelId) {
|
|
270
|
+
fail("Missing model. Use --model or set defaultModel in settings.");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const model = modelRegistry.find(resolvedProvider, resolvedModelId);
|
|
274
|
+
if (!model) {
|
|
275
|
+
fail(`Model not found: ${resolvedProvider}/${resolvedModelId}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return model;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatToolLine(toolName, args) {
|
|
282
|
+
if (toolName === "read" && args?.path) {
|
|
283
|
+
return gray(`Read ${args.path}`);
|
|
284
|
+
}
|
|
285
|
+
if (toolName === "edit" && args?.path) {
|
|
286
|
+
return gray(`Edit ${args.path}`);
|
|
287
|
+
}
|
|
288
|
+
if (toolName === "write" && args?.path) {
|
|
289
|
+
return gray(`Write ${args.path}`);
|
|
290
|
+
}
|
|
291
|
+
if (toolName === "bash" && args?.command) {
|
|
292
|
+
return gray(`! ${args.command}`);
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isAssistantMessage(message) {
|
|
298
|
+
return typeof message === "object" && message !== null && message.role === "assistant";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function extractText(message) {
|
|
302
|
+
const content = message.content;
|
|
303
|
+
if (typeof content === "string") {
|
|
304
|
+
return content;
|
|
305
|
+
}
|
|
306
|
+
if (!Array.isArray(content)) {
|
|
307
|
+
return "";
|
|
308
|
+
}
|
|
309
|
+
return content
|
|
310
|
+
.filter((block) => block && block.type === "text")
|
|
311
|
+
.map((block) => block.text)
|
|
312
|
+
.join("");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function resolvePrompt(parts) {
|
|
316
|
+
if (parts.length > 0) {
|
|
317
|
+
return parts.join(" ").trim();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (process.stdin.isTTY) {
|
|
321
|
+
return "";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const chunks = [];
|
|
325
|
+
for await (const chunk of process.stdin) {
|
|
326
|
+
chunks.push(chunk);
|
|
327
|
+
}
|
|
328
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function printVersion() {
|
|
332
|
+
const pkg = readPackageJson();
|
|
333
|
+
process.stdout.write(`${COMMAND_NAME} ${pkg.version}\n`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function printHelp() {
|
|
337
|
+
process.stdout.write(
|
|
338
|
+
`Usage: ${COMMAND_NAME} [options] <prompt>\n\n` +
|
|
339
|
+
`Commands:\n` +
|
|
340
|
+
` config Interactive configuration\n\n` +
|
|
341
|
+
`Options:\n` +
|
|
342
|
+
` -c, --continue, -r, --resume Continue last session\n` +
|
|
343
|
+
` -p, --provider <name> Model provider\n` +
|
|
344
|
+
` -m, --model <id> Model id\n` +
|
|
345
|
+
` --thinking <level> off|minimal|low|medium|high|xhigh\n` +
|
|
346
|
+
` --source <local|models.dev> Model source for config\n` +
|
|
347
|
+
` -h, --help Show help\n` +
|
|
348
|
+
` -v, --version Show version\n`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readPackageJson() {
|
|
353
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
354
|
+
return JSON.parse(readFileSync(pkgUrl, "utf-8"));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function requireValue(flag, value) {
|
|
358
|
+
if (!value || value.startsWith("-")) {
|
|
359
|
+
fail(`Missing value for ${flag}`);
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function runConfigurator({ agentDir, settingsManager, authStorage, modelRegistry, source }) {
|
|
365
|
+
intro("infer config");
|
|
366
|
+
|
|
367
|
+
const localCatalog = buildLocalCatalog(modelRegistry);
|
|
368
|
+
if (localCatalog.models.length === 0) {
|
|
369
|
+
outro("No local models found. Check your installation.");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const selectedSource =
|
|
374
|
+
source ??
|
|
375
|
+
(await select({
|
|
376
|
+
message: "Model source",
|
|
377
|
+
options: [
|
|
378
|
+
{ value: "local", label: "Local Pi registry", hint: "Fast, tool-calling models only" },
|
|
379
|
+
{ value: "models.dev", label: "models.dev", hint: "Filtered to models supported here" },
|
|
380
|
+
],
|
|
381
|
+
initialValue: "local",
|
|
382
|
+
}));
|
|
383
|
+
|
|
384
|
+
if (isCancel(selectedSource)) {
|
|
385
|
+
outro("Canceled.");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let catalog = localCatalog;
|
|
390
|
+
if (selectedSource === "models.dev") {
|
|
391
|
+
const spin = spinner();
|
|
392
|
+
spin.start("Fetching models.dev");
|
|
393
|
+
try {
|
|
394
|
+
catalog = await buildModelsDevCatalog(modelRegistry, localCatalog.index);
|
|
395
|
+
spin.stop("Loaded models.dev");
|
|
396
|
+
} catch (error) {
|
|
397
|
+
spin.stop("Failed to load models.dev");
|
|
398
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
399
|
+
note(message, "Using local registry instead");
|
|
400
|
+
catalog = localCatalog;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const reasoningOnly = await confirm({
|
|
405
|
+
message: "Require reasoning support?",
|
|
406
|
+
initialValue: false,
|
|
407
|
+
});
|
|
408
|
+
if (isCancel(reasoningOnly)) {
|
|
409
|
+
outro("Canceled.");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const minContext = await select({
|
|
414
|
+
message: "Minimum context window",
|
|
415
|
+
options: [
|
|
416
|
+
{ value: 0, label: "No minimum" },
|
|
417
|
+
{ value: 32000, label: "32k" },
|
|
418
|
+
{ value: 128000, label: "128k" },
|
|
419
|
+
{ value: 256000, label: "256k" },
|
|
420
|
+
{ value: 1000000, label: "1M" },
|
|
421
|
+
],
|
|
422
|
+
initialValue: 0,
|
|
423
|
+
});
|
|
424
|
+
if (isCancel(minContext)) {
|
|
425
|
+
outro("Canceled.");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const filtered = filterCatalog(catalog.models, {
|
|
430
|
+
reasoningOnly,
|
|
431
|
+
minContext,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (filtered.length === 0) {
|
|
435
|
+
note("No models matched those filters.", "No matches");
|
|
436
|
+
outro("Canceled.");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const providerOptions = buildProviderOptions(filtered);
|
|
441
|
+
const providerId = await autocomplete({
|
|
442
|
+
message: "Provider",
|
|
443
|
+
options: providerOptions,
|
|
444
|
+
maxItems: 12,
|
|
445
|
+
});
|
|
446
|
+
if (isCancel(providerId)) {
|
|
447
|
+
outro("Canceled.");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const modelOptions = buildModelOptions(filtered, providerId);
|
|
452
|
+
if (modelOptions.length === 0) {
|
|
453
|
+
note("No models found for that provider.", "No matches");
|
|
454
|
+
outro("Canceled.");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const modelId = await autocomplete({
|
|
459
|
+
message: "Model",
|
|
460
|
+
options: modelOptions,
|
|
461
|
+
maxItems: 12,
|
|
462
|
+
});
|
|
463
|
+
if (isCancel(modelId)) {
|
|
464
|
+
outro("Canceled.");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const currentThinking = settingsManager.getDefaultThinkingLevel() ?? "off";
|
|
469
|
+
const thinkingLevel = await select({
|
|
470
|
+
message: "Default thinking level",
|
|
471
|
+
options: [
|
|
472
|
+
{ value: "off", label: "off" },
|
|
473
|
+
{ value: "minimal", label: "minimal" },
|
|
474
|
+
{ value: "low", label: "low" },
|
|
475
|
+
{ value: "medium", label: "medium" },
|
|
476
|
+
{ value: "high", label: "high" },
|
|
477
|
+
{ value: "xhigh", label: "xhigh" },
|
|
478
|
+
],
|
|
479
|
+
initialValue: currentThinking,
|
|
480
|
+
});
|
|
481
|
+
if (isCancel(thinkingLevel)) {
|
|
482
|
+
outro("Canceled.");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const saveDefaults = await confirm({
|
|
487
|
+
message: `Save ${providerId}/${modelId} as defaults?`,
|
|
488
|
+
initialValue: true,
|
|
489
|
+
});
|
|
490
|
+
if (isCancel(saveDefaults) || !saveDefaults) {
|
|
491
|
+
outro("No changes saved.");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
settingsManager.setDefaultModelAndProvider(providerId, modelId);
|
|
496
|
+
settingsManager.setDefaultThinkingLevel(thinkingLevel);
|
|
497
|
+
|
|
498
|
+
const storeKey = await confirm({
|
|
499
|
+
message: "Store an API key now?",
|
|
500
|
+
initialValue: false,
|
|
501
|
+
});
|
|
502
|
+
if (isCancel(storeKey)) {
|
|
503
|
+
outro("Saved defaults without API key.");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (storeKey) {
|
|
508
|
+
const apiKey = await password({
|
|
509
|
+
message: "API key (stored in auth.json)",
|
|
510
|
+
mask: "*",
|
|
511
|
+
});
|
|
512
|
+
if (isCancel(apiKey)) {
|
|
513
|
+
outro("Saved defaults without API key.");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (apiKey) {
|
|
517
|
+
authStorage.set(providerId, { type: "api_key", key: apiKey });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
note(
|
|
522
|
+
`Defaults saved to ${join(agentDir, "settings.json")}.\nAuth stored in ${join(
|
|
523
|
+
agentDir,
|
|
524
|
+
"auth.json",
|
|
525
|
+
)}.`,
|
|
526
|
+
"Done",
|
|
527
|
+
);
|
|
528
|
+
outro("Configuration complete.");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function buildLocalCatalog(modelRegistry) {
|
|
532
|
+
const models = modelRegistry.getAll().map((model) => ({
|
|
533
|
+
providerId: model.provider,
|
|
534
|
+
providerName: model.provider,
|
|
535
|
+
modelId: model.id,
|
|
536
|
+
name: model.name ?? model.id,
|
|
537
|
+
reasoning: Boolean(model.reasoning),
|
|
538
|
+
contextWindow: typeof model.contextWindow === "number" ? model.contextWindow : 0,
|
|
539
|
+
maxTokens: typeof model.maxTokens === "number" ? model.maxTokens : 0,
|
|
540
|
+
}));
|
|
541
|
+
return {
|
|
542
|
+
source: "local",
|
|
543
|
+
models,
|
|
544
|
+
index: indexModels(models),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function buildModelsDevCatalog(modelRegistry, localIndex) {
|
|
549
|
+
const response = await fetch("https://models.dev/api.json");
|
|
550
|
+
if (!response.ok) {
|
|
551
|
+
throw new Error(`models.dev request failed: ${response.status}`);
|
|
552
|
+
}
|
|
553
|
+
const data = await response.json();
|
|
554
|
+
const providerAliases = {
|
|
555
|
+
azure: "azure-openai-responses",
|
|
556
|
+
"kimi-for-coding": "kimi-coding",
|
|
557
|
+
vercel: "vercel-ai-gateway",
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const models = [];
|
|
561
|
+
const providers = Object.values(data);
|
|
562
|
+
for (const provider of providers) {
|
|
563
|
+
if (!provider || !provider.id || !provider.models) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const rawProviderId = String(provider.id);
|
|
567
|
+
const providerId = providerAliases[rawProviderId] ?? rawProviderId;
|
|
568
|
+
const providerModels = provider.models;
|
|
569
|
+
if (!localIndex.has(providerId)) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
for (const model of Object.values(providerModels)) {
|
|
573
|
+
if (!model || !model.id) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (model.tool_call === false) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const modelId = String(model.id);
|
|
580
|
+
const localProviderModels = localIndex.get(providerId);
|
|
581
|
+
if (!localProviderModels || !localProviderModels.has(modelId)) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const localModel = localProviderModels.get(modelId);
|
|
585
|
+
models.push({
|
|
586
|
+
providerId,
|
|
587
|
+
providerName: provider.name ?? providerId,
|
|
588
|
+
modelId,
|
|
589
|
+
name: model.name ?? (localModel?.name ?? modelId),
|
|
590
|
+
reasoning: model.reasoning ?? Boolean(localModel?.reasoning),
|
|
591
|
+
contextWindow: resolveNumber(model.limit?.context, localModel?.contextWindow),
|
|
592
|
+
maxTokens: resolveNumber(model.limit?.output, localModel?.maxTokens),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (models.length === 0) {
|
|
598
|
+
return buildLocalCatalog(modelRegistry);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
source: "models.dev",
|
|
603
|
+
models,
|
|
604
|
+
index: indexModels(models),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function indexModels(models) {
|
|
609
|
+
const map = new Map();
|
|
610
|
+
for (const model of models) {
|
|
611
|
+
if (!map.has(model.providerId)) {
|
|
612
|
+
map.set(model.providerId, new Map());
|
|
613
|
+
}
|
|
614
|
+
map.get(model.providerId).set(model.modelId, model);
|
|
615
|
+
}
|
|
616
|
+
return map;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function filterCatalog(models, { reasoningOnly, minContext }) {
|
|
620
|
+
return models.filter((model) => {
|
|
621
|
+
if (reasoningOnly && !model.reasoning) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
if (minContext > 0) {
|
|
625
|
+
return model.contextWindow >= minContext;
|
|
626
|
+
}
|
|
627
|
+
return true;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function buildProviderOptions(models) {
|
|
632
|
+
const counts = new Map();
|
|
633
|
+
const names = new Map();
|
|
634
|
+
for (const model of models) {
|
|
635
|
+
counts.set(model.providerId, (counts.get(model.providerId) ?? 0) + 1);
|
|
636
|
+
if (!names.has(model.providerId)) {
|
|
637
|
+
names.set(model.providerId, model.providerName || model.providerId);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return Array.from(counts.entries())
|
|
641
|
+
.map(([providerId, count]) => ({
|
|
642
|
+
value: providerId,
|
|
643
|
+
label: `${names.get(providerId)} (${providerId})`,
|
|
644
|
+
hint: `${count} models`,
|
|
645
|
+
}))
|
|
646
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function buildModelOptions(models, providerId) {
|
|
650
|
+
const filtered = models.filter((model) => model.providerId === providerId);
|
|
651
|
+
return filtered
|
|
652
|
+
.map((model) => ({
|
|
653
|
+
value: model.modelId,
|
|
654
|
+
label: model.name,
|
|
655
|
+
hint: `id: ${model.modelId} | ctx ${formatContext(model.contextWindow)} | reasoning ${
|
|
656
|
+
model.reasoning ? "yes" : "no"
|
|
657
|
+
}`,
|
|
658
|
+
}))
|
|
659
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function formatContext(value) {
|
|
663
|
+
if (!value || value <= 0) {
|
|
664
|
+
return "n/a";
|
|
665
|
+
}
|
|
666
|
+
if (value >= 1000000) {
|
|
667
|
+
return `${Math.round(value / 100000) / 10}M`;
|
|
668
|
+
}
|
|
669
|
+
if (value >= 1000) {
|
|
670
|
+
return `${Math.round(value / 100) / 10}k`;
|
|
671
|
+
}
|
|
672
|
+
return String(value);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function resolveNumber(value, fallback) {
|
|
676
|
+
if (typeof value === "number" && !Number.isNaN(value)) {
|
|
677
|
+
return value;
|
|
678
|
+
}
|
|
679
|
+
if (typeof fallback === "number" && !Number.isNaN(fallback)) {
|
|
680
|
+
return fallback;
|
|
681
|
+
}
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function gray(text) {
|
|
686
|
+
if (!process.stdout.isTTY) {
|
|
687
|
+
return text;
|
|
688
|
+
}
|
|
689
|
+
return `${DIM}${text}${RESET}`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function createBashApprovalExtension() {
|
|
693
|
+
let allowAll = false;
|
|
694
|
+
|
|
695
|
+
return (pi) => {
|
|
696
|
+
pi.on("tool_call", async (event) => {
|
|
697
|
+
if (event.toolName !== "bash") {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (allowAll) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const command = typeof event.input?.command === "string" ? event.input.command : "";
|
|
706
|
+
if (!process.stdin.isTTY) {
|
|
707
|
+
return { block: true, reason: "Bash command blocked: no TTY for approval." };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
suppressBashToolLine = true;
|
|
711
|
+
let decision;
|
|
712
|
+
try {
|
|
713
|
+
decision = await promptBashApproval(command);
|
|
714
|
+
} finally {
|
|
715
|
+
suppressBashToolLine = false;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (decision === "accept_all") {
|
|
719
|
+
allowAll = true;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (decision === "accept") {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return { block: true, reason: "Bash command rejected by user." };
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function promptBashApproval(command) {
|
|
733
|
+
return inquirerSelect({
|
|
734
|
+
message: gray(`! ${command || "(empty)"}`),
|
|
735
|
+
choices: [
|
|
736
|
+
{ value: "accept", name: "Accept" },
|
|
737
|
+
{ value: "reject", name: "Reject" },
|
|
738
|
+
{ value: "accept_all", name: "Dangerous Accept All" },
|
|
739
|
+
],
|
|
740
|
+
pageSize: 3,
|
|
741
|
+
loop: false,
|
|
742
|
+
theme: {
|
|
743
|
+
prefix: "",
|
|
744
|
+
icon: {
|
|
745
|
+
cursor: ">",
|
|
746
|
+
},
|
|
747
|
+
indexMode: "hidden",
|
|
748
|
+
style: {
|
|
749
|
+
keysHelpTip: () => undefined,
|
|
750
|
+
disabled: (text) => text,
|
|
751
|
+
description: (text) => text,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function fail(message) {
|
|
758
|
+
process.stderr.write(`${message}\n`);
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|