@eldrex/core
Advanced tools
| // src/config/loader.ts | ||
| import { cosmiconfig } from "cosmiconfig"; | ||
| // src/config/schema.ts | ||
| import { z } from "zod"; | ||
| var ProviderSchema = z.object({ | ||
| name: z.string(), | ||
| url: z.string(), | ||
| // e.g. 'ollama://llama3.2:3b', 'openai://gpt-4o-mini' | ||
| apiKey: z.string().optional(), | ||
| maxTokens: z.number().int().positive().optional(), | ||
| priority: z.number().int().nonnegative().default(1), | ||
| maxDailyCost: z.number().nonnegative().optional() | ||
| }); | ||
| var RoutingSchema = z.object({ | ||
| strategy: z.enum(["priority", "cost-aware", "latency"]).default("priority"), | ||
| complexityThreshold: z.number().min(0).max(1).default(0.6), | ||
| localOnly: z.boolean().default(false) | ||
| }); | ||
| var ConfigSchema = z.object({ | ||
| ai: z.object({ | ||
| providers: z.array(ProviderSchema).default([]), | ||
| routing: RoutingSchema.default({}), | ||
| cloudProviders: z.string().optional(), | ||
| allowedCloudRegions: z.array(z.string()).optional() | ||
| }).default({}), | ||
| exclude: z.array(z.string()).default([ | ||
| "node_modules/**", | ||
| "dist/**", | ||
| "package-lock.json", | ||
| "pnpm-lock.yaml", | ||
| "yarn.lock" | ||
| ]), | ||
| cache: z.object({ | ||
| enabled: z.boolean().default(true), | ||
| path: z.string().default(".devdiff/cache.json") | ||
| }).default({}), | ||
| format: z.enum(["markdown", "json", "html"]).default("markdown"), | ||
| privacy: z.record(z.any()).optional(), | ||
| monitoring: z.record(z.any()).optional(), | ||
| security: z.record(z.any()).optional() | ||
| }).passthrough(); | ||
| // src/config/defaults.ts | ||
| var DEFAULTS = { | ||
| ai: { | ||
| providers: [ | ||
| { | ||
| name: "local-ollama", | ||
| url: "ollama://llama3.2:3b", | ||
| priority: 1 | ||
| } | ||
| ], | ||
| routing: { | ||
| strategy: "priority", | ||
| complexityThreshold: 0.6, | ||
| localOnly: true | ||
| } | ||
| }, | ||
| exclude: [ | ||
| "node_modules/**", | ||
| "dist/**", | ||
| "build/**", | ||
| "out/**", | ||
| ".next/**", | ||
| ".nuxt/**", | ||
| "pnpm-lock.yaml", | ||
| "package-lock.json", | ||
| "yarn.lock", | ||
| "*.log", | ||
| ".git/**", | ||
| ".devdiff/**" | ||
| ], | ||
| cache: { | ||
| enabled: true, | ||
| path: ".devdiff/cache.json" | ||
| }, | ||
| format: "markdown" | ||
| }; | ||
| // src/config/loader.ts | ||
| var MODULE_NAME = "devdiff"; | ||
| async function loadConfig(searchFrom) { | ||
| const explorer = cosmiconfig(MODULE_NAME, { | ||
| searchPlaces: [ | ||
| `.${MODULE_NAME}rc`, | ||
| `.${MODULE_NAME}rc.json`, | ||
| `.${MODULE_NAME}rc.yaml`, | ||
| `.${MODULE_NAME}rc.yml`, | ||
| `.${MODULE_NAME}rc.js`, | ||
| `.${MODULE_NAME}rc.mjs`, | ||
| `.${MODULE_NAME}rc.cjs`, | ||
| `.${MODULE_NAME}.config.js`, | ||
| `.${MODULE_NAME}.config.mjs`, | ||
| `.${MODULE_NAME}.config.cjs`, | ||
| `${MODULE_NAME}.config.js`, | ||
| `${MODULE_NAME}.config.mjs`, | ||
| `${MODULE_NAME}.config.cjs`, | ||
| "package.json" | ||
| ] | ||
| }); | ||
| try { | ||
| const result = await explorer.search(searchFrom); | ||
| if (!result || !result.config) { | ||
| return DEFAULTS; | ||
| } | ||
| const parsed = ConfigSchema.parse(result.config); | ||
| return { | ||
| ...DEFAULTS, | ||
| ...parsed, | ||
| ai: { | ||
| ...DEFAULTS.ai, | ||
| ...parsed.ai, | ||
| routing: { | ||
| ...DEFAULTS.ai.routing, | ||
| ...parsed.ai?.routing | ||
| }, | ||
| providers: parsed.ai?.providers?.length ? parsed.ai.providers : DEFAULTS.ai.providers | ||
| }, | ||
| cache: { | ||
| ...DEFAULTS.cache, | ||
| ...parsed.cache | ||
| } | ||
| }; | ||
| } catch (error) { | ||
| console.warn( | ||
| "Failed to load DevDiff configuration, falling back to defaults:", | ||
| error | ||
| ); | ||
| return DEFAULTS; | ||
| } | ||
| } | ||
| export { | ||
| DEFAULTS, | ||
| loadConfig | ||
| }; | ||
| //# sourceMappingURL=chunk-3PFQZWQS.js.map |
| {"version":3,"sources":["../src/config/loader.ts","../src/config/schema.ts","../src/config/defaults.ts"],"sourcesContent":["import { cosmiconfig } from \"cosmiconfig\";\nimport { ConfigSchema, DevDiffConfig } from \"./schema\";\nimport { DEFAULTS } from \"./defaults\";\n\nconst MODULE_NAME = \"devdiff\";\n\nexport async function loadConfig(searchFrom?: string): Promise<DevDiffConfig> {\n const explorer = cosmiconfig(MODULE_NAME, {\n searchPlaces: [\n `.${MODULE_NAME}rc`,\n `.${MODULE_NAME}rc.json`,\n `.${MODULE_NAME}rc.yaml`,\n `.${MODULE_NAME}rc.yml`,\n `.${MODULE_NAME}rc.js`,\n `.${MODULE_NAME}rc.mjs`,\n `.${MODULE_NAME}rc.cjs`,\n `.${MODULE_NAME}.config.js`,\n `.${MODULE_NAME}.config.mjs`,\n `.${MODULE_NAME}.config.cjs`,\n `${MODULE_NAME}.config.js`,\n `${MODULE_NAME}.config.mjs`,\n `${MODULE_NAME}.config.cjs`,\n \"package.json\",\n ],\n });\n\n try {\n const result = await explorer.search(searchFrom);\n if (!result || !result.config) {\n return DEFAULTS;\n }\n\n // Parse and validate config\n const parsed = ConfigSchema.parse(result.config);\n\n // Deep merge with defaults for any missing nested structures\n return {\n ...DEFAULTS,\n ...parsed,\n ai: {\n ...DEFAULTS.ai,\n ...parsed.ai,\n routing: {\n ...DEFAULTS.ai.routing,\n ...parsed.ai?.routing,\n },\n providers: parsed.ai?.providers?.length\n ? parsed.ai.providers\n : DEFAULTS.ai.providers,\n },\n cache: {\n ...DEFAULTS.cache,\n ...parsed.cache,\n },\n };\n } catch (error) {\n console.warn(\n \"Failed to load DevDiff configuration, falling back to defaults:\",\n error,\n );\n return DEFAULTS;\n }\n}\n","import { z } from \"zod\";\n\nexport const ProviderSchema = z.object({\n name: z.string(),\n url: z.string(), // e.g. 'ollama://llama3.2:3b', 'openai://gpt-4o-mini'\n apiKey: z.string().optional(),\n maxTokens: z.number().int().positive().optional(),\n priority: z.number().int().nonnegative().default(1),\n maxDailyCost: z.number().nonnegative().optional(),\n});\n\nexport const RoutingSchema = z.object({\n strategy: z.enum([\"priority\", \"cost-aware\", \"latency\"]).default(\"priority\"),\n complexityThreshold: z.number().min(0).max(1).default(0.6),\n localOnly: z.boolean().default(false),\n});\n\nexport const ConfigSchema = z\n .object({\n ai: z\n .object({\n providers: z.array(ProviderSchema).default([]),\n routing: RoutingSchema.default({}),\n cloudProviders: z.string().optional(),\n allowedCloudRegions: z.array(z.string()).optional(),\n })\n .default({}),\n exclude: z\n .array(z.string())\n .default([\n \"node_modules/**\",\n \"dist/**\",\n \"package-lock.json\",\n \"pnpm-lock.yaml\",\n \"yarn.lock\",\n ]),\n cache: z\n .object({\n enabled: z.boolean().default(true),\n path: z.string().default(\".devdiff/cache.json\"),\n })\n .default({}),\n format: z.enum([\"markdown\", \"json\", \"html\"]).default(\"markdown\"),\n privacy: z.record(z.any()).optional(),\n monitoring: z.record(z.any()).optional(),\n security: z.record(z.any()).optional(),\n })\n .passthrough();\n\nexport type Provider = z.infer<typeof ProviderSchema>;\nexport type Routing = z.infer<typeof RoutingSchema>;\nexport type DevDiffConfig = z.infer<typeof ConfigSchema> & {\n privacy?: Record<string, any>;\n monitoring?: Record<string, any>;\n security?: Record<string, any>;\n};\n","import { DevDiffConfig } from \"./schema\";\n\nexport const DEFAULTS: DevDiffConfig = {\n ai: {\n providers: [\n {\n name: \"local-ollama\",\n url: \"ollama://llama3.2:3b\",\n priority: 1,\n },\n ],\n routing: {\n strategy: \"priority\",\n complexityThreshold: 0.6,\n localOnly: true,\n },\n },\n exclude: [\n \"node_modules/**\",\n \"dist/**\",\n \"build/**\",\n \"out/**\",\n \".next/**\",\n \".nuxt/**\",\n \"pnpm-lock.yaml\",\n \"package-lock.json\",\n \"yarn.lock\",\n \"*.log\",\n \".git/**\",\n \".devdiff/**\",\n ],\n cache: {\n enabled: true,\n path: \".devdiff/cache.json\",\n },\n format: \"markdown\",\n};\n"],"mappings":";AAAA,SAAS,mBAAmB;;;ACA5B,SAAS,SAAS;AAEX,IAAM,iBAAiB,EAAE,OAAO;AAAA,EACrC,MAAM,EAAE,OAAO;AAAA,EACf,KAAK,EAAE,OAAO;AAAA;AAAA,EACd,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EAChD,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;AAAA,EAClD,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS;AAClD,CAAC;AAEM,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,UAAU,EAAE,KAAK,CAAC,YAAY,cAAc,SAAS,CAAC,EAAE,QAAQ,UAAU;AAAA,EAC1E,qBAAqB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG;AAAA,EACzD,WAAW,EAAE,QAAQ,EAAE,QAAQ,KAAK;AACtC,CAAC;AAEM,IAAM,eAAe,EACzB,OAAO;AAAA,EACN,IAAI,EACD,OAAO;AAAA,IACN,WAAW,EAAE,MAAM,cAAc,EAAE,QAAQ,CAAC,CAAC;AAAA,IAC7C,SAAS,cAAc,QAAQ,CAAC,CAAC;AAAA,IACjC,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,IACpC,qBAAqB,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,EACpD,CAAC,EACA,QAAQ,CAAC,CAAC;AAAA,EACb,SAAS,EACN,MAAM,EAAE,OAAO,CAAC,EAChB,QAAQ;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAAA,EACH,OAAO,EACJ,OAAO;AAAA,IACN,SAAS,EAAE,QAAQ,EAAE,QAAQ,IAAI;AAAA,IACjC,MAAM,EAAE,OAAO,EAAE,QAAQ,qBAAqB;AAAA,EAChD,CAAC,EACA,QAAQ,CAAC,CAAC;AAAA,EACb,QAAQ,EAAE,KAAK,CAAC,YAAY,QAAQ,MAAM,CAAC,EAAE,QAAQ,UAAU;AAAA,EAC/D,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACpC,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACvC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AACvC,CAAC,EACA,YAAY;;;AC7CR,IAAM,WAA0B;AAAA,EACrC,IAAI;AAAA,IACF,WAAW;AAAA,MACT;AAAA,QACE,MAAM;AAAA,QACN,KAAK;AAAA,QACL,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP,UAAU;AAAA,MACV,qBAAqB;AAAA,MACrB,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,OAAO;AAAA,IACL,SAAS;AAAA,IACT,MAAM;AAAA,EACR;AAAA,EACA,QAAQ;AACV;;;AFhCA,IAAM,cAAc;AAEpB,eAAsB,WAAW,YAA6C;AAC5E,QAAM,WAAW,YAAY,aAAa;AAAA,IACxC,cAAc;AAAA,MACZ,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,IAAI,WAAW;AAAA,MACf,GAAG,WAAW;AAAA,MACd,GAAG,WAAW;AAAA,MACd,GAAG,WAAW;AAAA,MACd;AAAA,IACF;AAAA,EACF,CAAC;AAED,MAAI;AACF,UAAM,SAAS,MAAM,SAAS,OAAO,UAAU;AAC/C,QAAI,CAAC,UAAU,CAAC,OAAO,QAAQ;AAC7B,aAAO;AAAA,IACT;AAGA,UAAM,SAAS,aAAa,MAAM,OAAO,MAAM;AAG/C,WAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,IAAI;AAAA,QACF,GAAG,SAAS;AAAA,QACZ,GAAG,OAAO;AAAA,QACV,SAAS;AAAA,UACP,GAAG,SAAS,GAAG;AAAA,UACf,GAAG,OAAO,IAAI;AAAA,QAChB;AAAA,QACA,WAAW,OAAO,IAAI,WAAW,SAC7B,OAAO,GAAG,YACV,SAAS,GAAG;AAAA,MAClB;AAAA,MACA,OAAO;AAAA,QACL,GAAG,SAAS;AAAA,QACZ,GAAG,OAAO;AAAA,MACZ;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;","names":[]} |
| // src/context/compiler.ts | ||
| import * as fs2 from "fs/promises"; | ||
| import * as path2 from "path"; | ||
| // src/diff/secret-scanner.ts | ||
| var SECRET_PATTERNS = [ | ||
| { | ||
| name: "Generic API Key / Token", | ||
| type: "generic-api-key", | ||
| // Matches patterns like apiKey = "...", token: "..." | ||
| regex: /(api[_-]?key|secret|token|password|passwd|auth[_-]?key|private[_-]?key|passphrase)\s*[:=]\s*['"`]([A-Za-z0-9%_-]{8,})['"`]/gi, | ||
| severity: "high" | ||
| }, | ||
| { | ||
| name: "OpenAI API Key", | ||
| type: "openai-api-key", | ||
| regex: /sk-[a-zA-Z0-9-]{20,}/g, | ||
| severity: "critical" | ||
| }, | ||
| { | ||
| name: "Anthropic API Key", | ||
| type: "anthropic-api-key", | ||
| regex: /sk-ant-[a-zA-Z0-9-]{20,}/g, | ||
| severity: "critical" | ||
| }, | ||
| { | ||
| name: "Google Gemini API Key", | ||
| type: "gemini-api-key", | ||
| regex: /AIzaSy[a-zA-Z0-9_-]{33}/g, | ||
| severity: "critical" | ||
| }, | ||
| { | ||
| name: "Slack Webhook / Token", | ||
| type: "slack-webhook", | ||
| regex: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9]+\/[A-Za-z0-9]+\/[A-Za-z0-9]+/g, | ||
| severity: "high" | ||
| }, | ||
| { | ||
| name: "AWS Access Key ID / Secret", | ||
| type: "aws-access-key", | ||
| regex: /(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/g, | ||
| severity: "high" | ||
| }, | ||
| { | ||
| name: "Connection String", | ||
| type: "connection-string", | ||
| regex: /(mongodb\+srv|mongodb|postgres|postgresql|mysql|redis|sqlite):\/\/[^:\s]+:[^@\s]+@[^@\s]+/gi, | ||
| severity: "high" | ||
| }, | ||
| { | ||
| name: "Private Key PEM Block", | ||
| type: "private-key", | ||
| regex: /-----BEGIN (RSA |EC |PGP |)?PRIVATE KEY-----[\s\S]+?-----END (RSA |EC |PGP |)?PRIVATE KEY-----/g, | ||
| severity: "high" | ||
| }, | ||
| { | ||
| name: "JWT Token", | ||
| type: "jwt-token", | ||
| regex: /eyJhbGciOi[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/g, | ||
| severity: "medium" | ||
| } | ||
| ]; | ||
| var SecretScanner = class { | ||
| customPatterns = []; | ||
| constructor(config) { | ||
| if (config?.security?.customSecretPatterns) { | ||
| for (const [index, p] of config.security.customSecretPatterns.entries()) { | ||
| try { | ||
| this.customPatterns.push({ | ||
| name: `Custom Pattern ${index + 1}`, | ||
| type: `custom-secret-${index + 1}`, | ||
| regex: new RegExp(p, "g"), | ||
| severity: "high" | ||
| }); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| } | ||
| scan(content) { | ||
| if (!content) return []; | ||
| const findings = []; | ||
| const allPatterns = [...SECRET_PATTERNS, ...this.customPatterns]; | ||
| for (const pattern of allPatterns) { | ||
| pattern.regex.lastIndex = 0; | ||
| if (pattern.regex.test(content)) { | ||
| findings.push({ | ||
| type: pattern.type, | ||
| name: pattern.name, | ||
| severity: pattern.severity | ||
| }); | ||
| } | ||
| } | ||
| return findings; | ||
| } | ||
| redact(content) { | ||
| if (!content) return ""; | ||
| let redacted = content; | ||
| const allPatterns = [...SECRET_PATTERNS, ...this.customPatterns]; | ||
| for (const pattern of allPatterns) { | ||
| pattern.regex.lastIndex = 0; | ||
| if (pattern.type === "generic-api-key") { | ||
| redacted = redacted.replace(pattern.regex, (match, prefix, secret) => { | ||
| const secretIndex = match.lastIndexOf(secret); | ||
| const prefixPart = match.substring(0, secretIndex); | ||
| const suffixPart = match.substring(secretIndex + secret.length); | ||
| return `${prefixPart}[REDACTED:${pattern.type}]${suffixPart}`; | ||
| }); | ||
| } else { | ||
| redacted = redacted.replace( | ||
| pattern.regex, | ||
| `[REDACTED:${pattern.type}]` | ||
| ); | ||
| } | ||
| } | ||
| return redacted; | ||
| } | ||
| }; | ||
| function redactSecrets(content) { | ||
| const scanner = new SecretScanner(); | ||
| let redacted = content; | ||
| for (const pattern of SECRET_PATTERNS) { | ||
| if (pattern.type === "generic-api-key") { | ||
| redacted = redacted.replace(pattern.regex, (match, prefix, secret) => { | ||
| const secretIndex = match.lastIndexOf(secret); | ||
| const prefixPart = match.substring(0, secretIndex); | ||
| const suffixPart = match.substring(secretIndex + secret.length); | ||
| return `${prefixPart}[REDACTED]${suffixPart}`; | ||
| }); | ||
| } else { | ||
| redacted = redacted.replace(pattern.regex, "[REDACTED]"); | ||
| } | ||
| } | ||
| return redacted; | ||
| } | ||
| function scanForSecrets(content) { | ||
| const scanner = new SecretScanner(); | ||
| return scanner.scan(content).map((f) => f.name); | ||
| } | ||
| // src/context/scanner.ts | ||
| import * as fs from "fs/promises"; | ||
| import * as path from "path"; | ||
| var FRAMEWORK_SIGNATURES = { | ||
| next: "Next.js (React SSR/SSG)", | ||
| react: "React", | ||
| vue: "Vue.js", | ||
| svelte: "Svelte", | ||
| angular: "@angular/core", | ||
| express: "Express (Node.js HTTP server)", | ||
| fastify: "Fastify (Node.js HTTP server)", | ||
| koa: "Koa (Node.js HTTP server)", | ||
| hono: "Hono (edge-first HTTP framework)", | ||
| nestjs: "NestJS (@nestjs/core)", | ||
| "@nestjs/core": "NestJS", | ||
| vite: "Vite", | ||
| astro: "Astro", | ||
| nuxt: "Nuxt.js", | ||
| remix: "Remix", | ||
| electron: "Electron (desktop app)", | ||
| tauri: "@tauri-apps/api", | ||
| "@tauri-apps/api": "Tauri (desktop app)", | ||
| prisma: "Prisma ORM", | ||
| drizzle: "Drizzle ORM", | ||
| mongoose: "Mongoose (MongoDB ODM)", | ||
| typeorm: "TypeORM", | ||
| trpc: "@trpc/server", | ||
| "@trpc/server": "tRPC", | ||
| graphql: "GraphQL", | ||
| "@apollo/server": "Apollo GraphQL Server" | ||
| }; | ||
| var DIR_PURPOSE = { | ||
| src: "main source code", | ||
| lib: "library/shared code", | ||
| app: "application entry & routing", | ||
| api: "API routes/handlers", | ||
| server: "server-side code", | ||
| client: "client-side code", | ||
| pages: "page components (Next.js/Nuxt style)", | ||
| components: "reusable UI components", | ||
| hooks: "custom React hooks", | ||
| utils: "utility functions", | ||
| helpers: "helper functions", | ||
| services: "business logic / service layer", | ||
| repositories: "data access layer", | ||
| models: "data models / entities", | ||
| schemas: "data schemas / validation", | ||
| controllers: "MVC controllers", | ||
| routes: "HTTP route definitions", | ||
| middleware: "middleware functions", | ||
| handlers: "request handlers", | ||
| queries: "database queries / CQRS queries", | ||
| commands: "CQRS commands / CLI commands", | ||
| config: "configuration files", | ||
| types: "TypeScript type definitions", | ||
| interfaces: "TypeScript interfaces", | ||
| constants: "application constants", | ||
| migrations: "database migrations", | ||
| seeds: "database seed data", | ||
| scripts: "build/automation scripts", | ||
| tests: "test suites", | ||
| test: "test suites", | ||
| __tests__: "test suites (Jest convention)", | ||
| spec: "test specs", | ||
| e2e: "end-to-end tests", | ||
| packages: "monorepo packages", | ||
| plugins: "plugins / extensions", | ||
| adapters: "adapter pattern implementations", | ||
| providers: "dependency injection providers", | ||
| store: "state management (Redux/Zustand/Pinia)", | ||
| assets: "static assets", | ||
| public: "publicly served files", | ||
| styles: "CSS/SCSS stylesheets", | ||
| i18n: "internationalization strings", | ||
| locales: "locale files", | ||
| docs: "documentation" | ||
| }; | ||
| var SKIP_DIRS = /* @__PURE__ */ new Set([ | ||
| "node_modules", | ||
| ".git", | ||
| "dist", | ||
| "build", | ||
| ".turbo", | ||
| ".next", | ||
| ".nuxt", | ||
| ".svelte-kit", | ||
| "coverage", | ||
| ".cache", | ||
| "__pycache__", | ||
| ".vitepress", | ||
| ".changeset", | ||
| "tmp", | ||
| "temp" | ||
| ]); | ||
| var ENTRY_POINT_NAMES = [ | ||
| "index.ts", | ||
| "index.js", | ||
| "index.mjs", | ||
| "main.ts", | ||
| "main.js", | ||
| "app.ts", | ||
| "app.js", | ||
| "app.tsx", | ||
| "server.ts", | ||
| "server.js" | ||
| ]; | ||
| var ProjectContextScanner = class { | ||
| repoPath; | ||
| constructor(repoPath) { | ||
| this.repoPath = repoPath; | ||
| } | ||
| async scan() { | ||
| const [pkgInfo, readmePurpose, topDirs] = await Promise.all([ | ||
| this.readPackageJson(), | ||
| this.readReadme(), | ||
| this.listTopLevelDirs() | ||
| ]); | ||
| const entryPoints = await this.findEntryPoints(topDirs.rawDirs); | ||
| const techStack = this.buildTechStack( | ||
| pkgInfo.dependencies, | ||
| pkgInfo.devDependencies, | ||
| pkgInfo.lang | ||
| ); | ||
| return { | ||
| projectName: pkgInfo.name || path.basename(this.repoPath), | ||
| purpose: pkgInfo.description || readmePurpose, | ||
| techStack, | ||
| architecture: topDirs.architecture, | ||
| entryPoints, | ||
| keyConcepts: [] | ||
| }; | ||
| } | ||
| async readPackageJson() { | ||
| try { | ||
| const raw = await fs.readFile( | ||
| path.join(this.repoPath, "package.json"), | ||
| "utf-8" | ||
| ); | ||
| const pkg = JSON.parse(raw); | ||
| return { | ||
| name: pkg.name || "", | ||
| description: pkg.description || "", | ||
| dependencies: pkg.dependencies || {}, | ||
| devDependencies: pkg.devDependencies || {}, | ||
| lang: "TypeScript/JavaScript" | ||
| }; | ||
| } catch { | ||
| const lang = await this.detectLanguage(); | ||
| return { | ||
| name: "", | ||
| description: "", | ||
| dependencies: {}, | ||
| devDependencies: {}, | ||
| lang | ||
| }; | ||
| } | ||
| } | ||
| async detectLanguage() { | ||
| const checks = [ | ||
| ["Cargo.toml", "Rust"], | ||
| ["go.mod", "Go"], | ||
| ["pyproject.toml", "Python"], | ||
| ["requirements.txt", "Python"], | ||
| ["pom.xml", "Java (Maven)"], | ||
| ["build.gradle", "Java/Kotlin (Gradle)"], | ||
| ["composer.json", "PHP"], | ||
| ["Gemfile", "Ruby"] | ||
| ]; | ||
| for (const [file, lang] of checks) { | ||
| try { | ||
| await fs.access(path.join(this.repoPath, file)); | ||
| return lang; | ||
| } catch { | ||
| } | ||
| } | ||
| return "Unknown"; | ||
| } | ||
| async readReadme() { | ||
| const candidates = ["README.md", "readme.md", "README.txt", "README"]; | ||
| for (const name of candidates) { | ||
| try { | ||
| const content = await fs.readFile( | ||
| path.join(this.repoPath, name), | ||
| "utf-8" | ||
| ); | ||
| const lines = content.split("\n"); | ||
| const paragraphs = []; | ||
| let wordCount = 0; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("![")) { | ||
| continue; | ||
| } | ||
| if (trimmed.startsWith("[![")) continue; | ||
| const words = trimmed.split(/\s+/); | ||
| paragraphs.push(trimmed); | ||
| wordCount += words.length; | ||
| if (wordCount >= 80) break; | ||
| } | ||
| const excerpt = paragraphs.join(" ").substring(0, 500); | ||
| return excerpt || ""; | ||
| } catch { | ||
| } | ||
| } | ||
| return ""; | ||
| } | ||
| async listTopLevelDirs() { | ||
| try { | ||
| const entries = await fs.readdir(this.repoPath, { withFileTypes: true }); | ||
| const dirs = entries.filter( | ||
| (e) => e.isDirectory() && !SKIP_DIRS.has(e.name) && !e.name.startsWith(".") | ||
| ).map((e) => e.name).sort(); | ||
| const architecture = []; | ||
| for (const dir of dirs) { | ||
| const purpose = DIR_PURPOSE[dir.toLowerCase()]; | ||
| if (purpose) { | ||
| architecture.push({ path: dir + "/", description: purpose }); | ||
| } else { | ||
| architecture.push({ | ||
| path: dir + "/", | ||
| description: "project directory" | ||
| }); | ||
| } | ||
| } | ||
| const meaningful = architecture.filter( | ||
| (a) => a.description !== "project directory" | ||
| ); | ||
| const others = architecture.filter( | ||
| (a) => a.description === "project directory" | ||
| ); | ||
| return { | ||
| architecture: [...meaningful.slice(0, 10), ...others.slice(0, 2)], | ||
| rawDirs: dirs | ||
| }; | ||
| } catch { | ||
| return { architecture: [], rawDirs: [] }; | ||
| } | ||
| } | ||
| async findEntryPoints(topDirs) { | ||
| const found = []; | ||
| for (const name of ENTRY_POINT_NAMES) { | ||
| try { | ||
| await fs.access(path.join(this.repoPath, name)); | ||
| found.push(name); | ||
| } catch { | ||
| } | ||
| } | ||
| const sourceDirs = topDirs.filter( | ||
| (d) => ["src", "app", "lib", "packages"].includes(d) | ||
| ); | ||
| for (const dir of sourceDirs.slice(0, 3)) { | ||
| for (const name of ENTRY_POINT_NAMES) { | ||
| try { | ||
| await fs.access(path.join(this.repoPath, dir, name)); | ||
| found.push(`${dir}/${name}`); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| return found.slice(0, 5); | ||
| } | ||
| buildTechStack(deps, devDeps, lang) { | ||
| const stack = [lang]; | ||
| const allDeps = { ...deps, ...devDeps }; | ||
| const detected = /* @__PURE__ */ new Set(); | ||
| for (const [dep, label] of Object.entries(FRAMEWORK_SIGNATURES)) { | ||
| if (allDeps[dep]) { | ||
| detected.add(label); | ||
| } | ||
| } | ||
| if (allDeps["typescript"] || allDeps["ts-node"]) { | ||
| if (!stack.includes("TypeScript/JavaScript")) { | ||
| stack.push("TypeScript"); | ||
| } | ||
| } | ||
| if (allDeps["vitest"]) stack.push("Vitest (testing)"); | ||
| else if (allDeps["jest"]) stack.push("Jest (testing)"); | ||
| else if (allDeps["mocha"]) stack.push("Mocha (testing)"); | ||
| if (allDeps["turbo"] || allDeps["turborepo"]) | ||
| stack.push("Turborepo (monorepo)"); | ||
| else if (allDeps["nx"]) stack.push("Nx (monorepo)"); | ||
| if (allDeps["vite"]) stack.push("Vite (build)"); | ||
| else if (allDeps["webpack"]) stack.push("Webpack (build)"); | ||
| else if (allDeps["esbuild"]) stack.push("esbuild (build)"); | ||
| else if (allDeps["tsup"]) stack.push("tsup (build)"); | ||
| return [...stack, ...Array.from(detected)].slice(0, 8); | ||
| } | ||
| }; | ||
| function formatContext(ctx) { | ||
| const lines = [`# Project: ${ctx.projectName}`]; | ||
| if (ctx.purpose) { | ||
| lines.push(` | ||
| ## Purpose | ||
| ${ctx.purpose}`); | ||
| } | ||
| if (ctx.techStack.length > 0) { | ||
| lines.push(` | ||
| ## Tech Stack`); | ||
| for (const item of ctx.techStack) { | ||
| lines.push(`- ${item}`); | ||
| } | ||
| } | ||
| if (ctx.architecture.length > 0) { | ||
| lines.push(` | ||
| ## Architecture`); | ||
| for (const { path: p, description } of ctx.architecture) { | ||
| lines.push(`- \`${p}\` \u2014 ${description}`); | ||
| } | ||
| } | ||
| if (ctx.entryPoints.length > 0) { | ||
| lines.push(` | ||
| ## Entry Points`); | ||
| for (const ep of ctx.entryPoints) { | ||
| lines.push(`- ${ep}`); | ||
| } | ||
| } | ||
| if (ctx.keyConcepts.length > 0) { | ||
| lines.push(` | ||
| ## Key Concepts`); | ||
| for (const c of ctx.keyConcepts) { | ||
| lines.push(`- ${c}`); | ||
| } | ||
| } | ||
| lines.push( | ||
| ` | ||
| ## Custom Notes | ||
| <!-- Edit this section to add domain knowledge, naming conventions, or architecture notes -->` | ||
| ); | ||
| return lines.join("\n"); | ||
| } | ||
| // src/context/compiler.ts | ||
| var MAX_CONTEXT_CHARS = 2e3; | ||
| var CONTEXT_FILE_NAME = ".devdiff/context.md"; | ||
| async function loadContext(repoPath) { | ||
| const contextFilePath = path2.join(repoPath, CONTEXT_FILE_NAME); | ||
| try { | ||
| const raw = await fs2.readFile(contextFilePath, "utf-8"); | ||
| if (raw.trim()) { | ||
| const { redacted, hadSecrets } = sanitizeContext(raw); | ||
| return { raw: redacted, source: "file", hadSecrets }; | ||
| } | ||
| } catch { | ||
| } | ||
| try { | ||
| const scanner = new ProjectContextScanner(repoPath); | ||
| const ctx = await scanner.scan(); | ||
| const raw = formatContext(ctx); | ||
| if (!raw.trim()) return null; | ||
| const { redacted, hadSecrets } = sanitizeContext(raw); | ||
| return { raw: redacted, source: "auto-scan", hadSecrets }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function sanitizeContext(raw) { | ||
| const scanner = new SecretScanner(); | ||
| const findings = scanner.scan(raw); | ||
| const hadSecrets = findings.length > 0; | ||
| const redacted = hadSecrets ? scanner.redact(raw) : raw; | ||
| const trimmed = redacted.length > MAX_CONTEXT_CHARS ? redacted.substring(0, MAX_CONTEXT_CHARS) + "\n...(truncated for token budget)" : redacted; | ||
| return { redacted: trimmed, hadSecrets }; | ||
| } | ||
| function injectContextIntoPrompt(systemPrompt, context) { | ||
| const contextBlock = [ | ||
| "", | ||
| "=== PROJECT KNOWLEDGE BASE ===", | ||
| "Use this to understand the project. Base all explanations on the diff AND this context.", | ||
| "Do NOT fabricate modules, files, or identifiers not present in the diff.", | ||
| "", | ||
| context.trim(), | ||
| "=== END PROJECT KNOWLEDGE BASE ===", | ||
| "" | ||
| ].join("\n"); | ||
| return systemPrompt + contextBlock; | ||
| } | ||
| async function generateContextFile(repoPath) { | ||
| const scanner = new ProjectContextScanner(repoPath); | ||
| const context = await scanner.scan(); | ||
| const rawMarkdown = formatContext(context); | ||
| const { redacted, hadSecrets } = sanitizeContext(rawMarkdown); | ||
| const devdiffDir = path2.join(repoPath, ".devdiff"); | ||
| await fs2.mkdir(devdiffDir, { recursive: true }); | ||
| const filePath = path2.join(devdiffDir, "context.md"); | ||
| await fs2.writeFile(filePath, redacted, "utf-8"); | ||
| return { filePath, context, hadSecrets }; | ||
| } | ||
| async function validateContextFile(repoPath) { | ||
| const filePath = path2.join(repoPath, CONTEXT_FILE_NAME); | ||
| try { | ||
| const raw = await fs2.readFile(filePath, "utf-8"); | ||
| const scanner = new SecretScanner(); | ||
| const findings = scanner.scan(raw); | ||
| return { | ||
| exists: true, | ||
| filePath, | ||
| secrets: findings, | ||
| charCount: raw.length, | ||
| overBudget: raw.length > MAX_CONTEXT_CHARS | ||
| }; | ||
| } catch { | ||
| return { | ||
| exists: false, | ||
| filePath, | ||
| secrets: [], | ||
| charCount: 0, | ||
| overBudget: false | ||
| }; | ||
| } | ||
| } | ||
| export { | ||
| SecretScanner, | ||
| redactSecrets, | ||
| scanForSecrets, | ||
| ProjectContextScanner, | ||
| formatContext, | ||
| loadContext, | ||
| injectContextIntoPrompt, | ||
| generateContextFile, | ||
| validateContextFile | ||
| }; | ||
| //# sourceMappingURL=chunk-47DLDZSM.js.map |
| {"version":3,"sources":["../src/context/compiler.ts","../src/diff/secret-scanner.ts","../src/context/scanner.ts"],"sourcesContent":["import * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport { SecretScanner } from \"../diff/secret-scanner\";\nimport {\n ProjectContextScanner,\n formatContext,\n ScannedContext,\n} from \"./scanner\";\n\n/** Maximum characters to include in the injected context (~500 tokens). */\nconst MAX_CONTEXT_CHARS = 2000;\n\n/** Path inside the repo where the user's context file lives. */\nconst CONTEXT_FILE_NAME = \".devdiff/context.md\";\n\nexport interface LoadedContext {\n raw: string;\n source: \"file\" | \"auto-scan\" | \"none\";\n hadSecrets: boolean;\n}\n\n/**\n * Loads the project context from `.devdiff/context.md` if it exists,\n * or falls back to auto-scanning the project.\n *\n * Returns null if neither source yields content (empty project).\n */\nexport async function loadContext(\n repoPath: string,\n): Promise<LoadedContext | null> {\n const contextFilePath = path.join(repoPath, CONTEXT_FILE_NAME);\n\n // Try to load user's handcrafted context file\n try {\n const raw = await fs.readFile(contextFilePath, \"utf-8\");\n if (raw.trim()) {\n const { redacted, hadSecrets } = sanitizeContext(raw);\n return { raw: redacted, source: \"file\", hadSecrets };\n }\n } catch {\n // File doesn't exist or unreadable — fall through to auto-scan\n }\n\n // Auto-scan\n try {\n const scanner = new ProjectContextScanner(repoPath);\n const ctx = await scanner.scan();\n const raw = formatContext(ctx);\n if (!raw.trim()) return null;\n const { redacted, hadSecrets } = sanitizeContext(raw);\n return { raw: redacted, source: \"auto-scan\", hadSecrets };\n } catch {\n return null;\n }\n}\n\n/**\n * Compiles a context string: applies secret redaction and trims to token budget.\n */\nfunction sanitizeContext(raw: string): {\n redacted: string;\n hadSecrets: boolean;\n} {\n const scanner = new SecretScanner();\n const findings = scanner.scan(raw);\n const hadSecrets = findings.length > 0;\n const redacted = hadSecrets ? scanner.redact(raw) : raw;\n\n // Trim to budget\n const trimmed =\n redacted.length > MAX_CONTEXT_CHARS\n ? redacted.substring(0, MAX_CONTEXT_CHARS) +\n \"\\n...(truncated for token budget)\"\n : redacted;\n\n return { redacted: trimmed, hadSecrets };\n}\n\n/**\n * Injects project context into the system prompt.\n * The context is placed between the system instructions and the diff content.\n *\n * @param systemPrompt - The base SYSTEM_PROMPT string\n * @param context - Compiled context markdown\n * @returns Enhanced system prompt with context block\n */\nexport function injectContextIntoPrompt(\n systemPrompt: string,\n context: string,\n): string {\n const contextBlock = [\n \"\",\n \"=== PROJECT KNOWLEDGE BASE ===\",\n \"Use this to understand the project. Base all explanations on the diff AND this context.\",\n \"Do NOT fabricate modules, files, or identifiers not present in the diff.\",\n \"\",\n context.trim(),\n \"=== END PROJECT KNOWLEDGE BASE ===\",\n \"\",\n ].join(\"\\n\");\n\n return systemPrompt + contextBlock;\n}\n\n/**\n * Generates and writes context to `.devdiff/context.md`.\n * Creates the `.devdiff/` directory if needed.\n */\nexport async function generateContextFile(repoPath: string): Promise<{\n filePath: string;\n context: ScannedContext;\n hadSecrets: boolean;\n}> {\n const scanner = new ProjectContextScanner(repoPath);\n const context = await scanner.scan();\n const rawMarkdown = formatContext(context);\n\n const { redacted, hadSecrets } = sanitizeContext(rawMarkdown);\n\n const devdiffDir = path.join(repoPath, \".devdiff\");\n await fs.mkdir(devdiffDir, { recursive: true });\n\n const filePath = path.join(devdiffDir, \"context.md\");\n await fs.writeFile(filePath, redacted, \"utf-8\");\n\n return { filePath, context, hadSecrets };\n}\n\n/**\n * Reads and validates the context file for secrets.\n * Returns a report suitable for `devdiff context validate`.\n */\nexport async function validateContextFile(repoPath: string): Promise<{\n exists: boolean;\n filePath: string;\n secrets: { type: string; name: string; severity: string }[];\n charCount: number;\n overBudget: boolean;\n}> {\n const filePath = path.join(repoPath, CONTEXT_FILE_NAME);\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const scanner = new SecretScanner();\n const findings = scanner.scan(raw);\n\n return {\n exists: true,\n filePath,\n secrets: findings,\n charCount: raw.length,\n overBudget: raw.length > MAX_CONTEXT_CHARS,\n };\n } catch {\n return {\n exists: false,\n filePath,\n secrets: [],\n charCount: 0,\n overBudget: false,\n };\n }\n}\n","import { DevDiffConfig } from \"../config/schema\";\n\nexport interface SecretPattern {\n name: string;\n type: string;\n regex: RegExp;\n severity: \"info\" | \"low\" | \"medium\" | \"high\" | \"critical\";\n}\n\nexport const SECRET_PATTERNS: SecretPattern[] = [\n {\n name: \"Generic API Key / Token\",\n type: \"generic-api-key\",\n // Matches patterns like apiKey = \"...\", token: \"...\"\n regex:\n /(api[_-]?key|secret|token|password|passwd|auth[_-]?key|private[_-]?key|passphrase)\\s*[:=]\\s*['\"`]([A-Za-z0-9%_-]{8,})['\"`]/gi,\n severity: \"high\",\n },\n {\n name: \"OpenAI API Key\",\n type: \"openai-api-key\",\n regex: /sk-[a-zA-Z0-9-]{20,}/g,\n severity: \"critical\",\n },\n {\n name: \"Anthropic API Key\",\n type: \"anthropic-api-key\",\n regex: /sk-ant-[a-zA-Z0-9-]{20,}/g,\n severity: \"critical\",\n },\n {\n name: \"Google Gemini API Key\",\n type: \"gemini-api-key\",\n regex: /AIzaSy[a-zA-Z0-9_-]{33}/g,\n severity: \"critical\",\n },\n {\n name: \"Slack Webhook / Token\",\n type: \"slack-webhook\",\n regex:\n /https:\\/\\/hooks\\.slack\\.com\\/services\\/[A-Za-z0-9]+\\/[A-Za-z0-9]+\\/[A-Za-z0-9]+/g,\n severity: \"high\",\n },\n {\n name: \"AWS Access Key ID / Secret\",\n type: \"aws-access-key\",\n regex: /(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/g,\n severity: \"high\",\n },\n {\n name: \"Connection String\",\n type: \"connection-string\",\n regex:\n /(mongodb\\+srv|mongodb|postgres|postgresql|mysql|redis|sqlite):\\/\\/[^:\\s]+:[^@\\s]+@[^@\\s]+/gi,\n severity: \"high\",\n },\n {\n name: \"Private Key PEM Block\",\n type: \"private-key\",\n regex:\n /-----BEGIN (RSA |EC |PGP |)?PRIVATE KEY-----[\\s\\S]+?-----END (RSA |EC |PGP |)?PRIVATE KEY-----/g,\n severity: \"high\",\n },\n {\n name: \"JWT Token\",\n type: \"jwt-token\",\n regex: /eyJhbGciOi[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*/g,\n severity: \"medium\",\n },\n];\n\nexport interface Finding {\n type: string;\n name: string;\n severity: string;\n}\n\nexport class SecretScanner {\n private customPatterns: SecretPattern[] = [];\n\n constructor(config?: DevDiffConfig) {\n if (config?.security?.customSecretPatterns) {\n for (const [index, p] of config.security.customSecretPatterns.entries()) {\n try {\n this.customPatterns.push({\n name: `Custom Pattern ${index + 1}`,\n type: `custom-secret-${index + 1}`,\n regex: new RegExp(p, \"g\"),\n severity: \"high\",\n });\n } catch {}\n }\n }\n }\n\n scan(content: string): Finding[] {\n if (!content) return [];\n const findings: Finding[] = [];\n const allPatterns = [...SECRET_PATTERNS, ...this.customPatterns];\n\n for (const pattern of allPatterns) {\n // Reset regex index for safety\n pattern.regex.lastIndex = 0;\n if (pattern.regex.test(content)) {\n findings.push({\n type: pattern.type,\n name: pattern.name,\n severity: pattern.severity,\n });\n }\n }\n\n return findings;\n }\n\n redact(content: string): string {\n if (!content) return \"\";\n let redacted = content;\n const allPatterns = [...SECRET_PATTERNS, ...this.customPatterns];\n\n for (const pattern of allPatterns) {\n pattern.regex.lastIndex = 0;\n if (pattern.type === \"generic-api-key\") {\n // Redact only the captured secret value\n redacted = redacted.replace(pattern.regex, (match, prefix, secret) => {\n const secretIndex = match.lastIndexOf(secret);\n const prefixPart = match.substring(0, secretIndex);\n const suffixPart = match.substring(secretIndex + secret.length);\n return `${prefixPart}[REDACTED:${pattern.type}]${suffixPart}`;\n });\n } else {\n redacted = redacted.replace(\n pattern.regex,\n `[REDACTED:${pattern.type}]`,\n );\n }\n }\n\n return redacted;\n }\n}\n\n// Legacy helper functions for backward compatibility:\nexport function redactSecrets(content: string): string {\n const scanner = new SecretScanner();\n // We need generic replacement style compat for legacy\n let redacted = content;\n for (const pattern of SECRET_PATTERNS) {\n if (pattern.type === \"generic-api-key\") {\n redacted = redacted.replace(pattern.regex, (match, prefix, secret) => {\n const secretIndex = match.lastIndexOf(secret);\n const prefixPart = match.substring(0, secretIndex);\n const suffixPart = match.substring(secretIndex + secret.length);\n return `${prefixPart}[REDACTED]${suffixPart}`;\n });\n } else {\n redacted = redacted.replace(pattern.regex, \"[REDACTED]\");\n }\n }\n return redacted;\n}\n\nexport function scanForSecrets(content: string): string[] {\n const scanner = new SecretScanner();\n return scanner.scan(content).map((f) => f.name);\n}\n","import * as fs from \"fs/promises\";\nimport * as path from \"path\";\n\nexport interface ScannedContext {\n projectName: string;\n purpose: string;\n techStack: string[];\n architecture: { path: string; description: string }[];\n entryPoints: string[];\n keyConcepts: string[];\n}\n\n/**\n * Known framework signatures mapped from dependency keys.\n */\nconst FRAMEWORK_SIGNATURES: Record<string, string> = {\n next: \"Next.js (React SSR/SSG)\",\n react: \"React\",\n vue: \"Vue.js\",\n svelte: \"Svelte\",\n angular: \"@angular/core\",\n express: \"Express (Node.js HTTP server)\",\n fastify: \"Fastify (Node.js HTTP server)\",\n koa: \"Koa (Node.js HTTP server)\",\n hono: \"Hono (edge-first HTTP framework)\",\n nestjs: \"NestJS (@nestjs/core)\",\n \"@nestjs/core\": \"NestJS\",\n vite: \"Vite\",\n astro: \"Astro\",\n nuxt: \"Nuxt.js\",\n remix: \"Remix\",\n electron: \"Electron (desktop app)\",\n tauri: \"@tauri-apps/api\",\n \"@tauri-apps/api\": \"Tauri (desktop app)\",\n prisma: \"Prisma ORM\",\n drizzle: \"Drizzle ORM\",\n mongoose: \"Mongoose (MongoDB ODM)\",\n typeorm: \"TypeORM\",\n trpc: \"@trpc/server\",\n \"@trpc/server\": \"tRPC\",\n graphql: \"GraphQL\",\n \"@apollo/server\": \"Apollo GraphQL Server\",\n};\n\n/**\n * Directory names and their likely purpose.\n */\nconst DIR_PURPOSE: Record<string, string> = {\n src: \"main source code\",\n lib: \"library/shared code\",\n app: \"application entry & routing\",\n api: \"API routes/handlers\",\n server: \"server-side code\",\n client: \"client-side code\",\n pages: \"page components (Next.js/Nuxt style)\",\n components: \"reusable UI components\",\n hooks: \"custom React hooks\",\n utils: \"utility functions\",\n helpers: \"helper functions\",\n services: \"business logic / service layer\",\n repositories: \"data access layer\",\n models: \"data models / entities\",\n schemas: \"data schemas / validation\",\n controllers: \"MVC controllers\",\n routes: \"HTTP route definitions\",\n middleware: \"middleware functions\",\n handlers: \"request handlers\",\n queries: \"database queries / CQRS queries\",\n commands: \"CQRS commands / CLI commands\",\n config: \"configuration files\",\n types: \"TypeScript type definitions\",\n interfaces: \"TypeScript interfaces\",\n constants: \"application constants\",\n migrations: \"database migrations\",\n seeds: \"database seed data\",\n scripts: \"build/automation scripts\",\n tests: \"test suites\",\n test: \"test suites\",\n __tests__: \"test suites (Jest convention)\",\n spec: \"test specs\",\n e2e: \"end-to-end tests\",\n packages: \"monorepo packages\",\n plugins: \"plugins / extensions\",\n adapters: \"adapter pattern implementations\",\n providers: \"dependency injection providers\",\n store: \"state management (Redux/Zustand/Pinia)\",\n assets: \"static assets\",\n public: \"publicly served files\",\n styles: \"CSS/SCSS stylesheets\",\n i18n: \"internationalization strings\",\n locales: \"locale files\",\n docs: \"documentation\",\n};\n\n/**\n * Directories to skip during scanning — not relevant to project architecture.\n */\nconst SKIP_DIRS = new Set([\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \".turbo\",\n \".next\",\n \".nuxt\",\n \".svelte-kit\",\n \"coverage\",\n \".cache\",\n \"__pycache__\",\n \".vitepress\",\n \".changeset\",\n \"tmp\",\n \"temp\",\n]);\n\nconst ENTRY_POINT_NAMES = [\n \"index.ts\",\n \"index.js\",\n \"index.mjs\",\n \"main.ts\",\n \"main.js\",\n \"app.ts\",\n \"app.js\",\n \"app.tsx\",\n \"server.ts\",\n \"server.js\",\n];\n\n/**\n * Scans a project directory and extracts structured context.\n * Caps total output at ~2000 chars (~500 tokens).\n */\nexport class ProjectContextScanner {\n private repoPath: string;\n\n constructor(repoPath: string) {\n this.repoPath = repoPath;\n }\n\n async scan(): Promise<ScannedContext> {\n const [pkgInfo, readmePurpose, topDirs] = await Promise.all([\n this.readPackageJson(),\n this.readReadme(),\n this.listTopLevelDirs(),\n ]);\n\n const entryPoints = await this.findEntryPoints(topDirs.rawDirs);\n\n const techStack = this.buildTechStack(\n pkgInfo.dependencies,\n pkgInfo.devDependencies,\n pkgInfo.lang,\n );\n\n return {\n projectName: pkgInfo.name || path.basename(this.repoPath),\n purpose: pkgInfo.description || readmePurpose,\n techStack,\n architecture: topDirs.architecture,\n entryPoints,\n keyConcepts: [],\n };\n }\n\n private async readPackageJson(): Promise<{\n name: string;\n description: string;\n dependencies: Record<string, string>;\n devDependencies: Record<string, string>;\n lang: string;\n }> {\n try {\n const raw = await fs.readFile(\n path.join(this.repoPath, \"package.json\"),\n \"utf-8\",\n );\n const pkg = JSON.parse(raw);\n return {\n name: pkg.name || \"\",\n description: pkg.description || \"\",\n dependencies: pkg.dependencies || {},\n devDependencies: pkg.devDependencies || {},\n lang: \"TypeScript/JavaScript\",\n };\n } catch {\n // Not a Node.js project — check for other languages\n const lang = await this.detectLanguage();\n return {\n name: \"\",\n description: \"\",\n dependencies: {},\n devDependencies: {},\n lang,\n };\n }\n }\n\n private async detectLanguage(): Promise<string> {\n const checks: [string, string][] = [\n [\"Cargo.toml\", \"Rust\"],\n [\"go.mod\", \"Go\"],\n [\"pyproject.toml\", \"Python\"],\n [\"requirements.txt\", \"Python\"],\n [\"pom.xml\", \"Java (Maven)\"],\n [\"build.gradle\", \"Java/Kotlin (Gradle)\"],\n [\"composer.json\", \"PHP\"],\n [\"Gemfile\", \"Ruby\"],\n ];\n for (const [file, lang] of checks) {\n try {\n await fs.access(path.join(this.repoPath, file));\n return lang;\n } catch {}\n }\n return \"Unknown\";\n }\n\n private async readReadme(): Promise<string> {\n const candidates = [\"README.md\", \"readme.md\", \"README.txt\", \"README\"];\n for (const name of candidates) {\n try {\n const content = await fs.readFile(\n path.join(this.repoPath, name),\n \"utf-8\",\n );\n // Extract the first non-heading paragraph (first 400 words)\n const lines = content.split(\"\\n\");\n const paragraphs: string[] = [];\n let wordCount = 0;\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\") || trimmed.startsWith(\"![\")) {\n continue;\n }\n // Skip badge lines\n if (trimmed.startsWith(\"[![\")) continue;\n const words = trimmed.split(/\\s+/);\n paragraphs.push(trimmed);\n wordCount += words.length;\n if (wordCount >= 80) break;\n }\n const excerpt = paragraphs.join(\" \").substring(0, 500);\n return excerpt || \"\";\n } catch {}\n }\n return \"\";\n }\n\n private async listTopLevelDirs(): Promise<{\n architecture: { path: string; description: string }[];\n rawDirs: string[];\n }> {\n try {\n const entries = await fs.readdir(this.repoPath, { withFileTypes: true });\n const dirs = entries\n .filter(\n (e) =>\n e.isDirectory() &&\n !SKIP_DIRS.has(e.name) &&\n !e.name.startsWith(\".\"),\n )\n .map((e) => e.name)\n .sort();\n\n const architecture: { path: string; description: string }[] = [];\n\n for (const dir of dirs) {\n const purpose = DIR_PURPOSE[dir.toLowerCase()];\n if (purpose) {\n architecture.push({ path: dir + \"/\", description: purpose });\n } else {\n // Try to infer from subdirectories\n architecture.push({\n path: dir + \"/\",\n description: \"project directory\",\n });\n }\n }\n\n // Limit to most meaningful dirs (max 12)\n const meaningful = architecture.filter(\n (a) => a.description !== \"project directory\",\n );\n const others = architecture.filter(\n (a) => a.description === \"project directory\",\n );\n\n return {\n architecture: [...meaningful.slice(0, 10), ...others.slice(0, 2)],\n rawDirs: dirs,\n };\n } catch {\n return { architecture: [], rawDirs: [] };\n }\n }\n\n private async findEntryPoints(topDirs: string[]): Promise<string[]> {\n const found: string[] = [];\n\n // Check root level\n for (const name of ENTRY_POINT_NAMES) {\n try {\n await fs.access(path.join(this.repoPath, name));\n found.push(name);\n } catch {}\n }\n\n // Check common source dirs\n const sourceDirs = topDirs.filter((d) =>\n [\"src\", \"app\", \"lib\", \"packages\"].includes(d),\n );\n for (const dir of sourceDirs.slice(0, 3)) {\n for (const name of ENTRY_POINT_NAMES) {\n try {\n await fs.access(path.join(this.repoPath, dir, name));\n found.push(`${dir}/${name}`);\n } catch {}\n }\n }\n\n return found.slice(0, 5);\n }\n\n private buildTechStack(\n deps: Record<string, string>,\n devDeps: Record<string, string>,\n lang: string,\n ): string[] {\n const stack: string[] = [lang];\n const allDeps = { ...deps, ...devDeps };\n\n // Detect frameworks\n const detected = new Set<string>();\n for (const [dep, label] of Object.entries(FRAMEWORK_SIGNATURES)) {\n if (allDeps[dep]) {\n detected.add(label);\n }\n }\n\n // Detect TypeScript\n if (allDeps[\"typescript\"] || allDeps[\"ts-node\"]) {\n if (!stack.includes(\"TypeScript/JavaScript\")) {\n stack.push(\"TypeScript\");\n }\n }\n\n // Detect test framework\n if (allDeps[\"vitest\"]) stack.push(\"Vitest (testing)\");\n else if (allDeps[\"jest\"]) stack.push(\"Jest (testing)\");\n else if (allDeps[\"mocha\"]) stack.push(\"Mocha (testing)\");\n\n // Detect monorepo tools\n if (allDeps[\"turbo\"] || allDeps[\"turborepo\"])\n stack.push(\"Turborepo (monorepo)\");\n else if (allDeps[\"nx\"]) stack.push(\"Nx (monorepo)\");\n\n // Detect build tools\n if (allDeps[\"vite\"]) stack.push(\"Vite (build)\");\n else if (allDeps[\"webpack\"]) stack.push(\"Webpack (build)\");\n else if (allDeps[\"esbuild\"]) stack.push(\"esbuild (build)\");\n else if (allDeps[\"tsup\"]) stack.push(\"tsup (build)\");\n\n return [...stack, ...Array.from(detected)].slice(0, 8);\n }\n}\n\n/**\n * Generates a context markdown string from a scanned project.\n */\nexport function formatContext(ctx: ScannedContext): string {\n const lines: string[] = [`# Project: ${ctx.projectName}`];\n\n if (ctx.purpose) {\n lines.push(`\\n## Purpose\\n${ctx.purpose}`);\n }\n\n if (ctx.techStack.length > 0) {\n lines.push(`\\n## Tech Stack`);\n for (const item of ctx.techStack) {\n lines.push(`- ${item}`);\n }\n }\n\n if (ctx.architecture.length > 0) {\n lines.push(`\\n## Architecture`);\n for (const { path: p, description } of ctx.architecture) {\n lines.push(`- \\`${p}\\` — ${description}`);\n }\n }\n\n if (ctx.entryPoints.length > 0) {\n lines.push(`\\n## Entry Points`);\n for (const ep of ctx.entryPoints) {\n lines.push(`- ${ep}`);\n }\n }\n\n if (ctx.keyConcepts.length > 0) {\n lines.push(`\\n## Key Concepts`);\n for (const c of ctx.keyConcepts) {\n lines.push(`- ${c}`);\n }\n }\n\n lines.push(\n `\\n## Custom Notes\\n<!-- Edit this section to add domain knowledge, naming conventions, or architecture notes -->`,\n );\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["fs","path"]} |
| // src/ai/providers/ollama-discovery.ts | ||
| var OllamaModelDiscovery = class { | ||
| /** | ||
| * Query Ollama API for all installed models | ||
| */ | ||
| static async discoverModels() { | ||
| const ollamaUrls = this.getOllamaUrls(); | ||
| for (const url of ollamaUrls) { | ||
| try { | ||
| const response = await fetch(`${url}/api/tags`, { | ||
| signal: AbortSignal.timeout(3e3) | ||
| // 3s timeout for quick failure | ||
| }); | ||
| if (!response.ok) continue; | ||
| const data = await response.json(); | ||
| if (!data.models || data.models.length === 0) { | ||
| return []; | ||
| } | ||
| return data.models.map((model) => ({ | ||
| name: model.name, | ||
| size: model.size, | ||
| modified: model.modified_at, | ||
| digest: model.digest, | ||
| family: this.detectModelFamily(model.name), | ||
| parameterSize: this.detectParameterSize(model.name), | ||
| quantization: this.detectQuantization(model.name) | ||
| })); | ||
| } catch { | ||
| continue; | ||
| } | ||
| } | ||
| return []; | ||
| } | ||
| /** | ||
| * Detect which model family this belongs to | ||
| */ | ||
| static detectModelFamily(name) { | ||
| const lower = name.toLowerCase(); | ||
| if (lower.includes("qwen")) return "qwen"; | ||
| if (lower.includes("codellama") || lower.includes("code-llama")) | ||
| return "codellama"; | ||
| if (lower.includes("llama")) return "llama"; | ||
| if (lower.includes("mistral")) return "mistral"; | ||
| if (lower.includes("gemma")) return "gemma"; | ||
| if (lower.includes("phi")) return "phi"; | ||
| if (lower.includes("deepseek")) return "deepseek"; | ||
| if (lower.includes("mixtral")) return "mixtral"; | ||
| if (lower.includes("command-r")) return "command-r"; | ||
| return "unknown"; | ||
| } | ||
| /** | ||
| * Extract parameter size from model name | ||
| */ | ||
| static detectParameterSize(name) { | ||
| const match = name.match(/(\d+\.?\d*)b/i); | ||
| return match ? match[1].toLowerCase() + "b" : "unknown"; | ||
| } | ||
| /** | ||
| * Detect quantization level | ||
| */ | ||
| static detectQuantization(name) { | ||
| const lower = name.toLowerCase(); | ||
| if (lower.includes("q8_0")) return "Q8_0"; | ||
| if (lower.includes("q6_k")) return "Q6_K"; | ||
| if (lower.includes("q5_k_m")) return "Q5_K_M"; | ||
| if (lower.includes("q5_k_s")) return "Q5_K_S"; | ||
| if (lower.includes("q4_k_m")) return "Q4_K_M"; | ||
| if (lower.includes("q4_k_s")) return "Q4_K_S"; | ||
| if (lower.includes("q4_0")) return "Q4_0"; | ||
| if (lower.includes("f16")) return "F16"; | ||
| if (lower.includes("fp16")) return "FP16"; | ||
| return "unknown"; | ||
| } | ||
| /** | ||
| * Get all possible Ollama URLs to try | ||
| */ | ||
| static getOllamaUrls() { | ||
| const urls = []; | ||
| if (process.env.OLLAMA_HOST) { | ||
| const customUrl = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`; | ||
| urls.push(customUrl); | ||
| } | ||
| urls.push("http://localhost:11434"); | ||
| urls.push("http://127.0.0.1:11434"); | ||
| return urls; | ||
| } | ||
| /** | ||
| * Get the best model for code analysis from available models | ||
| */ | ||
| static selectBestModel(models, preference) { | ||
| if (models.length === 0) return null; | ||
| if (preference) { | ||
| const preferred = models.find( | ||
| (m) => m.name === preference || m.name.includes(preference) | ||
| ); | ||
| if (preferred) return preferred; | ||
| } | ||
| const scored = models.map((model) => ({ | ||
| model, | ||
| score: this.calculateCodeScore(model) | ||
| })); | ||
| scored.sort((a, b) => b.score - a.score); | ||
| return scored[0]?.model || null; | ||
| } | ||
| /** | ||
| * Score model for code analysis capability | ||
| */ | ||
| static calculateCodeScore(model) { | ||
| let score = 0; | ||
| const codeSpecialized = ["codellama", "qwen", "deepseek"]; | ||
| if (codeSpecialized.includes(model.family)) { | ||
| score += 30; | ||
| } | ||
| const paramSize = parseInt(model.parameterSize); | ||
| if (paramSize >= 13) score += 25; | ||
| else if (paramSize >= 7) score += 20; | ||
| else if (paramSize >= 3) score += 10; | ||
| if (model.quantization.startsWith("Q8") || model.quantization === "F16") | ||
| score += 15; | ||
| else if (model.quantization.startsWith("Q6")) score += 10; | ||
| else if (model.quantization.startsWith("Q5")) score += 8; | ||
| else if (model.quantization.startsWith("Q4")) score += 5; | ||
| const daysSincePull = (Date.now() - new Date(model.modified).getTime()) / (1e3 * 60 * 60 * 24); | ||
| if (daysSincePull < 7) score += 5; | ||
| return score; | ||
| } | ||
| }; | ||
| export { | ||
| OllamaModelDiscovery | ||
| }; | ||
| //# sourceMappingURL=chunk-HRKIFQSQ.js.map |
| {"version":3,"sources":["../src/ai/providers/ollama-discovery.ts"],"sourcesContent":["export interface OllamaModel {\n name: string;\n size: number; // bytes\n modified: string; // ISO date\n digest: string;\n family: string; // llama, qwen, codellama, mistral, gemma, phi\n parameterSize: string; // 3b, 7b, 8b, 13b, 34b, 70b\n quantization: string; // Q4_K_M, Q8_0, F16\n}\n\nexport class OllamaModelDiscovery {\n /**\n * Query Ollama API for all installed models\n */\n static async discoverModels(): Promise<OllamaModel[]> {\n const ollamaUrls = this.getOllamaUrls();\n\n for (const url of ollamaUrls) {\n try {\n const response = await fetch(`${url}/api/tags`, {\n signal: AbortSignal.timeout(3000), // 3s timeout for quick failure\n });\n\n if (!response.ok) continue;\n\n const data = (await response.json()) as {\n models?: Array<{\n name: string;\n size: number;\n modified_at: string;\n digest: string;\n }>;\n };\n\n if (!data.models || data.models.length === 0) {\n return [];\n }\n\n return data.models.map((model) => ({\n name: model.name,\n size: model.size,\n modified: model.modified_at,\n digest: model.digest,\n family: this.detectModelFamily(model.name),\n parameterSize: this.detectParameterSize(model.name),\n quantization: this.detectQuantization(model.name),\n }));\n } catch {\n // Try next URL\n continue;\n }\n }\n\n return [];\n }\n\n /**\n * Detect which model family this belongs to\n */\n private static detectModelFamily(name: string): string {\n const lower = name.toLowerCase();\n if (lower.includes(\"qwen\")) return \"qwen\";\n if (lower.includes(\"codellama\") || lower.includes(\"code-llama\"))\n return \"codellama\";\n if (lower.includes(\"llama\")) return \"llama\";\n if (lower.includes(\"mistral\")) return \"mistral\";\n if (lower.includes(\"gemma\")) return \"gemma\";\n if (lower.includes(\"phi\")) return \"phi\";\n if (lower.includes(\"deepseek\")) return \"deepseek\";\n if (lower.includes(\"mixtral\")) return \"mixtral\";\n if (lower.includes(\"command-r\")) return \"command-r\";\n return \"unknown\";\n }\n\n /**\n * Extract parameter size from model name\n */\n private static detectParameterSize(name: string): string {\n const match = name.match(/(\\d+\\.?\\d*)b/i);\n return match ? match[1].toLowerCase() + \"b\" : \"unknown\";\n }\n\n /**\n * Detect quantization level\n */\n private static detectQuantization(name: string): string {\n const lower = name.toLowerCase();\n if (lower.includes(\"q8_0\")) return \"Q8_0\";\n if (lower.includes(\"q6_k\")) return \"Q6_K\";\n if (lower.includes(\"q5_k_m\")) return \"Q5_K_M\";\n if (lower.includes(\"q5_k_s\")) return \"Q5_K_S\";\n if (lower.includes(\"q4_k_m\")) return \"Q4_K_M\";\n if (lower.includes(\"q4_k_s\")) return \"Q4_K_S\";\n if (lower.includes(\"q4_0\")) return \"Q4_0\";\n if (lower.includes(\"f16\")) return \"F16\";\n if (lower.includes(\"fp16\")) return \"FP16\";\n return \"unknown\";\n }\n\n /**\n * Get all possible Ollama URLs to try\n */\n private static getOllamaUrls(): string[] {\n const urls: string[] = [];\n\n // Custom host from environment\n if (process.env.OLLAMA_HOST) {\n const customUrl = process.env.OLLAMA_HOST.startsWith(\"http\")\n ? process.env.OLLAMA_HOST\n : `http://${process.env.OLLAMA_HOST}`;\n urls.push(customUrl);\n }\n\n // Default localhost\n urls.push(\"http://localhost:11434\");\n urls.push(\"http://127.0.0.1:11434\");\n\n return urls;\n }\n\n /**\n * Get the best model for code analysis from available models\n */\n static selectBestModel(\n models: OllamaModel[],\n preference?: string,\n ): OllamaModel | null {\n if (models.length === 0) return null;\n\n // If user has a preference and it exists, use it\n if (preference) {\n const preferred = models.find(\n (m) => m.name === preference || m.name.includes(preference),\n );\n if (preferred) return preferred;\n }\n\n // Scoring system for code analysis capability\n const scored = models.map((model) => ({\n model,\n score: this.calculateCodeScore(model),\n }));\n\n // Sort by score descending\n scored.sort((a, b) => b.score - a.score);\n\n return scored[0]?.model || null;\n }\n\n /**\n * Score model for code analysis capability\n */\n private static calculateCodeScore(model: OllamaModel): number {\n let score = 0;\n\n // Code-specialized models score highest\n const codeSpecialized = [\"codellama\", \"qwen\", \"deepseek\"];\n if (codeSpecialized.includes(model.family)) {\n score += 30;\n }\n\n // Larger models generally better (up to a point)\n const paramSize = parseInt(model.parameterSize);\n if (paramSize >= 13) score += 25;\n else if (paramSize >= 7) score += 20;\n else if (paramSize >= 3) score += 10;\n\n // Higher quantization = better quality\n if (model.quantization.startsWith(\"Q8\") || model.quantization === \"F16\")\n score += 15;\n else if (model.quantization.startsWith(\"Q6\")) score += 10;\n else if (model.quantization.startsWith(\"Q5\")) score += 8;\n else if (model.quantization.startsWith(\"Q4\")) score += 5;\n\n // Prefer recently pulled (more likely maintained)\n const daysSincePull =\n (Date.now() - new Date(model.modified).getTime()) / (1000 * 60 * 60 * 24);\n if (daysSincePull < 7) score += 5;\n\n return score;\n }\n}\n"],"mappings":"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","names":[]} |
| import { | ||
| OllamaModelDiscovery | ||
| } from "./chunk-HRKIFQSQ.js"; | ||
| // src/ai/router.ts | ||
| import * as fs4 from "fs/promises"; | ||
| import * as path4 from "path"; | ||
| // src/security/shell-sandbox.ts | ||
| import { exec } from "child_process"; | ||
| // src/security/security-audit.ts | ||
| import * as fs from "fs/promises"; | ||
| import * as path from "path"; | ||
| import * as crypto from "crypto"; | ||
| var SecurityAudit = class { | ||
| static getLogPath() { | ||
| return process.env.DEVDIFF_AUDIT_PATH || path.resolve(process.cwd(), ".devdiff/security-audit.enc"); | ||
| } | ||
| // Legacy plaintext path (read-only migration) | ||
| static getLegacyPath() { | ||
| return process.env.DEVDIFF_LEGACY_AUDIT_PATH || path.resolve(process.cwd(), ".devdiff/security-audit.json"); | ||
| } | ||
| static getKey() { | ||
| const envKey = process.env.DEVDIFF_AUDIT_KEY; | ||
| if (!envKey) return null; | ||
| return crypto.createHash("sha256").update(envKey).digest(); | ||
| } | ||
| static async readLegacyLogs() { | ||
| const legacyPath = this.getLegacyPath(); | ||
| try { | ||
| const fileContent = await fs.readFile(legacyPath, "utf-8"); | ||
| if (!fileContent.trim()) return []; | ||
| return JSON.parse(fileContent); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| static async readEncryptedLogs(key) { | ||
| try { | ||
| const content = await fs.readFile(this.getLogPath(), "utf-8"); | ||
| const lines = content.split("\n").filter(Boolean); | ||
| const entries = []; | ||
| for (const line of lines) { | ||
| try { | ||
| const decrypted = this.decrypt(line.trim(), key); | ||
| entries.push(JSON.parse(decrypted)); | ||
| } catch { | ||
| } | ||
| } | ||
| return entries; | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
| static encrypt(plaintext, key) { | ||
| const iv = crypto.randomBytes(12); | ||
| const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); | ||
| const encrypted = Buffer.concat([ | ||
| cipher.update(plaintext, "utf8"), | ||
| cipher.final() | ||
| ]); | ||
| const authTag = cipher.getAuthTag(); | ||
| return Buffer.concat([iv, authTag, encrypted]).toString("base64"); | ||
| } | ||
| static decrypt(encoded, key) { | ||
| const buf = Buffer.from(encoded, "base64"); | ||
| const iv = buf.slice(0, 12); | ||
| const authTag = buf.slice(12, 28); | ||
| const ciphertext = buf.slice(28); | ||
| const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); | ||
| decipher.setAuthTag(authTag); | ||
| return decipher.update(ciphertext) + decipher.final("utf8"); | ||
| } | ||
| static async log(entry) { | ||
| try { | ||
| const logPath = this.getLogPath(); | ||
| await fs.mkdir(path.dirname(logPath), { recursive: true }); | ||
| const key = this.getKey(); | ||
| const entryToWrite = { ...entry, timestamp: Date.now() }; | ||
| const entryJson = JSON.stringify(entryToWrite); | ||
| if (key) { | ||
| try { | ||
| const encryptedLine = this.encrypt(entryJson, key) + "\n"; | ||
| await fs.appendFile(logPath, encryptedLine, "utf-8"); | ||
| return; | ||
| } catch (encryptError) { | ||
| console.warn( | ||
| "Encrypted audit log failed, falling back to plaintext:", | ||
| encryptError | ||
| ); | ||
| } | ||
| } | ||
| const legacyPath = this.getLegacyPath(); | ||
| const logs = await this.readLegacyLogs(); | ||
| logs.push(entryToWrite); | ||
| await fs.writeFile(legacyPath, JSON.stringify(logs, null, 2), "utf-8"); | ||
| } catch (err) { | ||
| console.error("Failed to write to security audit trail:", err); | ||
| } | ||
| } | ||
| static async getLogs() { | ||
| const key = this.getKey(); | ||
| if (key) { | ||
| const encryptedLogs = await this.readEncryptedLogs(key); | ||
| if (encryptedLogs.length > 0) { | ||
| return encryptedLogs; | ||
| } | ||
| } | ||
| return this.readLegacyLogs(); | ||
| } | ||
| /** | ||
| * Verify integrity of the encrypted log file. | ||
| * Returns true if all entries can be decrypted without errors. | ||
| */ | ||
| static async verifyIntegrity() { | ||
| const key = this.getKey(); | ||
| if (!key) { | ||
| return { valid: true, total: 0, corrupted: 0 }; | ||
| } | ||
| try { | ||
| const content = await fs.readFile(this.getLogPath(), "utf-8"); | ||
| const lines = content.split("\n").filter(Boolean); | ||
| let corrupted = 0; | ||
| for (const line of lines) { | ||
| try { | ||
| this.decrypt(line.trim(), key); | ||
| } catch { | ||
| corrupted++; | ||
| } | ||
| } | ||
| return { valid: corrupted === 0, total: lines.length, corrupted }; | ||
| } catch { | ||
| return { valid: true, total: 0, corrupted: 0 }; | ||
| } | ||
| } | ||
| }; | ||
| // src/security/shell-sandbox.ts | ||
| var ShellAccessDeniedError = class extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = "ShellAccessDeniedError"; | ||
| } | ||
| }; | ||
| function execAsync(command, args, options) { | ||
| return new Promise((resolve4, reject) => { | ||
| const fullCommand = `${command} ${args.join(" ")}`.trim(); | ||
| exec(fullCommand, options, (error, stdout, stderr) => { | ||
| if (error) { | ||
| reject(error); | ||
| } else { | ||
| resolve4({ stdout, stderr }); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| var ShellSandbox = class { | ||
| static ALLOWED_COMMANDS = ["git", "ollama", "which", "node"]; | ||
| static BLOCKED_PATTERNS = [ | ||
| /rm\s+-rf/, | ||
| /sudo/, | ||
| /curl.*\|.*sh/, | ||
| /eval/, | ||
| /\$\(/, | ||
| /`/, | ||
| /&&/, | ||
| /\|\|/, | ||
| /;/ | ||
| ]; | ||
| static async exec(command, args) { | ||
| const baseCommand = command.split(" ")[0]; | ||
| if (!this.ALLOWED_COMMANDS.includes(baseCommand)) { | ||
| throw new ShellAccessDeniedError( | ||
| `Command "${baseCommand}" is not in the allowed list. Allowed: ${this.ALLOWED_COMMANDS.join(", ")}.` | ||
| ); | ||
| } | ||
| const fullCommand = `${command} ${args.join(" ")}`.trim(); | ||
| for (const pattern of this.BLOCKED_PATTERNS) { | ||
| if (pattern.test(fullCommand)) { | ||
| throw new ShellAccessDeniedError( | ||
| `Command contains blocked pattern: ${pattern}. This is a security precaution.` | ||
| ); | ||
| } | ||
| } | ||
| await SecurityAudit.log({ | ||
| type: "shell-access", | ||
| command: baseCommand, | ||
| args, | ||
| timestamp: Date.now(), | ||
| caller: new Error().stack?.split("\n")[2]?.trim() | ||
| }); | ||
| const result = await execAsync(command, args, { timeout: 3e4 }); | ||
| return result.stdout; | ||
| } | ||
| static disable() { | ||
| console.log( | ||
| "\u26A0\uFE0F Shell access disabled. Git analysis will use isomorphic-git (pure JS)." | ||
| ); | ||
| } | ||
| }; | ||
| var SHELL_ACCESS_CONFIG = { | ||
| disableShellAccess: false, | ||
| allowedShellCommands: ["git", "ollama", "which"], | ||
| shellTimeout: 3e4, | ||
| auditShellAccess: true | ||
| }; | ||
| // src/ai/providers/base.ts | ||
| var SYSTEM_PROMPT = `You are DevDiff, an AI that analyzes git diffs and writes clear, human-readable changelogs. | ||
| Your task is to review the provided git diff, explain what changed and why (inferring the developer's intent), and assess the impact. | ||
| CRITICAL ACCURACY RULES: | ||
| 1. If a PROJECT KNOWLEDGE BASE section is provided, use it to understand the project's purpose, architecture, and naming \u2014 but base your explanation ONLY on what is actually present in the diff. | ||
| 2. Only mention file paths, function names, class names, or identifiers that explicitly appear in the diff. Do NOT invent or guess identifiers. | ||
| 3. If you cannot determine the intent of a change, say "intent unclear from diff" rather than fabricating a reason. | ||
| 4. Do NOT reference modules, files, or concepts from the project context unless they are directly touched by the diff. | ||
| 5. File paths in the "files" array must exactly match paths shown in the diff headers (e.g., "diff --git a/src/foo.ts"). | ||
| You must respond ONLY with a valid JSON object matching this schema: | ||
| { | ||
| "summary": "A concise, high-level explanation of the changes and why they were made in plain English.", | ||
| "impact": "none" | "minor" | "major" | "breaking", | ||
| "breaking": false, | ||
| "files": [ | ||
| { | ||
| "path": "path/to/file.ts", | ||
| "explanation": "Specific description of changes in this file." | ||
| } | ||
| ], | ||
| "relatedIssues": [] | ||
| } | ||
| Do not include any markdown framing (like \`\`\`json ... \`\`\`) in your direct response, only return raw JSON text. If you must use markdown backticks, ensure the JSON is still valid and parseable.`; | ||
| function parseAIJSONResponse(text) { | ||
| let cleaned = text.trim(); | ||
| if (cleaned.startsWith("```")) { | ||
| cleaned = cleaned.replace(/^```json\s*/i, "").replace(/```$/, "").trim(); | ||
| } | ||
| try { | ||
| const parsed = JSON.parse(cleaned); | ||
| return { | ||
| summary: parsed.summary || "No summary generated.", | ||
| impact: ["none", "minor", "major", "breaking"].includes(parsed.impact) ? parsed.impact : "minor", | ||
| breaking: Boolean(parsed.breaking), | ||
| files: Array.isArray(parsed.files) ? parsed.files : [], | ||
| relatedIssues: Array.isArray(parsed.relatedIssues) ? parsed.relatedIssues : [] | ||
| }; | ||
| } catch (err) { | ||
| console.error("Failed to parse AI JSON response:", cleaned); | ||
| return { | ||
| summary: cleaned.substring(0, 500), | ||
| impact: "minor", | ||
| breaking: false, | ||
| files: [], | ||
| relatedIssues: [] | ||
| }; | ||
| } | ||
| } | ||
| // src/ai/providers/ollama.ts | ||
| var OllamaNotAvailableError = class extends Error { | ||
| platform; | ||
| originalError; | ||
| constructor(message, details) { | ||
| super(message); | ||
| this.name = "OllamaNotAvailableError"; | ||
| this.platform = details.platform; | ||
| this.originalError = details.error; | ||
| } | ||
| }; | ||
| var DynamicTimeout = class { | ||
| static BASE_TIMEOUT_MS = 15e3; | ||
| // 15 seconds base | ||
| static PER_FILE_MS = 2e3; | ||
| // +2 seconds per file | ||
| static PER_1K_TOKENS_MS = 5e3; | ||
| // +5 seconds per 1K tokens | ||
| static MAX_TIMEOUT_MS = 3e5; | ||
| // 5 minutes absolute max | ||
| static MIN_TIMEOUT_MS = 1e4; | ||
| // 10 seconds minimum | ||
| /** | ||
| * Calculate appropriate timeout for this request | ||
| */ | ||
| static calculate(params) { | ||
| let timeout = this.BASE_TIMEOUT_MS; | ||
| timeout += params.fileCount * this.PER_FILE_MS; | ||
| const tokenThousands = params.estimatedTokens / 1e3; | ||
| timeout += tokenThousands * this.PER_1K_TOKENS_MS; | ||
| const modelSizeGB = parseFloat(params.modelSize) || 3; | ||
| if (modelSizeGB <= 3) { | ||
| timeout *= 1.5; | ||
| } else if (modelSizeGB <= 7) { | ||
| timeout *= 1.2; | ||
| } | ||
| if (params.historicalAvgMs) { | ||
| timeout = Math.max(timeout, params.historicalAvgMs * 1.5); | ||
| } | ||
| timeout = Math.max(this.MIN_TIMEOUT_MS, Math.min(this.MAX_TIMEOUT_MS, timeout)); | ||
| return Math.round(timeout); | ||
| } | ||
| /** | ||
| * Calculate for fallback model (extra time since it's second attempt) | ||
| */ | ||
| static calculateForFallback(params) { | ||
| const base = this.calculate(params); | ||
| const multiplier = 1 + params.attemptNumber * 0.5; | ||
| return Math.round(base * multiplier); | ||
| } | ||
| }; | ||
| var OllamaProvider = class { | ||
| name = "ollama"; | ||
| host; | ||
| timeoutMs; | ||
| performanceHistory = /* @__PURE__ */ new Map(); | ||
| constructor(host = "http://localhost:11434", timeoutMs) { | ||
| this.host = host; | ||
| this.timeoutMs = timeoutMs; | ||
| } | ||
| async generateExplanation(diffText, modelName, systemPrompt, attemptNumber) { | ||
| const url = `${this.host}/api/generate`; | ||
| const prompt = `System Instructions: | ||
| ${systemPrompt || SYSTEM_PROMPT} | ||
| Git Diff: | ||
| ${diffText}`; | ||
| const startTime = Date.now(); | ||
| const fileCount = (diffText.match(/^diff --git /gm) || []).length || 1; | ||
| const estimatedTokens = this.estimateTokens(diffText); | ||
| const modelSize = this.detectModelSize(modelName); | ||
| const historicalAvgMs = this.getHistoricalAverage(modelName); | ||
| let timeoutMs = this.timeoutMs; | ||
| if (timeoutMs === void 0) { | ||
| timeoutMs = attemptNumber && attemptNumber > 1 ? DynamicTimeout.calculateForFallback({ | ||
| fileCount, | ||
| estimatedTokens, | ||
| modelSize, | ||
| attemptNumber: attemptNumber - 1 | ||
| }) : DynamicTimeout.calculate({ | ||
| fileCount, | ||
| estimatedTokens, | ||
| modelSize, | ||
| historicalAvgMs | ||
| }); | ||
| } | ||
| console.log(`\u23F1\uFE0F Dynamic timeout: ${(timeoutMs / 1e3).toFixed(0)}s for ${fileCount} files (~${estimatedTokens} tokens)`); | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), timeoutMs); | ||
| try { | ||
| const response = await fetch(url, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| model: modelName, | ||
| prompt, | ||
| stream: false, | ||
| options: { | ||
| temperature: 0.2 | ||
| } | ||
| }), | ||
| signal: controller.signal | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Ollama returned status ${response.status}: ${await response.text()}` | ||
| ); | ||
| } | ||
| const data = await response.json(); | ||
| const elapsed = Date.now() - startTime; | ||
| this.recordPerformance(modelName, elapsed); | ||
| console.log(`\u2705 Ollama response: ${elapsed}ms (timeout was ${timeoutMs}ms)`); | ||
| return parseAIJSONResponse(data.response); | ||
| } catch (error) { | ||
| clearTimeout(timeoutId); | ||
| const elapsed = Date.now() - startTime; | ||
| if (error.name === "AbortError" || error.name === "TimeoutError" || error.message.includes("timed out")) { | ||
| console.log(`\u23F1\uFE0F Ollama timed out after ${elapsed}ms (timeout was ${timeoutMs}ms)`); | ||
| console.log(` Tip: ${fileCount} files may be too many for model ${modelName}`); | ||
| console.log(` Consider: devdiff generate --depth minimal (for faster results)`); | ||
| console.log(` Or split into smaller commits`); | ||
| throw new OllamaNotAvailableError( | ||
| `Ollama request timed out after ${timeoutMs}ms`, | ||
| { platform: process.platform, error } | ||
| ); | ||
| } | ||
| console.log(""); | ||
| console.log("\u274C Cannot connect to Ollama"); | ||
| console.log(""); | ||
| console.log( | ||
| " DevDiff uses Ollama for local AI. It needs to be running." | ||
| ); | ||
| console.log(""); | ||
| if (process.platform === "win32") { | ||
| console.log(" Windows:"); | ||
| console.log( | ||
| " 1. Download Ollama from: https://ollama.com/download/windows" | ||
| ); | ||
| console.log(" 2. Install and run the Ollama app"); | ||
| console.log(" 3. Open PowerShell and run: ollama pull llama3.2:3b"); | ||
| console.log(" 4. Verify it works: ollama list"); | ||
| console.log(" 5. Try again: devdiff generate"); | ||
| } else if (process.platform === "darwin") { | ||
| console.log(" macOS:"); | ||
| console.log(" 1. brew install ollama"); | ||
| console.log(" 2. ollama serve"); | ||
| console.log(" 3. ollama pull llama3.2:3b"); | ||
| } else { | ||
| console.log(" Linux:"); | ||
| console.log(" 1. curl -fsSL https://ollama.com/install.sh | sh"); | ||
| console.log(" 2. ollama serve"); | ||
| console.log(" 3. ollama pull llama3.2:3b"); | ||
| } | ||
| console.log(""); | ||
| console.log(" \u{1F4A1} No Ollama? You can use:"); | ||
| console.log(" \u2022 Dry run mode: devdiff generate --dry-run"); | ||
| console.log(" \u2022 Cloud AI: Set OPENAI_API_KEY in .env"); | ||
| console.log(" \u2022 WebGPU: Open dashboard at http://localhost:3737"); | ||
| console.log(""); | ||
| console.log(" Error details:", error.message); | ||
| console.log(""); | ||
| throw new OllamaNotAvailableError( | ||
| "Ollama is not running. Install from https://ollama.com", | ||
| { platform: process.platform, error } | ||
| ); | ||
| } | ||
| } | ||
| /** | ||
| * Estimate token count (fast approximation) | ||
| */ | ||
| estimateTokens(text) { | ||
| return Math.ceil(text.length / 3.5); | ||
| } | ||
| /** | ||
| * Detect model parameter size from name | ||
| */ | ||
| detectModelSize(modelName) { | ||
| const match = modelName.match(/(\d+\.?\d*)b/i); | ||
| return match ? match[1] + "b" : "3b"; | ||
| } | ||
| /** | ||
| * Get historical average response time | ||
| */ | ||
| getHistoricalAverage(modelName) { | ||
| const history = this.performanceHistory.get(modelName); | ||
| if (!history || history.length === 0) return void 0; | ||
| return history.reduce((sum, t) => sum + t, 0) / history.length; | ||
| } | ||
| /** | ||
| * Record performance for future timeout calculations | ||
| */ | ||
| recordPerformance(modelName, elapsedMs) { | ||
| const history = this.performanceHistory.get(modelName) || []; | ||
| history.push(elapsedMs); | ||
| if (history.length > 20) history.shift(); | ||
| this.performanceHistory.set(modelName, history); | ||
| } | ||
| }; | ||
| // src/ai/providers/openai.ts | ||
| var OpenAIProvider = class { | ||
| name = "openai"; | ||
| apiKey; | ||
| constructor(apiKey) { | ||
| this.apiKey = apiKey || process.env.OPENAI_API_KEY; | ||
| } | ||
| async generateExplanation(diffText, modelName, systemPrompt) { | ||
| const key = this.apiKey; | ||
| if (!key) { | ||
| throw new Error("OpenAI API Key is not configured."); | ||
| } | ||
| const url = "https://api.openai.com/v1/chat/completions"; | ||
| try { | ||
| const response = await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bearer ${key}` | ||
| }, | ||
| body: JSON.stringify({ | ||
| model: modelName, | ||
| messages: [ | ||
| { role: "system", content: systemPrompt || SYSTEM_PROMPT }, | ||
| { role: "user", content: `Analyze this diff: | ||
| ${diffText}` } | ||
| ], | ||
| temperature: 0.2, | ||
| response_format: { type: "json_object" } | ||
| }) | ||
| }); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `OpenAI API returned status ${response.status}: ${await response.text()}` | ||
| ); | ||
| } | ||
| const data = await response.json(); | ||
| const content = data.choices[0]?.message?.content || ""; | ||
| return parseAIJSONResponse(content); | ||
| } catch (error) { | ||
| console.error("OpenAI execution error:", error); | ||
| throw error; | ||
| } | ||
| } | ||
| }; | ||
| // src/ai/providers/gemini.ts | ||
| var GeminiProvider = class { | ||
| name = "gemini"; | ||
| apiKey; | ||
| constructor(apiKey) { | ||
| this.apiKey = apiKey || process.env.GEMINI_API_KEY; | ||
| } | ||
| async generateExplanation(diffText, modelName, systemPrompt) { | ||
| const key = this.apiKey; | ||
| if (!key) { | ||
| throw new Error("Gemini API Key is not configured."); | ||
| } | ||
| const model = modelName.includes("/") ? modelName : `models/${modelName}`; | ||
| const url = `https://generativelanguage.googleapis.com/v1beta/${model}:generateContent?key=${key}`; | ||
| try { | ||
| const response = await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify({ | ||
| contents: [ | ||
| { | ||
| parts: [{ text: `Analyze this diff: | ||
| ${diffText}` }] | ||
| } | ||
| ], | ||
| systemInstruction: { | ||
| parts: [{ text: systemPrompt || SYSTEM_PROMPT }] | ||
| }, | ||
| generationConfig: { | ||
| responseMimeType: "application/json", | ||
| temperature: 0.2 | ||
| } | ||
| }) | ||
| }); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Gemini API returned status ${response.status}: ${await response.text()}` | ||
| ); | ||
| } | ||
| const data = await response.json(); | ||
| const text = data.candidates?.[0]?.content?.parts?.[0]?.text || ""; | ||
| return parseAIJSONResponse(text); | ||
| } catch (error) { | ||
| console.error("Gemini execution error:", error); | ||
| throw error; | ||
| } | ||
| } | ||
| }; | ||
| // src/ai/providers/anthropic.ts | ||
| var AnthropicProvider = class { | ||
| name = "anthropic"; | ||
| apiKey; | ||
| constructor(apiKey) { | ||
| this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY; | ||
| } | ||
| async generateExplanation(diffText, modelName, systemPrompt) { | ||
| const key = this.apiKey; | ||
| if (!key) { | ||
| throw new Error("Anthropic API Key is not configured."); | ||
| } | ||
| const url = "https://api.anthropic.com/v1/messages"; | ||
| try { | ||
| const response = await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "x-api-key": key, | ||
| "anthropic-version": "2023-06-01" | ||
| }, | ||
| body: JSON.stringify({ | ||
| model: modelName, | ||
| max_tokens: 4e3, | ||
| system: systemPrompt || SYSTEM_PROMPT, | ||
| messages: [ | ||
| { role: "user", content: `Analyze this diff: | ||
| ${diffText}` } | ||
| ], | ||
| temperature: 0.2 | ||
| }) | ||
| }); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Anthropic API returned status ${response.status}: ${await response.text()}` | ||
| ); | ||
| } | ||
| const data = await response.json(); | ||
| const text = data.content?.[0]?.text || ""; | ||
| return parseAIJSONResponse(text); | ||
| } catch (error) { | ||
| console.error("Anthropic execution error:", error); | ||
| throw error; | ||
| } | ||
| } | ||
| }; | ||
| // src/ai/cache.ts | ||
| import * as fs2 from "fs/promises"; | ||
| import * as path2 from "path"; | ||
| import * as crypto2 from "crypto"; | ||
| var ExplanationCache = class { | ||
| cachePath; | ||
| enabled; | ||
| memoryCache = {}; | ||
| constructor(enabled = true, cachePath = ".devdiff/cache.json") { | ||
| this.enabled = enabled; | ||
| this.cachePath = cachePath; | ||
| } | ||
| hashDiff(diffText) { | ||
| return crypto2.createHash("sha256").update(diffText).digest("hex"); | ||
| } | ||
| async load() { | ||
| if (!this.enabled) return; | ||
| try { | ||
| const dir = path2.dirname(this.cachePath); | ||
| await fs2.mkdir(dir, { recursive: true }); | ||
| const content = await fs2.readFile(this.cachePath, "utf-8"); | ||
| this.memoryCache = JSON.parse(content); | ||
| } catch { | ||
| this.memoryCache = {}; | ||
| } | ||
| } | ||
| async get(diffText) { | ||
| if (!this.enabled) return null; | ||
| const hash = this.hashDiff(diffText); | ||
| if (Object.keys(this.memoryCache).length === 0) { | ||
| await this.load(); | ||
| } | ||
| return this.memoryCache[hash] || null; | ||
| } | ||
| async set(diffText, entry) { | ||
| if (!this.enabled) return; | ||
| const hash = this.hashDiff(diffText); | ||
| this.memoryCache[hash] = { | ||
| ...entry, | ||
| timestamp: (/* @__PURE__ */ new Date()).toISOString() | ||
| }; | ||
| try { | ||
| const dir = path2.dirname(this.cachePath); | ||
| await fs2.mkdir(dir, { recursive: true }); | ||
| await fs2.writeFile( | ||
| this.cachePath, | ||
| JSON.stringify(this.memoryCache, null, 2), | ||
| "utf-8" | ||
| ); | ||
| } catch (err) { | ||
| console.warn("Failed to save explanation cache to disk:", err); | ||
| } | ||
| } | ||
| }; | ||
| // src/resilience/vibe-coder-guardian.ts | ||
| import * as fs3 from "fs/promises"; | ||
| import * as path3 from "path"; | ||
| var VibeCoderGuardian = class { | ||
| session; | ||
| checkpointDir; | ||
| sessionPath; | ||
| constructor() { | ||
| this.session = { | ||
| startedAt: Date.now(), | ||
| changes: [], | ||
| checkpoints: [], | ||
| failures: [], | ||
| aiCalls: [] | ||
| }; | ||
| this.checkpointDir = path3.resolve(process.cwd(), ".devdiff/checkpoints"); | ||
| this.sessionPath = path3.resolve( | ||
| process.cwd(), | ||
| ".devdiff/vibe-session.json" | ||
| ); | ||
| } | ||
| async loadSession() { | ||
| try { | ||
| const data = await fs3.readFile(this.sessionPath, "utf-8"); | ||
| this.session = JSON.parse(data); | ||
| } catch { | ||
| } | ||
| } | ||
| async saveSession() { | ||
| await fs3.mkdir(path3.dirname(this.sessionPath), { recursive: true }); | ||
| await fs3.writeFile( | ||
| this.sessionPath, | ||
| JSON.stringify(this.session, null, 2), | ||
| "utf-8" | ||
| ); | ||
| } | ||
| async deleteSession() { | ||
| try { | ||
| await fs3.rm(this.sessionPath, { force: true }); | ||
| } catch { | ||
| } | ||
| } | ||
| /** | ||
| * Save checkpoint before any AI operation | ||
| */ | ||
| async preAICheckpoint(context) { | ||
| const snapshot = await this.captureSnapshot(context.files); | ||
| const checkpoint = { | ||
| id: `ckpt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, | ||
| timestamp: Date.now(), | ||
| type: "pre-ai-call", | ||
| snapshot, | ||
| metadata: { | ||
| trigger: "ai-call", | ||
| aiModel: context.model, | ||
| prompt: context.prompt.slice(0, 200) | ||
| } | ||
| }; | ||
| await this.saveCheckpoint(checkpoint); | ||
| this.session.checkpoints.push(checkpoint); | ||
| console.log( | ||
| [ | ||
| `\u{1F4BE} Checkpoint: ${checkpoint.id}`, | ||
| `\u{1F4C1} Files: ${context.files.length}`, | ||
| `\u{1F916} Model: ${context.model}`, | ||
| `\u{1F4DD} Prompt: ${context.prompt.slice(0, 100)}...`, | ||
| `\u23F1\uFE0F ${new Date(checkpoint.timestamp).toLocaleTimeString()}` | ||
| ].join("\n") | ||
| ); | ||
| return checkpoint; | ||
| } | ||
| async captureSnapshot(files) { | ||
| const snapshots = []; | ||
| for (const filename of files) { | ||
| try { | ||
| const absolutePath = path3.resolve(process.cwd(), filename); | ||
| const content = await fs3.readFile(absolutePath, "utf-8"); | ||
| snapshots.push([filename, content]); | ||
| } catch (err) { | ||
| } | ||
| } | ||
| return { files: snapshots }; | ||
| } | ||
| async saveCheckpoint(checkpoint) { | ||
| await fs3.mkdir(this.checkpointDir, { recursive: true }); | ||
| const checkpointFile = path3.join( | ||
| this.checkpointDir, | ||
| `${checkpoint.id}.json` | ||
| ); | ||
| await fs3.writeFile( | ||
| checkpointFile, | ||
| JSON.stringify(checkpoint, null, 2), | ||
| "utf-8" | ||
| ); | ||
| } | ||
| /** | ||
| * Handle AI failure with automatic recovery | ||
| */ | ||
| async handleFailure(failure) { | ||
| console.log(` | ||
| \u26A0\uFE0F AI CALL FAILED \u2014 Auto-recovery initiated`); | ||
| console.log(`\u274C Error: ${failure.error.message}`); | ||
| console.log(`\u{1F916} Model: ${failure.model}`); | ||
| console.log(`\u{1F504} Attempt: ${failure.attempt}/3`); | ||
| this.session.failures.push({ | ||
| timestamp: Date.now(), | ||
| error: failure.error.message, | ||
| model: failure.model, | ||
| attempt: failure.attempt, | ||
| checkpointId: failure.checkpointId | ||
| }); | ||
| const transparency = { | ||
| whatHappened: `AI call to ${failure.model} failed on attempt ${failure.attempt}`, | ||
| why: failure.error.message, | ||
| whatWasSaved: `Checkpoint ${failure.checkpointId} \u2014 ALL changes are safe`, | ||
| whatHappensNext: failure.attempt < 3 ? `Retrying with fallback model...` : "Restoring checkpoint. All changes preserved.", | ||
| howToManualRecover: `npx devdiff recover --checkpoint ${failure.checkpointId}` | ||
| }; | ||
| console.log("\n\u{1F4CA} TRANSPARENCY REPORT:"); | ||
| console.log(JSON.stringify(transparency, null, 2)); | ||
| if (failure.attempt < 3) { | ||
| const fallbackModel = await this.getFallbackModel(failure.model); | ||
| return { | ||
| status: "retrying", | ||
| message: `Switching to ${fallbackModel}`, | ||
| nextModel: fallbackModel, | ||
| checkpointId: failure.checkpointId, | ||
| transparency | ||
| }; | ||
| } | ||
| console.log("\n\u{1F6D1} Max retries exhausted. Restoring checkpoint..."); | ||
| await this.restoreCheckpoint(failure.checkpointId); | ||
| return { | ||
| status: "failed-recovered", | ||
| message: "All AI attempts failed. Changes restored from checkpoint. No work lost.", | ||
| checkpointId: failure.checkpointId, | ||
| restoredFrom: failure.checkpointId, | ||
| transparency, | ||
| manualActions: [ | ||
| "Check internet connection", | ||
| "Verify Ollama is running: ollama ps", | ||
| "Try different model: npx devdiff config set ai.model ollama://llama3.1:8b", | ||
| `Manual restore: npx devdiff recover --checkpoint ${failure.checkpointId}` | ||
| ] | ||
| }; | ||
| } | ||
| async getFallbackModel(model) { | ||
| try { | ||
| const { OllamaModelDiscovery: OllamaModelDiscovery2 } = await import("./ollama-discovery-GKAJCUAA.js"); | ||
| const installedModels = await OllamaModelDiscovery2.discoverModels(); | ||
| if (installedModels.length > 0) { | ||
| const currentModelName = model.replace("ollama://", ""); | ||
| const others = installedModels.filter((m) => m.name !== currentModelName); | ||
| if (others.length > 0) { | ||
| return `ollama://${others[0].name}`; | ||
| } | ||
| return model; | ||
| } | ||
| } catch { | ||
| } | ||
| if (model.includes("llama3.2:3b")) return "ollama://llama3.1:8b"; | ||
| return "ollama://llama3.2:3b"; | ||
| } | ||
| /** | ||
| * Restore from checkpoint — NEVER lose work | ||
| */ | ||
| async restoreCheckpoint(checkpointId) { | ||
| let checkpoint = this.session.checkpoints.find( | ||
| (c) => c.id === checkpointId | ||
| ); | ||
| if (!checkpoint) { | ||
| try { | ||
| const fileContent = await fs3.readFile( | ||
| path3.join(this.checkpointDir, `${checkpointId}.json`), | ||
| "utf-8" | ||
| ); | ||
| checkpoint = JSON.parse(fileContent); | ||
| } catch { | ||
| } | ||
| } | ||
| if (!checkpoint) throw new Error(`Checkpoint ${checkpointId} not found`); | ||
| console.log(` | ||
| \u{1F504} Restoring checkpoint ${checkpointId}...`); | ||
| for (const [filename, content] of checkpoint.snapshot.files) { | ||
| const absolutePath = path3.resolve(process.cwd(), filename); | ||
| await fs3.mkdir(path3.dirname(absolutePath), { recursive: true }); | ||
| await fs3.writeFile(absolutePath, content, "utf-8"); | ||
| console.log(` \u2705 Restored: ${filename}`); | ||
| } | ||
| console.log(` | ||
| \u2705 Restore complete. All files back to pre-AI state.`); | ||
| } | ||
| /** | ||
| * Generate session summary | ||
| */ | ||
| generateReport() { | ||
| return { | ||
| duration: Date.now() - this.session.startedAt, | ||
| totalChanges: this.session.changes.length, | ||
| checkpointsCreated: this.session.checkpoints.length, | ||
| aiCallsSucceeded: this.session.aiCalls.filter((c) => c.success).length, | ||
| aiCallsFailed: this.session.failures.length, | ||
| dataLossEvents: 0, | ||
| // Always 0 | ||
| guarantee: "\u2705 ZERO data loss. All changes preserved in local checkpoints.", | ||
| recommendations: this.generateRecommendations() | ||
| }; | ||
| } | ||
| generateRecommendations() { | ||
| return ["Keep llama3.1:8b as fallback"]; | ||
| } | ||
| // Helper to record changes and calls | ||
| recordChange(filename) { | ||
| this.session.changes.push({ filename, timestamp: Date.now() }); | ||
| } | ||
| recordAICall(model, success) { | ||
| this.session.aiCalls.push({ model, success, timestamp: Date.now() }); | ||
| } | ||
| }; | ||
| // src/errors/index.ts | ||
| var DevDiffError = class extends Error { | ||
| code; | ||
| exitCode; | ||
| httpStatus; | ||
| fix; | ||
| docsUrl; | ||
| context; | ||
| constructor(params) { | ||
| super(params.message); | ||
| this.name = "DevDiffError"; | ||
| this.code = params.code; | ||
| this.exitCode = params.exitCode || 1; | ||
| this.httpStatus = params.httpStatus || 500; | ||
| this.fix = params.fix || "Check the documentation for more information."; | ||
| this.docsUrl = params.docsUrl || "https://devdiff.vercel.app/troubleshooting/common-fixes"; | ||
| this.context = params.context || {}; | ||
| } | ||
| /** | ||
| * Pretty-print for CLI | ||
| */ | ||
| toCLIOutput() { | ||
| const lines = []; | ||
| lines.push(""); | ||
| lines.push(`\u274C ${this.message}`); | ||
| lines.push(""); | ||
| lines.push(` Error Code: ${this.code}`); | ||
| lines.push(` Fix: ${this.fix}`); | ||
| lines.push(` Docs: ${this.docsUrl}`); | ||
| if (Object.keys(this.context).length > 0) { | ||
| lines.push(""); | ||
| lines.push(" Context:"); | ||
| for (const [key, value] of Object.entries(this.context)) { | ||
| lines.push(` \u2022 ${key}: ${value}`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| return lines.join("\n"); | ||
| } | ||
| }; | ||
| var GitError = class extends DevDiffError { | ||
| constructor(message, context) { | ||
| super({ | ||
| code: "GIT_001", | ||
| message, | ||
| exitCode: 3, | ||
| fix: 'Ensure you are in a git repository with at least one commit.\nRun: git init && git add . && git commit -m "initial commit"', | ||
| docsUrl: "https://devdiff.vercel.app/troubleshooting/common-fixes", | ||
| context | ||
| }); | ||
| this.name = "GitError"; | ||
| } | ||
| }; | ||
| var AINotAvailableError = class extends DevDiffError { | ||
| constructor(provider, context) { | ||
| const fixes = { | ||
| ollama: "Install Ollama: https://ollama.com/download\nThen: ollama pull llama3.2:3b", | ||
| openai: "Set your API key: devdiff auth add openai\nOr: export OPENAI_API_KEY=your-key", | ||
| anthropic: "Set your API key: devdiff auth add anthropic\nOr: export ANTHROPIC_API_KEY=your-key" | ||
| }; | ||
| super({ | ||
| code: "AI_001", | ||
| message: `${provider} is not available`, | ||
| exitCode: 4, | ||
| fix: fixes[provider] || `Check your ${provider} configuration.`, | ||
| docsUrl: `https://devdiff.vercel.app/ai-providers/${provider}-setup`, | ||
| context | ||
| }); | ||
| this.name = "AINotAvailableError"; | ||
| } | ||
| }; | ||
| var ConfigError = class extends DevDiffError { | ||
| constructor(message, context) { | ||
| super({ | ||
| code: "CFG_001", | ||
| message, | ||
| exitCode: 2, | ||
| fix: "Run: devdiff config --validate (to check your configuration)\nRun: devdiff config --reset (to reset to defaults)", | ||
| docsUrl: "https://devdiff.vercel.app/guide/configuration", | ||
| context | ||
| }); | ||
| this.name = "ConfigError"; | ||
| } | ||
| }; | ||
| var NetworkError = class extends DevDiffError { | ||
| constructor(message, context) { | ||
| super({ | ||
| code: "NET_001", | ||
| message, | ||
| exitCode: 5, | ||
| fix: "Check your internet connection.\nIf using local AI, no internet is needed \u2014 check your AI provider.\nRun: devdiff doctor (for full diagnostics)", | ||
| docsUrl: "https://devdiff.vercel.app/troubleshooting/common-fixes", | ||
| context | ||
| }); | ||
| this.name = "NetworkError"; | ||
| } | ||
| }; | ||
| var PermissionError = class extends DevDiffError { | ||
| constructor(message, context) { | ||
| super({ | ||
| code: "PERM_001", | ||
| message, | ||
| exitCode: 6, | ||
| fix: "Check file permissions for .devdiff/ directory.\nOn Linux/macOS: chmod -R 755 .devdiff/\nOn Windows: Run terminal as Administrator", | ||
| docsUrl: "https://devdiff.vercel.app/troubleshooting/common-fixes", | ||
| context | ||
| }); | ||
| this.name = "PermissionError"; | ||
| } | ||
| }; | ||
| var ResourceLimitError = class extends DevDiffError { | ||
| constructor(resource, limit, current) { | ||
| super({ | ||
| code: "RES_001", | ||
| message: `${resource} limit reached (${current} / ${limit})`, | ||
| exitCode: 7, | ||
| fix: resource === "memory" ? "Try using a smaller AI model: ollama pull llama3.2:1b\nOr use a cloud provider: devdiff auth add openai" : "Free up disk space or change output directory.", | ||
| docsUrl: "https://devdiff.vercel.app/troubleshooting/common-fixes", | ||
| context: { resource, limit, current } | ||
| }); | ||
| this.name = "ResourceLimitError"; | ||
| } | ||
| }; | ||
| var NonInteractiveError = class extends DevDiffError { | ||
| constructor(message) { | ||
| super({ | ||
| code: "TTY_001", | ||
| message, | ||
| exitCode: 1, | ||
| fix: "This command requires an interactive terminal.\nFor scripts, use environment variables or config files.", | ||
| docsUrl: "https://devdiff.vercel.app/guide/configuration" | ||
| }); | ||
| this.name = "NonInteractiveError"; | ||
| } | ||
| }; | ||
| var MVPStorageError = class extends DevDiffError { | ||
| constructor(message, context) { | ||
| super({ | ||
| code: "MVP_001", | ||
| message, | ||
| exitCode: 1, | ||
| fix: "Run: devdiff mvp status (to check queue)\nRun: devdiff mvp process (to process queued items)", | ||
| docsUrl: "https://devdiff.vercel.app/features/mvp-mode", | ||
| context | ||
| }); | ||
| this.name = "MVPStorageError"; | ||
| } | ||
| }; | ||
| // src/ai/router.ts | ||
| var AIRouter = class { | ||
| config; | ||
| cache; | ||
| providers = {}; | ||
| models = { | ||
| "ollama://llama3.2:3b": { | ||
| name: "Llama 3.2 3B", | ||
| provider: "ollama", | ||
| maxContextTokens: 128e3, | ||
| maxOutputTokens: 8192, | ||
| costPer1kInput: 0, | ||
| costPer1kOutput: 0, | ||
| latencyMs: 500, | ||
| capabilities: { | ||
| codeAnalysis: 0.6, | ||
| securityAudit: 0.4, | ||
| refactoringDetection: 0.5, | ||
| multiFileAnalysis: 0.3, | ||
| structuredOutput: 0.7 | ||
| } | ||
| }, | ||
| "ollama://llama3.1:8b": { | ||
| name: "Llama 3.1 8B", | ||
| provider: "ollama", | ||
| maxContextTokens: 128e3, | ||
| maxOutputTokens: 8192, | ||
| costPer1kInput: 0, | ||
| costPer1kOutput: 0, | ||
| latencyMs: 1200, | ||
| capabilities: { | ||
| codeAnalysis: 0.8, | ||
| securityAudit: 0.7, | ||
| refactoringDetection: 0.75, | ||
| multiFileAnalysis: 0.7, | ||
| structuredOutput: 0.8 | ||
| } | ||
| }, | ||
| "ollama://codellama:13b": { | ||
| name: "CodeLlama 13B", | ||
| provider: "ollama", | ||
| maxContextTokens: 16384, | ||
| maxOutputTokens: 4096, | ||
| costPer1kInput: 0, | ||
| costPer1kOutput: 0, | ||
| latencyMs: 3e3, | ||
| capabilities: { | ||
| codeAnalysis: 0.9, | ||
| securityAudit: 0.85, | ||
| refactoringDetection: 0.9, | ||
| multiFileAnalysis: 0.6, | ||
| structuredOutput: 0.75 | ||
| } | ||
| }, | ||
| "openai://gpt-4o-mini": { | ||
| name: "GPT-4o Mini", | ||
| provider: "openai", | ||
| maxContextTokens: 128e3, | ||
| maxOutputTokens: 16384, | ||
| costPer1kInput: 15e-5, | ||
| costPer1kOutput: 6e-4, | ||
| latencyMs: 800, | ||
| capabilities: { | ||
| codeAnalysis: 0.85, | ||
| securityAudit: 0.8, | ||
| refactoringDetection: 0.85, | ||
| multiFileAnalysis: 0.9, | ||
| structuredOutput: 0.95 | ||
| } | ||
| }, | ||
| "openai://gpt-4o": { | ||
| name: "GPT-4o", | ||
| provider: "openai", | ||
| maxContextTokens: 128e3, | ||
| maxOutputTokens: 16384, | ||
| costPer1kInput: 25e-4, | ||
| costPer1kOutput: 0.01, | ||
| latencyMs: 1500, | ||
| capabilities: { | ||
| codeAnalysis: 0.95, | ||
| securityAudit: 0.95, | ||
| refactoringDetection: 0.95, | ||
| multiFileAnalysis: 0.95, | ||
| structuredOutput: 0.98 | ||
| } | ||
| } | ||
| }; | ||
| discoveredModels = []; | ||
| initialized = false; | ||
| constructor(config) { | ||
| this.config = config; | ||
| this.cache = new ExplanationCache(config.cache.enabled, config.cache.path); | ||
| this.providers["ollama"] = new OllamaProvider(); | ||
| this.providers["openai"] = new OpenAIProvider(); | ||
| this.providers["gemini"] = new GeminiProvider(); | ||
| this.providers["anthropic"] = new AnthropicProvider(); | ||
| } | ||
| async initialize() { | ||
| if (this.initialized) return; | ||
| this.discoveredModels = await OllamaModelDiscovery.discoverModels(); | ||
| for (const model of this.discoveredModels) { | ||
| const modelKey = `ollama://${model.name}`; | ||
| const isCodeSpecialist = ["codellama", "qwen", "deepseek"].includes( | ||
| model.family | ||
| ); | ||
| const paramSize = parseInt(model.parameterSize) || 3; | ||
| this.models[modelKey] = { | ||
| name: model.name, | ||
| provider: "ollama", | ||
| maxContextTokens: 128e3, | ||
| maxOutputTokens: 8192, | ||
| costPer1kInput: 0, | ||
| costPer1kOutput: 0, | ||
| latencyMs: paramSize > 13 ? 3e3 : paramSize > 7 ? 1500 : 800, | ||
| capabilities: { | ||
| codeAnalysis: isCodeSpecialist ? 0.9 : 0.7, | ||
| securityAudit: isCodeSpecialist ? 0.8 : 0.6, | ||
| refactoringDetection: isCodeSpecialist ? 0.95 : 0.7, | ||
| multiFileAnalysis: paramSize > 7 ? 0.8 : 0.5, | ||
| structuredOutput: 0.8 | ||
| } | ||
| }; | ||
| } | ||
| if (this.discoveredModels.length > 0) { | ||
| console.log(`\u{1F4E6} Found ${this.discoveredModels.length} Ollama model(s):`); | ||
| for (const model of this.discoveredModels) { | ||
| console.log( | ||
| ` \u2022 ${model.name} (${model.parameterSize}, ${model.family})` | ||
| ); | ||
| } | ||
| } else { | ||
| console.log("\u{1F4E6} No Ollama models found."); | ||
| console.log(" Install one: ollama pull llama3.2:3b"); | ||
| console.log(" Or pull a code specialist: ollama pull qwen2.5-coder:7b"); | ||
| } | ||
| this.initialized = true; | ||
| } | ||
| async getBestProvider() { | ||
| await this.initialize(); | ||
| if (this.discoveredModels.length > 0) { | ||
| const bestModel = OllamaModelDiscovery.selectBestModel( | ||
| this.discoveredModels, | ||
| this.config.preferredModel || this.config.ai?.preferredModel | ||
| // User's preference from config | ||
| ); | ||
| if (bestModel) { | ||
| console.log(`\u{1F916} Using: ${bestModel.name} (auto-detected, local, free)`); | ||
| return new OllamaProvider(); | ||
| } | ||
| } | ||
| if (process.env.OPENAI_API_KEY) { | ||
| console.log("\u{1F916} Using: OpenAI (cloud, your API key)"); | ||
| return new OpenAIProvider(process.env.OPENAI_API_KEY); | ||
| } | ||
| if (process.env.ANTHROPIC_API_KEY) { | ||
| console.log("\u{1F916} Using: Anthropic (cloud, your API key)"); | ||
| return new AnthropicProvider(process.env.ANTHROPIC_API_KEY); | ||
| } | ||
| throw new AINotAvailableError("local", { | ||
| discoveredModels: this.discoveredModels.map((m) => m.name), | ||
| hasCloudKeys: { | ||
| openai: !!process.env.OPENAI_API_KEY, | ||
| anthropic: !!process.env.ANTHROPIC_API_KEY, | ||
| groq: !!process.env.GROQ_API_KEY, | ||
| gemini: !!process.env.GEMINI_API_KEY | ||
| } | ||
| }); | ||
| } | ||
| /** | ||
| * Generate fallback chain from ACTUALLY AVAILABLE providers only | ||
| */ | ||
| getActualFallbackChain() { | ||
| const chain = []; | ||
| for (const model of this.discoveredModels) { | ||
| chain.push(`ollama://${model.name}`); | ||
| } | ||
| if (process.env.OPENAI_API_KEY) chain.push("openai://gpt-4o-mini"); | ||
| if (process.env.ANTHROPIC_API_KEY) chain.push("anthropic://claude-3-haiku"); | ||
| if (process.env.GROQ_API_KEY) chain.push("groq://llama3-70b"); | ||
| if (process.env.GEMINI_API_KEY) chain.push("gemini://gemini-1.5-flash"); | ||
| return chain; | ||
| } | ||
| parseUrl(url) { | ||
| const match = url.match(/^([^:]+):\/\/(.+)$/); | ||
| if (!match) { | ||
| return { providerType: "ollama", modelName: url }; | ||
| } | ||
| return { | ||
| providerType: match[1].toLowerCase(), | ||
| modelName: match[2] | ||
| }; | ||
| } | ||
| /** | ||
| * Route a change analysis to the optimal model based on context constraints | ||
| */ | ||
| route(depth, stats) { | ||
| const requirements = this.assessRequirements(depth, stats); | ||
| const candidates = Object.entries(this.models).filter( | ||
| ([_, model]) => this.meetsMinimumRequirements(model, requirements) | ||
| ).filter(([key, model]) => { | ||
| if (!this.initialized) return true; | ||
| if (model.provider === "openai" && !process.env.OPENAI_API_KEY) | ||
| return false; | ||
| if (model.provider === "anthropic" && !process.env.ANTHROPIC_API_KEY) | ||
| return false; | ||
| if (model.provider === "gemini" && !process.env.GEMINI_API_KEY) | ||
| return false; | ||
| if (model.provider === "ollama") { | ||
| return this.discoveredModels.some( | ||
| (m) => `ollama://${m.name}` === key | ||
| ); | ||
| } | ||
| return true; | ||
| }).sort( | ||
| (a, b) => this.scoreModel(b[1], requirements) - this.scoreModel(a[1], requirements) | ||
| ); | ||
| if (candidates.length === 0) { | ||
| const best = Object.entries(this.models).sort( | ||
| (a, b) => b[1].maxContextTokens - a[1].maxContextTokens | ||
| )[0]; | ||
| return { | ||
| model: best[0], | ||
| reason: "No model meets requirements. Using largest context window with truncation.", | ||
| estimatedTokens: stats.estimatedTokens, | ||
| estimatedCost: this.estimateCost(best[0], stats.estimatedTokens), | ||
| estimatedLatency: best[1].latencyMs, | ||
| willTruncate: true, | ||
| fallbackChain: [] | ||
| }; | ||
| } | ||
| const selected = candidates[0]; | ||
| return { | ||
| model: selected[0], | ||
| reason: this.explainRouting(selected[1], requirements), | ||
| estimatedTokens: stats.estimatedTokens, | ||
| estimatedCost: this.estimateCost(selected[0], stats.estimatedTokens), | ||
| estimatedLatency: selected[1].latencyMs, | ||
| willTruncate: stats.estimatedTokens > selected[1].maxContextTokens * 0.8, | ||
| fallbackChain: candidates.slice(1, 4).map((c) => c[0]) | ||
| }; | ||
| } | ||
| meetsMinimumRequirements(model, req) { | ||
| if (req.requiresSecurityAudit && model.capabilities.securityAudit < req.minimumCapabilityScore) { | ||
| return false; | ||
| } | ||
| if (req.requiresMultiFile && model.capabilities.multiFileAnalysis < req.minimumCapabilityScore) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| assessRequirements(depth, stats) { | ||
| const depthMultipliers = { | ||
| minimal: 0.3, | ||
| standard: 0.5, | ||
| deep: 0.8, | ||
| exhaustive: 1 | ||
| }; | ||
| const multiplier = depthMultipliers[depth] || 0.5; | ||
| return { | ||
| estimatedTokens: stats.estimatedTokens, | ||
| requiresCodeAnalysis: true, | ||
| requiresSecurityAudit: stats.hasBreakingChanges, | ||
| requiresMultiFile: stats.fileCount > 5, | ||
| requiresStructuredOutput: true, | ||
| minimumCapabilityScore: 0.3 + multiplier * 0.5, | ||
| complexityScore: this.calculateComplexity(stats) | ||
| }; | ||
| } | ||
| calculateComplexity(stats) { | ||
| let score = 0; | ||
| score += Math.min(stats.fileCount / 20, 1) * 0.3; | ||
| score += Math.min(stats.totalChanges / 500, 1) * 0.3; | ||
| score += Math.min(stats.maxASTDepth / 10, 1) * 0.2; | ||
| if (stats.hasBreakingChanges) score += 0.2; | ||
| return Math.min(score, 1); | ||
| } | ||
| scoreModel(model, req) { | ||
| let score = 0; | ||
| if (req.estimatedTokens <= model.maxContextTokens * 0.8) { | ||
| score += 0.3; | ||
| } else if (req.estimatedTokens <= model.maxContextTokens) { | ||
| score += 0.1; | ||
| } | ||
| score += model.capabilities.codeAnalysis * 0.25; | ||
| if (req.requiresSecurityAudit) { | ||
| score += model.capabilities.securityAudit * 0.2; | ||
| } | ||
| if (req.requiresMultiFile) { | ||
| score += model.capabilities.multiFileAnalysis * 0.15; | ||
| } | ||
| if (req.requiresStructuredOutput) { | ||
| score += model.capabilities.structuredOutput * 0.1; | ||
| } | ||
| if (model.costPer1kInput > 0) { | ||
| score -= 0.15; | ||
| } | ||
| if (req.complexityScore > 0.7 && model.latencyMs > 2e3) { | ||
| score -= 0.1; | ||
| } | ||
| return score; | ||
| } | ||
| explainRouting(model, req) { | ||
| const reasons = []; | ||
| if (model.costPer1kInput === 0) reasons.push("local/free"); | ||
| if (model.capabilities.codeAnalysis >= 0.9) | ||
| reasons.push("high code analysis capability"); | ||
| if (req.requiresSecurityAudit && model.capabilities.securityAudit >= 0.8) { | ||
| reasons.push("security audit capable"); | ||
| } | ||
| if (req.estimatedTokens > 8e3 && model.maxContextTokens >= 128e3) { | ||
| reasons.push("large context window"); | ||
| } | ||
| return `Selected ${model.name}: ${reasons.join(", ")}`; | ||
| } | ||
| estimateCost(modelKey, estimatedTokens) { | ||
| const model = this.models[modelKey]; | ||
| if (!model) return 0; | ||
| return estimatedTokens / 1e3 * model.costPer1kInput + estimatedTokens * 0.3 / 1e3 * model.costPer1kOutput; | ||
| } | ||
| async getExplanation(diffText, options) { | ||
| if (options?.dryRun) { | ||
| return { | ||
| summary: "[DRY RUN] Would call AI to generate explanation for this diff.", | ||
| impact: "none", | ||
| breaking: false, | ||
| files: [], | ||
| relatedIssues: [] | ||
| }; | ||
| } | ||
| const cached = await this.cache.get(diffText); | ||
| if (cached) { | ||
| return cached.result; | ||
| } | ||
| const charCount = diffText.length; | ||
| const estimatedTokens = Math.ceil(charCount / 4); | ||
| const fileLines = diffText.split("\n"); | ||
| const totalChanges = fileLines.filter( | ||
| (l) => l.startsWith("+") || l.startsWith("-") | ||
| ).length; | ||
| const stats = { | ||
| fileCount: (diffText.match(/^diff --git /gm) || []).length || 1, | ||
| totalChanges, | ||
| maxASTDepth: 5, | ||
| hasBreakingChanges: diffText.includes("breaking") || diffText.includes("BREAKING CHANGE"), | ||
| estimatedTokens | ||
| }; | ||
| await this.initialize(); | ||
| const decision = this.route(options?.depth || "standard", stats); | ||
| console.log( | ||
| `[Intelligent Router] Decision: ${decision.model} - ${decision.reason}` | ||
| ); | ||
| const { providerType, modelName } = this.parseUrl(decision.model); | ||
| const provider = this.providers[providerType]; | ||
| if (!provider) { | ||
| throw new Error(`Unsupported routed provider type: ${providerType}`); | ||
| } | ||
| if (providerType === "ollama" && options?.timeoutMs !== void 0) { | ||
| provider.timeoutMs = options.timeoutMs; | ||
| } | ||
| const matchedConfig = this.config.ai.providers.find( | ||
| (p) => p.url.startsWith(providerType) | ||
| ); | ||
| if (matchedConfig?.apiKey) { | ||
| if (providerType === "openai") { | ||
| this.providers["openai"] = new OpenAIProvider(matchedConfig.apiKey); | ||
| } else if (providerType === "gemini") { | ||
| this.providers["gemini"] = new GeminiProvider(matchedConfig.apiKey); | ||
| } else if (providerType === "anthropic") { | ||
| this.providers["anthropic"] = new AnthropicProvider( | ||
| matchedConfig.apiKey | ||
| ); | ||
| } | ||
| } | ||
| let customSystemPrompt = SYSTEM_PROMPT; | ||
| if (options?.personaId) { | ||
| try { | ||
| const { PersonaRegistry, PersonaEngine } = await import("@eldrex/personas"); | ||
| const persona = PersonaRegistry.get(options.personaId); | ||
| if (persona) { | ||
| customSystemPrompt = `${SYSTEM_PROMPT} | ||
| ${PersonaEngine.generateSystemPrompt(persona)}`; | ||
| } | ||
| } catch (err) { | ||
| } | ||
| } | ||
| let sessionActive = false; | ||
| let guardian = null; | ||
| let checkpoint = null; | ||
| try { | ||
| const sessionPath = path4.resolve( | ||
| process.cwd(), | ||
| ".devdiff/vibe-session.json" | ||
| ); | ||
| await fs4.access(sessionPath); | ||
| sessionActive = true; | ||
| } catch { | ||
| } | ||
| if (sessionActive) { | ||
| try { | ||
| guardian = new VibeCoderGuardian(); | ||
| await guardian.loadSession(); | ||
| let modifiedFiles = []; | ||
| try { | ||
| const stdout = await ShellSandbox.exec("git", [ | ||
| "status", | ||
| "--porcelain" | ||
| ]); | ||
| modifiedFiles = stdout.split("\n").map((line) => line.slice(3).trim()).filter(Boolean); | ||
| } catch { | ||
| } | ||
| checkpoint = await guardian.preAICheckpoint({ | ||
| files: modifiedFiles, | ||
| model: decision.model, | ||
| prompt: diffText | ||
| }); | ||
| await guardian.saveSession(); | ||
| } catch (gErr) { | ||
| console.warn("VibeCoderGuardian failed to create checkpoint:", gErr); | ||
| } | ||
| } | ||
| try { | ||
| const contextualDiff = options?.projectContext ? `${options.projectContext} | ||
| ${diffText}` : diffText; | ||
| const result = await provider.generateExplanation( | ||
| contextualDiff, | ||
| modelName, | ||
| customSystemPrompt | ||
| ); | ||
| if (guardian) { | ||
| guardian.recordAICall(decision.model, true); | ||
| await guardian.saveSession(); | ||
| } | ||
| await this.cache.set(diffText, { | ||
| result, | ||
| provider: providerType, | ||
| model: modelName | ||
| }); | ||
| return result; | ||
| } catch (err) { | ||
| if (guardian && checkpoint) { | ||
| guardian.recordAICall(decision.model, false); | ||
| const recovery = await guardian.handleFailure({ | ||
| error: err, | ||
| model: decision.model, | ||
| checkpointId: checkpoint.id, | ||
| attempt: 1 | ||
| }); | ||
| await guardian.saveSession(); | ||
| if (recovery.status === "retrying" && recovery.nextModel) { | ||
| try { | ||
| const fb = this.parseUrl(recovery.nextModel); | ||
| const fbProvider = this.providers[fb.providerType] || this.providers["ollama"]; | ||
| const result = await fbProvider.generateExplanation( | ||
| diffText, | ||
| fb.modelName, | ||
| customSystemPrompt | ||
| ); | ||
| guardian.recordAICall(recovery.nextModel, true); | ||
| await guardian.saveSession(); | ||
| return result; | ||
| } catch (retryErr) { | ||
| const finalRecovery = await guardian.handleFailure({ | ||
| error: retryErr, | ||
| model: recovery.nextModel, | ||
| checkpointId: checkpoint.id, | ||
| attempt: 3 | ||
| }); | ||
| await guardian.saveSession(); | ||
| throw new Error(finalRecovery.message); | ||
| } | ||
| } else { | ||
| throw new Error(recovery.message); | ||
| } | ||
| } | ||
| console.warn( | ||
| `Routed AI provider ${decision.model} failed. Falling back to chain: ${decision.fallbackChain.join(", ")}` | ||
| ); | ||
| for (const fallbackModel of decision.fallbackChain) { | ||
| try { | ||
| const fb = this.parseUrl(fallbackModel); | ||
| const fbProvider = this.providers[fb.providerType]; | ||
| if (fbProvider) { | ||
| const result = await fbProvider.generateExplanation( | ||
| diffText, | ||
| fb.modelName, | ||
| customSystemPrompt | ||
| ); | ||
| return result; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| throw err; | ||
| } | ||
| } | ||
| }; | ||
| export { | ||
| SecurityAudit, | ||
| ShellAccessDeniedError, | ||
| ShellSandbox, | ||
| SHELL_ACCESS_CONFIG, | ||
| SYSTEM_PROMPT, | ||
| parseAIJSONResponse, | ||
| DynamicTimeout, | ||
| ExplanationCache, | ||
| VibeCoderGuardian, | ||
| DevDiffError, | ||
| GitError, | ||
| AINotAvailableError, | ||
| ConfigError, | ||
| NetworkError, | ||
| PermissionError, | ||
| ResourceLimitError, | ||
| NonInteractiveError, | ||
| MVPStorageError, | ||
| AIRouter | ||
| }; | ||
| //# sourceMappingURL=chunk-MNEDYP6H.js.map |
Sorry, the diff of this file is too big to display
| import { | ||
| generateContextFile, | ||
| injectContextIntoPrompt, | ||
| loadContext, | ||
| validateContextFile | ||
| } from "./chunk-47DLDZSM.js"; | ||
| export { | ||
| generateContextFile, | ||
| injectContextIntoPrompt, | ||
| loadContext, | ||
| validateContextFile | ||
| }; | ||
| //# sourceMappingURL=compiler-2EDKNVTK.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| import { | ||
| loadConfig | ||
| } from "./chunk-3PFQZWQS.js"; | ||
| export { | ||
| loadConfig | ||
| }; | ||
| //# sourceMappingURL=loader-3RF7VRAS.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| import { | ||
| OllamaModelDiscovery | ||
| } from "./chunk-HRKIFQSQ.js"; | ||
| export { | ||
| OllamaModelDiscovery | ||
| }; | ||
| //# sourceMappingURL=ollama-discovery-GKAJCUAA.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| import { | ||
| AIRouter | ||
| } from "./chunk-MNEDYP6H.js"; | ||
| import "./chunk-HRKIFQSQ.js"; | ||
| export { | ||
| AIRouter | ||
| }; | ||
| //# sourceMappingURL=router-ZAGMO2XR.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
+3
-2
| { | ||
| "name": "@eldrex/core", | ||
| "version": "1.0.5", | ||
| "version": "1.0.6", | ||
| "publishConfig": { | ||
@@ -28,3 +28,4 @@ "registry": "https://registry.npmjs.org" | ||
| "chalk": "^5.3.0", | ||
| "@eldrex/personas": "1.0.4" | ||
| "@eldrex/personas": "1.0.6", | ||
| "@eldrex/plugin-sdk": "1.0.6" | ||
| }, | ||
@@ -31,0 +32,0 @@ "devDependencies": { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1744266
37.76%25
177.78%19492
36.68%7
16.67%116
6.42%22
10%+ Added
+ Added
+ Added
- Removed
Updated