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/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
+ }
@@ -0,0 +1,4 @@
1
+ export interface ModelInfo {
2
+ id: string;
3
+ name: string;
4
+ }
@@ -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
+ }