crack-code 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 +15 -0
- package/bun.lock +79 -0
- package/package.json +26 -0
- package/src/agent.ts +104 -0
- package/src/config.ts +410 -0
- package/src/index.ts +329 -0
- package/src/logo/crack-code.ts +13 -0
- package/src/permissions/index.ts +127 -0
- package/src/providers/anthropic.ts +22 -0
- package/src/providers/google.ts +26 -0
- package/src/providers/ollama.ts +33 -0
- package/src/providers/openai.ts +25 -0
- package/src/providers/types.ts +4 -0
- package/src/providers.ts +39 -0
- package/src/repl.ts +284 -0
- package/src/tools/file-read.ts +77 -0
- package/src/tools/file-write.ts +42 -0
- package/src/tools/glob.ts +84 -0
- package/src/tools/registry.ts +63 -0
- package/src/tools/shell.ts +70 -0
- package/src/ui/renderer.ts +208 -0
- package/tsconfig.json +29 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { loadConfig, runSetup, type ConfigOverrides } from "./config.js";
|
|
4
|
+
import { getModel } from "./providers.js";
|
|
5
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
6
|
+
import {
|
|
7
|
+
PermissionManager,
|
|
8
|
+
type PermissionPolicy,
|
|
9
|
+
} from "./permissions/index.js";
|
|
10
|
+
import { readFileTool } from "./tools/file-read.js";
|
|
11
|
+
import { writeFileTool } from "./tools/file-write.js";
|
|
12
|
+
import { runCommandTool } from "./tools/shell.js";
|
|
13
|
+
import { listFilesTool } from "./tools/glob.js";
|
|
14
|
+
import { runAgent } from "./agent.js";
|
|
15
|
+
import { startRepl } from "./repl.js";
|
|
16
|
+
import * as ui from "./ui/renderer.js";
|
|
17
|
+
|
|
18
|
+
// Version
|
|
19
|
+
|
|
20
|
+
const VERSION = "0.1.0";
|
|
21
|
+
|
|
22
|
+
// Help
|
|
23
|
+
|
|
24
|
+
function printHelp(): void {
|
|
25
|
+
console.log(`
|
|
26
|
+
\x1b[1m\x1b[36m🔓 Crack Code\x1b[0m v${VERSION}
|
|
27
|
+
AI-powered security auditor for your codebase.
|
|
28
|
+
|
|
29
|
+
\x1b[1mUsage:\x1b[0m
|
|
30
|
+
crack-code [options] [prompt]
|
|
31
|
+
|
|
32
|
+
\x1b[1mExamples:\x1b[0m
|
|
33
|
+
crack-code Interactive REPL
|
|
34
|
+
crack-code "scan for vulnerabilities" One-shot scan
|
|
35
|
+
crack-code "check src/auth/ for flaws" Scan specific area
|
|
36
|
+
crack-code --allow-edits "fix SQL injection in src/db.ts"
|
|
37
|
+
|
|
38
|
+
\x1b[1mOptions:\x1b[0m
|
|
39
|
+
-i, --interactive Force interactive REPL mode
|
|
40
|
+
--setup Re-run first-time setup wizard
|
|
41
|
+
--allow-edits Enable file writing (read-only by default)
|
|
42
|
+
--provider <name> Override provider (anthropic, openai, google)
|
|
43
|
+
--model <name> Override model
|
|
44
|
+
--key <key> Override API key
|
|
45
|
+
--policy <policy> Permission policy (ask, skip, allow-all, deny-all)
|
|
46
|
+
--scan <glob> Only scan files matching this pattern
|
|
47
|
+
--max-steps <n> Max agent steps (default: 30)
|
|
48
|
+
--max-tokens <n> Max tokens per response (default: 16384)
|
|
49
|
+
-h, --help Show this help
|
|
50
|
+
-v, --version Show version
|
|
51
|
+
|
|
52
|
+
\x1b[1mREPL Commands:\x1b[0m
|
|
53
|
+
/help Show commands /clear Clear history
|
|
54
|
+
/exit Exit /mode Toggle edit mode
|
|
55
|
+
/usage Token usage /model Show model info
|
|
56
|
+
/policy Show/set policy /compact Reduce context size
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Arg Parsing
|
|
61
|
+
|
|
62
|
+
interface ParsedArgs {
|
|
63
|
+
flags: Record<string, string>;
|
|
64
|
+
positional: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
68
|
+
const flags: Record<string, string> = {};
|
|
69
|
+
const positional: string[] = [];
|
|
70
|
+
|
|
71
|
+
const booleanFlags = new Set([
|
|
72
|
+
"help",
|
|
73
|
+
"h",
|
|
74
|
+
"version",
|
|
75
|
+
"v",
|
|
76
|
+
"interactive",
|
|
77
|
+
"i",
|
|
78
|
+
"setup",
|
|
79
|
+
"allow-edits",
|
|
80
|
+
"yolo",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < argv.length; i++) {
|
|
84
|
+
const arg = argv[i]!;
|
|
85
|
+
|
|
86
|
+
if (arg === "--") {
|
|
87
|
+
positional.push(...argv.slice(i + 1));
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (arg.startsWith("--")) {
|
|
92
|
+
const key = arg.slice(2);
|
|
93
|
+
if (booleanFlags.has(key)) {
|
|
94
|
+
flags[key] = "true";
|
|
95
|
+
} else if (i + 1 < argv.length && !argv[i + 1]!.startsWith("--")) {
|
|
96
|
+
flags[key] = argv[++i]!;
|
|
97
|
+
} else {
|
|
98
|
+
flags[key] = "true";
|
|
99
|
+
}
|
|
100
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
101
|
+
const key = arg.slice(1);
|
|
102
|
+
if (booleanFlags.has(key)) {
|
|
103
|
+
flags[key] = "true";
|
|
104
|
+
} else if (i + 1 < argv.length) {
|
|
105
|
+
flags[key] = argv[++i]!;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
positional.push(arg);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { flags, positional };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Tool Registration
|
|
116
|
+
|
|
117
|
+
function registerTools(allowEdits: boolean): ToolRegistry {
|
|
118
|
+
const tools = new ToolRegistry();
|
|
119
|
+
|
|
120
|
+
// Always available — read-only tools
|
|
121
|
+
tools.register(readFileTool);
|
|
122
|
+
tools.register(listFilesTool);
|
|
123
|
+
|
|
124
|
+
// Shell is always available but goes through permission gate
|
|
125
|
+
tools.register(runCommandTool);
|
|
126
|
+
|
|
127
|
+
// Write tools only when edits are enabled
|
|
128
|
+
if (allowEdits) {
|
|
129
|
+
tools.register(writeFileTool);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return tools;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Main
|
|
136
|
+
|
|
137
|
+
async function main(): Promise<void> {
|
|
138
|
+
const { flags, positional } = parseArgs(process.argv.slice(2));
|
|
139
|
+
|
|
140
|
+
// Early exits
|
|
141
|
+
|
|
142
|
+
if (flags.help || flags.h) {
|
|
143
|
+
printHelp();
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (flags.version || flags.v) {
|
|
148
|
+
console.log(`crack-code v${VERSION}`);
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (flags.setup) {
|
|
153
|
+
await runSetup();
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Build config overrides from flags
|
|
158
|
+
|
|
159
|
+
const overrides: ConfigOverrides = {};
|
|
160
|
+
|
|
161
|
+
if (flags.provider) overrides.provider = flags.provider;
|
|
162
|
+
if (flags.model) overrides.model = flags.model;
|
|
163
|
+
if (flags.key) overrides.apiKey = flags.key;
|
|
164
|
+
if (flags["max-tokens"])
|
|
165
|
+
overrides.maxTokens = parseInt(flags["max-tokens"], 10);
|
|
166
|
+
if (flags["max-steps"]) overrides.maxSteps = parseInt(flags["max-steps"], 10);
|
|
167
|
+
if (flags["allow-edits"]) overrides.allowEdits = true;
|
|
168
|
+
if (flags.scan) overrides.scanPatterns = [flags.scan];
|
|
169
|
+
|
|
170
|
+
if (flags.yolo) {
|
|
171
|
+
overrides.permissionPolicy = "allow-all";
|
|
172
|
+
} else if (flags.policy) {
|
|
173
|
+
overrides.permissionPolicy =
|
|
174
|
+
flags.policy as ConfigOverrides["permissionPolicy"];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Load config (may trigger first-run wizard)
|
|
178
|
+
|
|
179
|
+
let config;
|
|
180
|
+
try {
|
|
181
|
+
config = await loadConfig(overrides);
|
|
182
|
+
} catch (e: any) {
|
|
183
|
+
ui.error(e.message);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Create provider model
|
|
188
|
+
|
|
189
|
+
let model;
|
|
190
|
+
try {
|
|
191
|
+
model = getModel(config.provider, config.model, config.apiKey);
|
|
192
|
+
} catch (e: any) {
|
|
193
|
+
ui.error(e.message);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Register tools
|
|
198
|
+
|
|
199
|
+
const tools = registerTools(config.allowEdits);
|
|
200
|
+
|
|
201
|
+
// Create permission manager
|
|
202
|
+
|
|
203
|
+
const permissions = new PermissionManager(config.permissionPolicy);
|
|
204
|
+
|
|
205
|
+
// Route: one-shot vs REPL
|
|
206
|
+
|
|
207
|
+
const hasPrompt = positional.length > 0;
|
|
208
|
+
const forceInteractive = flags.interactive || flags.i;
|
|
209
|
+
const isPiped = !process.stdin.isTTY;
|
|
210
|
+
|
|
211
|
+
if (hasPrompt && !forceInteractive) {
|
|
212
|
+
// One-shot mode
|
|
213
|
+
await runOneShot(positional.join(" "), model, config, tools, permissions);
|
|
214
|
+
} else if (isPiped) {
|
|
215
|
+
// Piped input: cat file.ts | crack-code
|
|
216
|
+
await runPiped(model, config, tools, permissions);
|
|
217
|
+
} else {
|
|
218
|
+
// Interactive REPL
|
|
219
|
+
await startRepl(model, config, tools, permissions);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── One-Shot Mode ──────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async function runOneShot(
|
|
226
|
+
prompt: string,
|
|
227
|
+
model: any,
|
|
228
|
+
config: any,
|
|
229
|
+
tools: ToolRegistry,
|
|
230
|
+
permissions: PermissionManager,
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
ui.newline();
|
|
233
|
+
|
|
234
|
+
const loading = ui.spinner("Analyzing...");
|
|
235
|
+
let firstToken = true;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await runAgent(
|
|
239
|
+
[{ role: "user" as const, content: prompt }],
|
|
240
|
+
{
|
|
241
|
+
model,
|
|
242
|
+
tools,
|
|
243
|
+
permissions,
|
|
244
|
+
systemPrompt: config.systemPrompt,
|
|
245
|
+
maxSteps: config.maxSteps,
|
|
246
|
+
maxTokens: config.maxTokens,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
onText: (delta) => {
|
|
250
|
+
if (firstToken) {
|
|
251
|
+
loading.stop();
|
|
252
|
+
firstToken = false;
|
|
253
|
+
}
|
|
254
|
+
ui.streamText(delta);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
onToolStart: (name, args) => {
|
|
258
|
+
if (firstToken) {
|
|
259
|
+
loading.stop();
|
|
260
|
+
firstToken = false;
|
|
261
|
+
}
|
|
262
|
+
ui.toolStart(name, args);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
onToolEnd: (name, result) => {
|
|
266
|
+
ui.toolEnd(name, result);
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
onUsage: (usage) => {
|
|
270
|
+
ui.newline();
|
|
271
|
+
ui.dim(
|
|
272
|
+
` [${usage.inputTokens} input + ${usage.outputTokens} output = ${usage.totalTokens} tokens]`,
|
|
273
|
+
);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
onError: (err) => {
|
|
277
|
+
loading.stop();
|
|
278
|
+
ui.error(err);
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (firstToken) loading.stop();
|
|
284
|
+
ui.newline();
|
|
285
|
+
} catch (e: any) {
|
|
286
|
+
loading.stop();
|
|
287
|
+
ui.error(e.message);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Piped Mode ─────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
async function runPiped(
|
|
295
|
+
model: any,
|
|
296
|
+
config: any,
|
|
297
|
+
tools: ToolRegistry,
|
|
298
|
+
permissions: PermissionManager,
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
const chunks: string[] = [];
|
|
301
|
+
|
|
302
|
+
const reader = process.stdin as unknown as AsyncIterable<Buffer>;
|
|
303
|
+
for await (const chunk of reader) {
|
|
304
|
+
chunks.push(chunk.toString());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const input = chunks.join("").trim();
|
|
308
|
+
|
|
309
|
+
if (!input) {
|
|
310
|
+
ui.error("No input received from pipe.");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const prompt = [
|
|
315
|
+
"Analyze the following code for security vulnerabilities:\n",
|
|
316
|
+
"```",
|
|
317
|
+
input,
|
|
318
|
+
"```",
|
|
319
|
+
].join("\n");
|
|
320
|
+
|
|
321
|
+
await runOneShot(prompt, model, config, tools, permissions);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── Run ─────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
main().catch((e) => {
|
|
327
|
+
ui.error(e.message ?? String(e));
|
|
328
|
+
process.exit(1);
|
|
329
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function CrackCodeLogo() {
|
|
2
|
+
const version = "0.1.0";
|
|
3
|
+
|
|
4
|
+
return String.raw`
|
|
5
|
+
_________ __ _________ .___
|
|
6
|
+
\_ ___ \____________ ____ | | __ \_ ___ \ ____ __| _/____
|
|
7
|
+
/ \ \/\_ __ \__ \ _/ ___\| |/ / / \ \/ / _ \ / __ |/ __ \
|
|
8
|
+
\ \____| | \// __ \\ \___| < \ \___( <_> ) /_/ \ ___/
|
|
9
|
+
\______ /|__| (____ /\___ >__|_ \ \______ /\____/\____ |\___ >
|
|
10
|
+
\/ \/ \/ \/ \/ \/ \/
|
|
11
|
+
v ${version}
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import * as ui from "../ui/renderer.js";
|
|
3
|
+
|
|
4
|
+
export type PermissionPolicy = "ask" | "skip" | "allow-all" | "deny-all";
|
|
5
|
+
|
|
6
|
+
export class PermissionManager {
|
|
7
|
+
private policy: PermissionPolicy;
|
|
8
|
+
private sessionApprovals = new Set<string>();
|
|
9
|
+
|
|
10
|
+
private readonly readOnlyTools = new Set(["read_file", "list_files"]);
|
|
11
|
+
|
|
12
|
+
constructor(policy: PermissionPolicy = "ask") {
|
|
13
|
+
this.policy = policy;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getPolicy(): PermissionPolicy {
|
|
17
|
+
return this.policy;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setPolicy(policy: PermissionPolicy): void {
|
|
21
|
+
this.policy = policy;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async check(
|
|
25
|
+
toolName: string,
|
|
26
|
+
input: Record<string, unknown>,
|
|
27
|
+
): Promise<boolean> {
|
|
28
|
+
if (this.readOnlyTools.has(toolName)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
switch (this.policy) {
|
|
33
|
+
case "allow-all":
|
|
34
|
+
return true;
|
|
35
|
+
|
|
36
|
+
case "deny-all":
|
|
37
|
+
ui.toolBlocked(toolName, "Blocked by deny-all policy.");
|
|
38
|
+
return false;
|
|
39
|
+
|
|
40
|
+
case "skip":
|
|
41
|
+
return true;
|
|
42
|
+
|
|
43
|
+
case "ask":
|
|
44
|
+
// Check session memory before prompting
|
|
45
|
+
if (this.isSessionApproved(toolName, input)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return this.promptUser(toolName, input);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clearSession(): void {
|
|
53
|
+
this.sessionApprovals.clear();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private isSessionApproved(
|
|
57
|
+
toolName: string,
|
|
58
|
+
input: Record<string, unknown>,
|
|
59
|
+
): boolean {
|
|
60
|
+
// Blanket tool approval (user chose "always")
|
|
61
|
+
if (this.sessionApprovals.has(`tool:${toolName}`)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// Exact action approval (user chose "yes" for this specific call)
|
|
65
|
+
if (
|
|
66
|
+
this.sessionApprovals.has(`exact:${toolName}:${JSON.stringify(input)}`)
|
|
67
|
+
) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async promptUser(
|
|
74
|
+
toolName: string,
|
|
75
|
+
input: Record<string, unknown>,
|
|
76
|
+
): Promise<boolean> {
|
|
77
|
+
const summary = this.summarize(toolName, input);
|
|
78
|
+
ui.permissionPrompt(toolName, summary);
|
|
79
|
+
|
|
80
|
+
const answer = await this.ask(
|
|
81
|
+
"\x1b[33m [y]es / [n]o / [a]lways for this session: \x1b[0m",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const choice = answer.toLowerCase();
|
|
85
|
+
|
|
86
|
+
if (choice === "y" || choice === "yes") {
|
|
87
|
+
// Remember this exact action
|
|
88
|
+
this.sessionApprovals.add(`exact:${toolName}:${JSON.stringify(input)}`);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (choice === "a" || choice === "always") {
|
|
93
|
+
// Remember all future calls to this tool
|
|
94
|
+
this.sessionApprovals.add(`tool:${toolName}`);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Anything else is a deny — empty input, "n", "no", gibberish
|
|
99
|
+
ui.toolBlocked(toolName, "Denied by user.");
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private summarize(toolName: string, input: Record<string, unknown>): string {
|
|
104
|
+
switch (toolName) {
|
|
105
|
+
case "write_file":
|
|
106
|
+
return `Write to ${input.path}`;
|
|
107
|
+
case "run_command":
|
|
108
|
+
return `$ ${input.command}`;
|
|
109
|
+
default:
|
|
110
|
+
const preview = JSON.stringify(input);
|
|
111
|
+
return preview.length > 150 ? preview.slice(0, 150) + "…" : preview;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private ask(question: string): Promise<string> {
|
|
116
|
+
const rl = readline.createInterface({
|
|
117
|
+
input: process.stdin,
|
|
118
|
+
output: process.stdout,
|
|
119
|
+
});
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
rl.question(question, (answer) => {
|
|
122
|
+
rl.close();
|
|
123
|
+
resolve(answer.trim());
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModelInfo } from "./types";
|
|
2
|
+
|
|
3
|
+
export async function fetchAnthropicModels(
|
|
4
|
+
apiKey: string,
|
|
5
|
+
): Promise<ModelInfo[]> {
|
|
6
|
+
const res = await fetch("https://api.anthropic.com/v1/models", {
|
|
7
|
+
headers: {
|
|
8
|
+
"x-api-key": apiKey,
|
|
9
|
+
"anthropic-version": "2023-06-01",
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
14
|
+
|
|
15
|
+
const data = (await res.json()) as {
|
|
16
|
+
data: Array<{ id: string; display_name?: string }>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return data.data
|
|
20
|
+
.map((m) => ({ id: m.id, name: m.display_name ?? m.id }))
|
|
21
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ModelInfo } from "./types";
|
|
2
|
+
|
|
3
|
+
export async function fetchGoogleModels(apiKey: string): Promise<ModelInfo[]> {
|
|
4
|
+
const res = await fetch(
|
|
5
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`,
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
9
|
+
|
|
10
|
+
const data = (await res.json()) as {
|
|
11
|
+
models: Array<{
|
|
12
|
+
name: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
supportedGenerationMethods?: string[];
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return data.models
|
|
19
|
+
.filter((m) => m.supportedGenerationMethods?.includes("generateContent"))
|
|
20
|
+
.map((m) => ({
|
|
21
|
+
// API returns "models/gemini-2.5-flash" → extract "gemini-2.5-flash"
|
|
22
|
+
id: m.name.replace("models/", ""),
|
|
23
|
+
name: m.displayName,
|
|
24
|
+
}))
|
|
25
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ModelInfo } from "./types";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OLLAMA_ENDPOINT = "http://localhost:11434";
|
|
4
|
+
|
|
5
|
+
export async function fetchOllamaModels(
|
|
6
|
+
endpoint?: string,
|
|
7
|
+
): Promise<ModelInfo[]> {
|
|
8
|
+
const base = (endpoint || DEFAULT_OLLAMA_ENDPOINT).replace(/\/+$/, "");
|
|
9
|
+
|
|
10
|
+
const res = await fetch(`${base}/api/tags`);
|
|
11
|
+
|
|
12
|
+
if (!res.ok) throw new Error(`Ollama API ${res.status}: ${await res.text()}`);
|
|
13
|
+
|
|
14
|
+
const data = (await res.json()) as {
|
|
15
|
+
models: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
model: string;
|
|
18
|
+
details?: {
|
|
19
|
+
family?: string;
|
|
20
|
+
parameter_size?: string;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return data.models
|
|
26
|
+
.map((m) => ({
|
|
27
|
+
id: m.name,
|
|
28
|
+
name: m.details?.parameter_size
|
|
29
|
+
? `${m.name} (${m.details.parameter_size})`
|
|
30
|
+
: m.name,
|
|
31
|
+
}))
|
|
32
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModelInfo } from "./types";
|
|
2
|
+
|
|
3
|
+
export async function fetchOpenAIModels(apiKey: string): Promise<ModelInfo[]> {
|
|
4
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
5
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
|
|
9
|
+
|
|
10
|
+
const data = (await res.json()) as { data: Array<{ id: string }> };
|
|
11
|
+
|
|
12
|
+
// Filter to chat models only — skip embeddings, tts, dall-e, whisper, etc.
|
|
13
|
+
const chatPrefixes = ["gpt-", "o1", "o3", "o4", "chatgpt-"];
|
|
14
|
+
const exclude = ["instruct", "realtime", "audio", "search"];
|
|
15
|
+
|
|
16
|
+
return data.data
|
|
17
|
+
.filter((m) => {
|
|
18
|
+
const id = m.id.toLowerCase();
|
|
19
|
+
const hasPrefix = chatPrefixes.some((p) => id.startsWith(p));
|
|
20
|
+
const isExcluded = exclude.some((e) => id.includes(e));
|
|
21
|
+
return hasPrefix && !isExcluded;
|
|
22
|
+
})
|
|
23
|
+
.map((m) => ({ id: m.id, name: m.id }))
|
|
24
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
25
|
+
}
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LanguageModel } from "ai";
|
|
2
|
+
import type { Config } from "./config";
|
|
3
|
+
|
|
4
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
5
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
6
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
7
|
+
import { createOllama } from "ollama-ai-provider-v2";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Takes a provider name, model string, and API key from config
|
|
11
|
+
* and returns an AI SDK LanguageModelV1 that streamText() can use.
|
|
12
|
+
*
|
|
13
|
+
* We use factory functions (createAnthropic, createOpenAI, etc.) instead
|
|
14
|
+
* of the default exports because the user's API key lives in
|
|
15
|
+
* ~/.crack-code/config.json, not necessarily in env vars.
|
|
16
|
+
*/
|
|
17
|
+
export function getModel(
|
|
18
|
+
provider: Config["provider"],
|
|
19
|
+
model: string,
|
|
20
|
+
apiKey: string,
|
|
21
|
+
): LanguageModel {
|
|
22
|
+
switch (provider) {
|
|
23
|
+
case "anthropic":
|
|
24
|
+
return createAnthropic({ apiKey })(model);
|
|
25
|
+
|
|
26
|
+
case "openai":
|
|
27
|
+
return createOpenAI({ apiKey })(model);
|
|
28
|
+
|
|
29
|
+
case "google":
|
|
30
|
+
return createGoogleGenerativeAI({ apiKey })(model);
|
|
31
|
+
|
|
32
|
+
case "ollama":
|
|
33
|
+
// For ollama the "apiKey" field holds the endpoint URL
|
|
34
|
+
return createOllama({ baseURL: apiKey || undefined })(model);
|
|
35
|
+
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unknown provider: "${provider}"`);
|
|
38
|
+
}
|
|
39
|
+
}
|