@dreb/coding-agent 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"forbidden-commands.d.ts","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmNH;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAgEhG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,EAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAgB7D","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"forbidden-commands.d.ts","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAkOH;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAgEhG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,EAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAgB7D","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n\t// Sensitive file access — block reading credential files via bash\n\t// Matches bare commands AND absolute-path invocations (/bin/cat, /usr/bin/cat, etc.)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\", // SSH private keys (not .pub)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\", // dreb credential store\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\", // dreb auth storage\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\", // AWS credentials\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\", // GPG private keys\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\", // GCloud credentials\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n\t// Sensitive file access in quoted content\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\",\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
@@ -29,6 +29,14 @@ const DEFAULT_FORBIDDEN_PATTERNS = [
29
29
  "^dd\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)", // dd writing to block devices
30
30
  "^mkfs", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)
31
31
  "^>>?\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)", // redirect to block device (> and >>)
32
+ // Sensitive file access — block reading credential files via bash
33
+ // Matches bare commands AND absolute-path invocations (/bin/cat, /usr/bin/cat, etc.)
34
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*(?:~|\\.ssh)/id_(?!.*\\.pub\\b)", // SSH private keys (not .pub)
35
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.dreb/secrets/", // dreb credential store
36
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.dreb/agent/auth\\.json", // dreb auth storage
37
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.aws/credentials", // AWS credentials
38
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.gnupg/private-keys", // GPG private keys
39
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.config/gcloud/credentials\\.db", // GCloud credentials
32
40
  ];
33
41
  /**
34
42
  * Patterns checked against the full (quote-masked) command string before
@@ -64,6 +72,13 @@ const QUOTED_CONTENT_PATTERNS = [
64
72
  "^gh api.*bypass",
65
73
  "^git\\s+commit.*--no-verify",
66
74
  ":\\(\\)\\s*\\{", // fork bomb
75
+ // Sensitive file access in quoted content
76
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*(?:~|\\.ssh)/id_(?!.*\\.pub\\b)",
77
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.dreb/secrets/",
78
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.dreb/agent/auth\\.json",
79
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.aws/credentials",
80
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.gnupg/private-keys",
81
+ "^(?:/\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\s+.*\\.config/gcloud/credentials\\.db",
67
82
  ];
68
83
  /**
69
84
  * Mask content inside single and double-quoted strings by replacing
@@ -1 +1 @@
1
- {"version":3,"file":"forbidden-commands.js","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"forbidden-commands.js","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n\t// Sensitive file access — block reading credential files via bash\n\t// Matches bare commands AND absolute-path invocations (/bin/cat, /usr/bin/cat, etc.)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\", // SSH private keys (not .pub)\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\", // dreb credential store\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\", // dreb auth storage\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\", // AWS credentials\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\", // GPG private keys\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\", // GCloud credentials\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n\t// Sensitive file access in quoted content\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*(?:~|\\\\.ssh)/id_(?!.*\\\\.pub\\\\b)\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/secrets/\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.dreb/agent/auth\\\\.json\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.aws/credentials\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.gnupg/private-keys\",\n\t\"^(?:/\\\\S+/)?(?:cat|head|tail|less|more|strings|grep|sed|awk|base64|xxd)\\\\s+.*\\\\.config/gcloud/credentials\\\\.db\",\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"model-resolver.d.ts","sourceRoot":"","sources":["../../src/core/model-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model resolution, scoping, and initial selection\n */\n\nimport type { ThinkingLevel } from \"@dreb/agent-core\";\nimport { type Api, findModelInList, type KnownProvider, type Model, modelsAreEqual } from \"@dreb/ai\";\nimport chalk from \"chalk\";\nimport { minimatch } from \"minimatch\";\nimport { isValidThinkingLevel } from \"../cli/args.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ModelRegistry } from \"./model-registry.js\";\n\n/** Default model IDs for each known provider */\nexport const defaultModelPerProvider: Record<KnownProvider, string> = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\tanthropic: \"claude-opus-4-6\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.2\",\n\t\"openai-codex\": \"gpt-5.4\",\n\tgoogle: \"gemini-2.5-pro\",\n\t\"google-gemini-cli\": \"gemini-2.5-pro\",\n\t\"google-antigravity\": \"gemini-3.1-pro-high\",\n\t\"google-vertex\": \"gemini-3-pro-preview\",\n\t\"github-copilot\": \"gpt-4o\",\n\topenrouter: \"openai/gpt-5.1-codex\",\n\t\"vercel-ai-gateway\": \"anthropic/claude-opus-4-6\",\n\txai: \"grok-4-fast-non-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"gpt-oss-120b\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\thuggingface: \"moonshotai/Kimi-K2.5\",\n\topencode: \"claude-opus-4-6\",\n\t\"opencode-go\": \"kimi-k2.5\",\n\t\"kimi-coding\": \"kimi-k2-thinking\",\n};\n\nexport interface ScopedModel {\n\tmodel: Model<Api>;\n\t/** Thinking level if explicitly specified in pattern (e.g., \"model:high\"), undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n}\n\n// isAlias logic moved to @dreb/ai as isModelAlias\n\n/**\n * Find an exact model reference match.\n * Supports either a bare model id or a canonical provider/modelId reference.\n * When matching by bare id, ambiguous matches across providers are rejected.\n */\nexport function findExactModelReferenceMatch(\n\tmodelReference: string,\n\tavailableModels: Model<Api>[],\n): Model<Api> | undefined {\n\tconst trimmedReference = modelReference.trim();\n\tif (!trimmedReference) {\n\t\treturn undefined;\n\t}\n\n\tconst normalizedReference = trimmedReference.toLowerCase();\n\n\tconst canonicalMatches = availableModels.filter(\n\t\t(model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,\n\t);\n\tif (canonicalMatches.length === 1) {\n\t\treturn canonicalMatches[0];\n\t}\n\tif (canonicalMatches.length > 1) {\n\t\treturn undefined;\n\t}\n\n\tconst slashIndex = trimmedReference.indexOf(\"/\");\n\tif (slashIndex !== -1) {\n\t\tconst provider = trimmedReference.substring(0, slashIndex).trim();\n\t\tconst modelId = trimmedReference.substring(slashIndex + 1).trim();\n\t\tif (provider && modelId) {\n\t\t\tconst providerMatches = availableModels.filter(\n\t\t\t\t(model) =>\n\t\t\t\t\tmodel.provider.toLowerCase() === provider.toLowerCase() &&\n\t\t\t\t\tmodel.id.toLowerCase() === modelId.toLowerCase(),\n\t\t\t);\n\t\t\tif (providerMatches.length === 1) {\n\t\t\t\treturn providerMatches[0];\n\t\t\t}\n\t\t\tif (providerMatches.length > 1) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);\n\treturn idMatches.length === 1 ? idMatches[0] : undefined;\n}\n\n/**\n * Try to match a pattern to a model from the available models list.\n *\n * Extends findModelInList() with support for canonical `provider/model`\n * references via findExactModelReferenceMatch.\n */\nfunction tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\t// Try canonical provider/model and cross-provider exact matches first\n\tconst exactMatch = findExactModelReferenceMatch(modelPattern, availableModels);\n\tif (exactMatch) return exactMatch;\n\n\t// Delegate fuzzy matching to @dreb/ai\n\treturn findModelInList(modelPattern, availableModels);\n}\n\nexport interface ParsedModelResult {\n\tmodel: Model<Api> | undefined;\n\t/** Thinking level if explicitly specified in pattern, undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n}\n\nfunction buildFallbackModel(provider: string, modelId: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\tconst providerModels = availableModels.filter((m) => m.provider === provider);\n\tif (providerModels.length === 0) return undefined;\n\n\tconst defaultId = defaultModelPerProvider[provider as KnownProvider];\n\tconst baseModel = defaultId\n\t\t? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0])\n\t\t: providerModels[0];\n\n\treturn {\n\t\t...baseModel,\n\t\tid: modelId,\n\t\tname: modelId,\n\t};\n}\n\n/**\n * Parse a pattern to extract model and thinking level.\n * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).\n *\n * Algorithm:\n * 1. Try to match full pattern as a model\n * 2. If found, return it with \"off\" thinking level\n * 3. If not found and has colons, split on last colon:\n * - If suffix is valid thinking level, use it and recurse on prefix\n * - If suffix is invalid, warn and recurse on prefix with \"off\"\n *\n * @internal Exported for testing\n */\nexport function parseModelPattern(\n\tpattern: string,\n\tavailableModels: Model<Api>[],\n\toptions?: { allowInvalidThinkingLevelFallback?: boolean },\n): ParsedModelResult {\n\t// Try exact match first\n\tconst exactMatch = tryMatchModel(pattern, availableModels);\n\tif (exactMatch) {\n\t\treturn { model: exactMatch, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\t// No match - try splitting on last colon if present\n\tconst lastColonIndex = pattern.lastIndexOf(\":\");\n\tif (lastColonIndex === -1) {\n\t\t// No colons, pattern simply doesn't match any model\n\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\tconst prefix = pattern.substring(0, lastColonIndex);\n\tconst suffix = pattern.substring(lastColonIndex + 1);\n\n\tif (isValidThinkingLevel(suffix)) {\n\t\t// Valid thinking level - recurse on prefix and use this level\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\t// Only use this thinking level if no warning from inner recursion\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: result.warning ? undefined : suffix,\n\t\t\t\twarning: result.warning,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t} else {\n\t\t// Invalid suffix\n\t\tconst allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;\n\t\tif (!allowFallback) {\n\t\t\t// In strict mode (CLI --model parsing), treat it as part of the model id and fail.\n\t\t\t// This avoids accidentally resolving to a different model.\n\t\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t\t}\n\n\t\t// Scope mode: recurse on prefix and warn\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: `Invalid thinking level \"${suffix}\" in pattern \"${pattern}\". Using default instead.`,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Resolve model patterns to actual Model objects with optional thinking levels\n * Format: \"pattern:level\" where :level is optional\n * For each pattern, finds all matching models and picks the best version:\n * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)\n * 2. If no alias, pick the latest dated version\n *\n * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).\n * The algorithm tries to match the full pattern first, then progressively\n * strips colon-suffixes to find a match.\n */\nexport async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {\n\tconst availableModels = await modelRegistry.getAvailable();\n\tconst scopedModels: ScopedModel[] = [];\n\n\tfor (const pattern of patterns) {\n\t\t// Check if pattern contains glob characters\n\t\tif (pattern.includes(\"*\") || pattern.includes(\"?\") || pattern.includes(\"[\")) {\n\t\t\t// Extract optional thinking level suffix (e.g., \"provider/*:high\")\n\t\t\tconst colonIdx = pattern.lastIndexOf(\":\");\n\t\t\tlet globPattern = pattern;\n\t\t\tlet thinkingLevel: ThinkingLevel | undefined;\n\n\t\t\tif (colonIdx !== -1) {\n\t\t\t\tconst suffix = pattern.substring(colonIdx + 1);\n\t\t\t\tif (isValidThinkingLevel(suffix)) {\n\t\t\t\t\tthinkingLevel = suffix;\n\t\t\t\t\tglobPattern = pattern.substring(0, colonIdx);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Match against \"provider/modelId\" format OR just model ID\n\t\t\t// This allows \"*sonnet*\" to match without requiring \"anthropic/*sonnet*\"\n\t\t\tconst matchingModels = availableModels.filter((m) => {\n\t\t\t\tconst fullId = `${m.provider}/${m.id}`;\n\t\t\t\treturn minimatch(fullId, globPattern, { nocase: true }) || minimatch(m.id, globPattern, { nocase: true });\n\t\t\t});\n\n\t\t\tif (matchingModels.length === 0) {\n\t\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const model of matchingModels) {\n\t\t\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);\n\n\t\tif (warning) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: ${warning}`));\n\t\t}\n\n\t\tif (!model) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Avoid duplicates\n\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t}\n\t}\n\n\treturn scopedModels;\n}\n\nexport interface ResolveCliModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n\t/**\n\t * Error message suitable for CLI display.\n\t * When set, model will be undefined.\n\t */\n\terror: string | undefined;\n\t/**\n\t * True when the model was constructed as a synthetic fallback (unknown model ID\n\t * with a known provider). Useful for subagent spawning where synthetic models\n\t * should be rejected in favor of trying the next fallback.\n\t */\n\tisSyntheticFallback?: boolean;\n}\n\n/**\n * Resolve a single model from CLI flags.\n *\n * Supports:\n * - --provider <provider> --model <pattern>\n * - --model <provider>/<pattern>\n * - Fuzzy matching (same rules as model scoping: exact id, then partial id/name)\n *\n * Note: This does not apply the thinking level by itself, but it may *parse* and\n * return a thinking level from \"<pattern>:<thinking>\" so the caller can apply it.\n */\nexport function resolveCliModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tmodelRegistry: ModelRegistry;\n}): ResolveCliModelResult {\n\tconst { cliProvider, cliModel, modelRegistry } = options;\n\n\tif (!cliModel) {\n\t\treturn { model: undefined, warning: undefined, error: undefined };\n\t}\n\n\t// Important: use *all* models here, not just models with pre-configured auth.\n\t// This allows \"--api-key\" to be used for first-time setup.\n\tconst availableModels = modelRegistry.getAll();\n\tif (availableModels.length === 0) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: \"No models available. Check your installation or add models to models.json.\",\n\t\t};\n\t}\n\n\t// Build canonical provider lookup (case-insensitive)\n\tconst providerMap = new Map<string, string>();\n\tfor (const m of availableModels) {\n\t\tproviderMap.set(m.provider.toLowerCase(), m.provider);\n\t}\n\n\tlet provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;\n\tif (cliProvider && !provider) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: `Unknown provider \"${cliProvider}\". Use --list-models to see available providers/models.`,\n\t\t};\n\t}\n\n\t// If no explicit --provider, try to interpret \"provider/model\" format first.\n\t// When the prefix before the first slash matches a known provider, prefer that\n\t// interpretation over matching models whose IDs literally contain slashes\n\t// (e.g. \"zai/glm-5\" should resolve to provider=zai, model=glm-5, not to a\n\t// vercel-ai-gateway model with id \"zai/glm-5\").\n\tlet pattern = cliModel;\n\tlet inferredProvider = false;\n\n\tif (!provider) {\n\t\tconst slashIndex = cliModel.indexOf(\"/\");\n\t\tif (slashIndex !== -1) {\n\t\t\tconst maybeProvider = cliModel.substring(0, slashIndex);\n\t\t\tconst canonical = providerMap.get(maybeProvider.toLowerCase());\n\t\t\tif (canonical) {\n\t\t\t\tprovider = canonical;\n\t\t\t\tpattern = cliModel.substring(slashIndex + 1);\n\t\t\t\tinferredProvider = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t// If no provider was inferred from the slash, try exact matches without provider inference.\n\t// This handles models whose IDs naturally contain slashes (e.g. OpenRouter-style IDs).\n\tif (!provider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t}\n\n\tif (cliProvider && provider) {\n\t\t// If both were provided, tolerate --model <provider>/<pattern> by stripping the provider prefix\n\t\tconst prefix = `${provider}/`;\n\t\tif (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {\n\t\t\tpattern = cliModel.substring(prefix.length);\n\t\t}\n\t}\n\n\tconst candidates = provider ? availableModels.filter((m) => m.provider === provider) : availableModels;\n\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, {\n\t\tallowInvalidThinkingLevelFallback: false,\n\t});\n\n\tif (model) {\n\t\treturn { model, thinkingLevel, warning, error: undefined };\n\t}\n\n\t// If we inferred a provider from the slash but found no match within that provider,\n\t// fall back to matching the full input as a raw model id across all models.\n\t// This handles OpenRouter-style IDs like \"openai/gpt-4o:extended\" where \"openai\"\n\t// looks like a provider but the full string is actually a model id on openrouter.\n\tif (inferredProvider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t\t// Also try parseModelPattern on the full input against all models\n\t\tconst fallback = parseModelPattern(cliModel, availableModels, {\n\t\t\tallowInvalidThinkingLevelFallback: false,\n\t\t});\n\t\tif (fallback.model) {\n\t\t\treturn {\n\t\t\t\tmodel: fallback.model,\n\t\t\t\tthinkingLevel: fallback.thinkingLevel,\n\t\t\t\twarning: fallback.warning,\n\t\t\t\terror: undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (provider) {\n\t\tconst fallbackModel = buildFallbackModel(provider, pattern, availableModels);\n\t\tif (fallbackModel) {\n\t\t\tconst fallbackWarning = warning\n\t\t\t\t? `${warning} Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`\n\t\t\t\t: `Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`;\n\t\t\treturn {\n\t\t\t\tmodel: fallbackModel,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: fallbackWarning,\n\t\t\t\terror: undefined,\n\t\t\t\tisSyntheticFallback: true,\n\t\t\t};\n\t\t}\n\t}\n\n\tconst display = provider ? `${provider}/${pattern}` : cliModel;\n\treturn {\n\t\tmodel: undefined,\n\t\tthinkingLevel: undefined,\n\t\twarning,\n\t\terror: `Model \"${display}\" not found. Use --list-models to see available models.`,\n\t};\n}\n\nexport interface InitialModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel: ThinkingLevel;\n\tfallbackMessage: string | undefined;\n}\n\n/**\n * Find the initial model to use based on priority:\n * 1. CLI args (provider + model)\n * 2. First model from scoped models (if not continuing/resuming)\n * 3. Restored from session (if continuing/resuming)\n * 4. Saved default from settings\n * 5. First available model with valid API key\n */\nexport async function findInitialModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tscopedModels: ScopedModel[];\n\tisContinuing: boolean;\n\tdefaultProvider?: string;\n\tdefaultModelId?: string;\n\tdefaultThinkingLevel?: ThinkingLevel;\n\tmodelRegistry: ModelRegistry;\n}): Promise<InitialModelResult> {\n\tconst {\n\t\tcliProvider,\n\t\tcliModel,\n\t\tscopedModels,\n\t\tisContinuing,\n\t\tdefaultProvider,\n\t\tdefaultModelId,\n\t\tdefaultThinkingLevel,\n\t\tmodelRegistry,\n\t} = options;\n\n\tlet model: Model<Api> | undefined;\n\tlet thinkingLevel: ThinkingLevel = DEFAULT_THINKING_LEVEL;\n\n\t// 1. CLI args take priority\n\tif (cliProvider && cliModel) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider,\n\t\t\tcliModel,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.error) {\n\t\t\tconsole.error(chalk.red(resolved.error));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tif (resolved.model) {\n\t\t\treturn { model: resolved.model, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 2. Use first model from scoped models (skip if continuing/resuming)\n\tif (scopedModels.length > 0 && !isContinuing) {\n\t\treturn {\n\t\t\tmodel: scopedModels[0].model,\n\t\t\tthinkingLevel: scopedModels[0].thinkingLevel ?? defaultThinkingLevel ?? DEFAULT_THINKING_LEVEL,\n\t\t\tfallbackMessage: undefined,\n\t\t};\n\t}\n\n\t// 3. Try saved default from settings\n\tif (defaultProvider && defaultModelId) {\n\t\tconst found = modelRegistry.find(defaultProvider, defaultModelId);\n\t\tif (found) {\n\t\t\tmodel = found;\n\t\t\tif (defaultThinkingLevel) {\n\t\t\t\tthinkingLevel = defaultThinkingLevel;\n\t\t\t}\n\t\t\treturn { model, thinkingLevel, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 4. Try first available model with valid API key\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\treturn { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\treturn { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t}\n\n\t// 5. No model found\n\treturn { model: undefined, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n}\n\n/**\n * Restore model from session, with fallback to available models\n */\nexport async function restoreModelFromSession(\n\tsavedProvider: string,\n\tsavedModelId: string,\n\tcurrentModel: Model<Api> | undefined,\n\tshouldPrintMessages: boolean,\n\tmodelRegistry: ModelRegistry,\n): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {\n\tconst restoredModel = modelRegistry.find(savedProvider, savedModelId);\n\n\t// Check if restored model exists and has a valid API key\n\tconst hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;\n\n\tif (restoredModel && hasApiKey) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));\n\t\t}\n\t\treturn { model: restoredModel, fallbackMessage: undefined };\n\t}\n\n\t// Model not found or no API key - fall back\n\tconst reason = !restoredModel ? \"model no longer exists\" : \"no API key available\";\n\n\tif (shouldPrintMessages) {\n\t\tconsole.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));\n\t}\n\n\t// If we already have a model, use it as fallback\n\tif (currentModel) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`));\n\t\t}\n\t\treturn {\n\t\t\tmodel: currentModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`,\n\t\t};\n\t}\n\n\t// Try to find any available model\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tlet fallbackModel: Model<Api> | undefined;\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\tfallbackModel = match;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\tif (!fallbackModel) {\n\t\t\tfallbackModel = availableModels[0];\n\t\t}\n\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));\n\t\t}\n\n\t\treturn {\n\t\t\tmodel: fallbackModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`,\n\t\t};\n\t}\n\n\t// No models available\n\treturn { model: undefined, fallbackMessage: undefined };\n}\n"]}
1
+ {"version":3,"file":"model-resolver.d.ts","sourceRoot":"","sources":["../../src/core/model-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model resolution, scoping, and initial selection\n */\n\nimport type { ThinkingLevel } from \"@dreb/agent-core\";\nimport { type Api, findModelInList, type KnownProvider, type Model, modelsAreEqual } from \"@dreb/ai\";\nimport chalk from \"chalk\";\nimport { minimatch } from \"minimatch\";\nimport { isValidThinkingLevel } from \"../cli/args.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ModelRegistry } from \"./model-registry.js\";\n\n/** Default model IDs for each known provider */\nexport const defaultModelPerProvider: Record<KnownProvider, string> = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\tanthropic: \"claude-opus-4-6\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.2\",\n\t\"openai-codex\": \"gpt-5.4\",\n\tgoogle: \"gemini-2.5-pro\",\n\t\"google-gemini-cli\": \"gemini-2.5-pro\",\n\t\"google-antigravity\": \"gemini-3.1-pro-high\",\n\t\"google-vertex\": \"gemini-3-pro-preview\",\n\t\"github-copilot\": \"gpt-4o\",\n\topenrouter: \"openai/gpt-5.1-codex\",\n\t\"vercel-ai-gateway\": \"anthropic/claude-opus-4-6\",\n\txai: \"grok-4-fast-non-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"gpt-oss-120b\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\thuggingface: \"moonshotai/Kimi-K2.5\",\n\topencode: \"claude-opus-4-6\",\n\t\"opencode-go\": \"kimi-k2.5\",\n\t\"kimi-coding\": \"kimi-k2-thinking\",\n\t\"kimi-coding-oauth\": \"kimi-for-coding\",\n};\n\nexport interface ScopedModel {\n\tmodel: Model<Api>;\n\t/** Thinking level if explicitly specified in pattern (e.g., \"model:high\"), undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n}\n\n// isAlias logic moved to @dreb/ai as isModelAlias\n\n/**\n * Find an exact model reference match.\n * Supports either a bare model id or a canonical provider/modelId reference.\n * When matching by bare id, ambiguous matches across providers are rejected.\n */\nexport function findExactModelReferenceMatch(\n\tmodelReference: string,\n\tavailableModels: Model<Api>[],\n): Model<Api> | undefined {\n\tconst trimmedReference = modelReference.trim();\n\tif (!trimmedReference) {\n\t\treturn undefined;\n\t}\n\n\tconst normalizedReference = trimmedReference.toLowerCase();\n\n\tconst canonicalMatches = availableModels.filter(\n\t\t(model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,\n\t);\n\tif (canonicalMatches.length === 1) {\n\t\treturn canonicalMatches[0];\n\t}\n\tif (canonicalMatches.length > 1) {\n\t\treturn undefined;\n\t}\n\n\tconst slashIndex = trimmedReference.indexOf(\"/\");\n\tif (slashIndex !== -1) {\n\t\tconst provider = trimmedReference.substring(0, slashIndex).trim();\n\t\tconst modelId = trimmedReference.substring(slashIndex + 1).trim();\n\t\tif (provider && modelId) {\n\t\t\tconst providerMatches = availableModels.filter(\n\t\t\t\t(model) =>\n\t\t\t\t\tmodel.provider.toLowerCase() === provider.toLowerCase() &&\n\t\t\t\t\tmodel.id.toLowerCase() === modelId.toLowerCase(),\n\t\t\t);\n\t\t\tif (providerMatches.length === 1) {\n\t\t\t\treturn providerMatches[0];\n\t\t\t}\n\t\t\tif (providerMatches.length > 1) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);\n\treturn idMatches.length === 1 ? idMatches[0] : undefined;\n}\n\n/**\n * Try to match a pattern to a model from the available models list.\n *\n * Extends findModelInList() with support for canonical `provider/model`\n * references via findExactModelReferenceMatch.\n */\nfunction tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\t// Try canonical provider/model and cross-provider exact matches first\n\tconst exactMatch = findExactModelReferenceMatch(modelPattern, availableModels);\n\tif (exactMatch) return exactMatch;\n\n\t// Delegate fuzzy matching to @dreb/ai\n\treturn findModelInList(modelPattern, availableModels);\n}\n\nexport interface ParsedModelResult {\n\tmodel: Model<Api> | undefined;\n\t/** Thinking level if explicitly specified in pattern, undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n}\n\nfunction buildFallbackModel(provider: string, modelId: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\tconst providerModels = availableModels.filter((m) => m.provider === provider);\n\tif (providerModels.length === 0) return undefined;\n\n\tconst defaultId = defaultModelPerProvider[provider as KnownProvider];\n\tconst baseModel = defaultId\n\t\t? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0])\n\t\t: providerModels[0];\n\n\treturn {\n\t\t...baseModel,\n\t\tid: modelId,\n\t\tname: modelId,\n\t};\n}\n\n/**\n * Parse a pattern to extract model and thinking level.\n * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).\n *\n * Algorithm:\n * 1. Try to match full pattern as a model\n * 2. If found, return it with \"off\" thinking level\n * 3. If not found and has colons, split on last colon:\n * - If suffix is valid thinking level, use it and recurse on prefix\n * - If suffix is invalid, warn and recurse on prefix with \"off\"\n *\n * @internal Exported for testing\n */\nexport function parseModelPattern(\n\tpattern: string,\n\tavailableModels: Model<Api>[],\n\toptions?: { allowInvalidThinkingLevelFallback?: boolean },\n): ParsedModelResult {\n\t// Try exact match first\n\tconst exactMatch = tryMatchModel(pattern, availableModels);\n\tif (exactMatch) {\n\t\treturn { model: exactMatch, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\t// No match - try splitting on last colon if present\n\tconst lastColonIndex = pattern.lastIndexOf(\":\");\n\tif (lastColonIndex === -1) {\n\t\t// No colons, pattern simply doesn't match any model\n\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\tconst prefix = pattern.substring(0, lastColonIndex);\n\tconst suffix = pattern.substring(lastColonIndex + 1);\n\n\tif (isValidThinkingLevel(suffix)) {\n\t\t// Valid thinking level - recurse on prefix and use this level\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\t// Only use this thinking level if no warning from inner recursion\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: result.warning ? undefined : suffix,\n\t\t\t\twarning: result.warning,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t} else {\n\t\t// Invalid suffix\n\t\tconst allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;\n\t\tif (!allowFallback) {\n\t\t\t// In strict mode (CLI --model parsing), treat it as part of the model id and fail.\n\t\t\t// This avoids accidentally resolving to a different model.\n\t\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t\t}\n\n\t\t// Scope mode: recurse on prefix and warn\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: `Invalid thinking level \"${suffix}\" in pattern \"${pattern}\". Using default instead.`,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Resolve model patterns to actual Model objects with optional thinking levels\n * Format: \"pattern:level\" where :level is optional\n * For each pattern, finds all matching models and picks the best version:\n * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)\n * 2. If no alias, pick the latest dated version\n *\n * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).\n * The algorithm tries to match the full pattern first, then progressively\n * strips colon-suffixes to find a match.\n */\nexport async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {\n\tconst availableModels = await modelRegistry.getAvailable();\n\tconst scopedModels: ScopedModel[] = [];\n\n\tfor (const pattern of patterns) {\n\t\t// Check if pattern contains glob characters\n\t\tif (pattern.includes(\"*\") || pattern.includes(\"?\") || pattern.includes(\"[\")) {\n\t\t\t// Extract optional thinking level suffix (e.g., \"provider/*:high\")\n\t\t\tconst colonIdx = pattern.lastIndexOf(\":\");\n\t\t\tlet globPattern = pattern;\n\t\t\tlet thinkingLevel: ThinkingLevel | undefined;\n\n\t\t\tif (colonIdx !== -1) {\n\t\t\t\tconst suffix = pattern.substring(colonIdx + 1);\n\t\t\t\tif (isValidThinkingLevel(suffix)) {\n\t\t\t\t\tthinkingLevel = suffix;\n\t\t\t\t\tglobPattern = pattern.substring(0, colonIdx);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Match against \"provider/modelId\" format OR just model ID\n\t\t\t// This allows \"*sonnet*\" to match without requiring \"anthropic/*sonnet*\"\n\t\t\tconst matchingModels = availableModels.filter((m) => {\n\t\t\t\tconst fullId = `${m.provider}/${m.id}`;\n\t\t\t\treturn minimatch(fullId, globPattern, { nocase: true }) || minimatch(m.id, globPattern, { nocase: true });\n\t\t\t});\n\n\t\t\tif (matchingModels.length === 0) {\n\t\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const model of matchingModels) {\n\t\t\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);\n\n\t\tif (warning) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: ${warning}`));\n\t\t}\n\n\t\tif (!model) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Avoid duplicates\n\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t}\n\t}\n\n\treturn scopedModels;\n}\n\nexport interface ResolveCliModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n\t/**\n\t * Error message suitable for CLI display.\n\t * When set, model will be undefined.\n\t */\n\terror: string | undefined;\n\t/**\n\t * True when the model was constructed as a synthetic fallback (unknown model ID\n\t * with a known provider). Useful for subagent spawning where synthetic models\n\t * should be rejected in favor of trying the next fallback.\n\t */\n\tisSyntheticFallback?: boolean;\n}\n\n/**\n * Resolve a single model from CLI flags.\n *\n * Supports:\n * - --provider <provider> --model <pattern>\n * - --model <provider>/<pattern>\n * - Fuzzy matching (same rules as model scoping: exact id, then partial id/name)\n *\n * Note: This does not apply the thinking level by itself, but it may *parse* and\n * return a thinking level from \"<pattern>:<thinking>\" so the caller can apply it.\n */\nexport function resolveCliModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tmodelRegistry: ModelRegistry;\n}): ResolveCliModelResult {\n\tconst { cliProvider, cliModel, modelRegistry } = options;\n\n\tif (!cliModel) {\n\t\treturn { model: undefined, warning: undefined, error: undefined };\n\t}\n\n\t// Important: use *all* models here, not just models with pre-configured auth.\n\t// This allows \"--api-key\" to be used for first-time setup.\n\tconst availableModels = modelRegistry.getAll();\n\tif (availableModels.length === 0) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: \"No models available. Check your installation or add models to models.json.\",\n\t\t};\n\t}\n\n\t// Build canonical provider lookup (case-insensitive)\n\tconst providerMap = new Map<string, string>();\n\tfor (const m of availableModels) {\n\t\tproviderMap.set(m.provider.toLowerCase(), m.provider);\n\t}\n\n\tlet provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;\n\tif (cliProvider && !provider) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: `Unknown provider \"${cliProvider}\". Use --list-models to see available providers/models.`,\n\t\t};\n\t}\n\n\t// If no explicit --provider, try to interpret \"provider/model\" format first.\n\t// When the prefix before the first slash matches a known provider, prefer that\n\t// interpretation over matching models whose IDs literally contain slashes\n\t// (e.g. \"zai/glm-5\" should resolve to provider=zai, model=glm-5, not to a\n\t// vercel-ai-gateway model with id \"zai/glm-5\").\n\tlet pattern = cliModel;\n\tlet inferredProvider = false;\n\n\tif (!provider) {\n\t\tconst slashIndex = cliModel.indexOf(\"/\");\n\t\tif (slashIndex !== -1) {\n\t\t\tconst maybeProvider = cliModel.substring(0, slashIndex);\n\t\t\tconst canonical = providerMap.get(maybeProvider.toLowerCase());\n\t\t\tif (canonical) {\n\t\t\t\tprovider = canonical;\n\t\t\t\tpattern = cliModel.substring(slashIndex + 1);\n\t\t\t\tinferredProvider = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t// If no provider was inferred from the slash, try exact matches without provider inference.\n\t// This handles models whose IDs naturally contain slashes (e.g. OpenRouter-style IDs).\n\tif (!provider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t}\n\n\tif (cliProvider && provider) {\n\t\t// If both were provided, tolerate --model <provider>/<pattern> by stripping the provider prefix\n\t\tconst prefix = `${provider}/`;\n\t\tif (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {\n\t\t\tpattern = cliModel.substring(prefix.length);\n\t\t}\n\t}\n\n\tconst candidates = provider ? availableModels.filter((m) => m.provider === provider) : availableModels;\n\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, {\n\t\tallowInvalidThinkingLevelFallback: false,\n\t});\n\n\tif (model) {\n\t\treturn { model, thinkingLevel, warning, error: undefined };\n\t}\n\n\t// If we inferred a provider from the slash but found no match within that provider,\n\t// fall back to matching the full input as a raw model id across all models.\n\t// This handles OpenRouter-style IDs like \"openai/gpt-4o:extended\" where \"openai\"\n\t// looks like a provider but the full string is actually a model id on openrouter.\n\tif (inferredProvider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t\t// Also try parseModelPattern on the full input against all models\n\t\tconst fallback = parseModelPattern(cliModel, availableModels, {\n\t\t\tallowInvalidThinkingLevelFallback: false,\n\t\t});\n\t\tif (fallback.model) {\n\t\t\treturn {\n\t\t\t\tmodel: fallback.model,\n\t\t\t\tthinkingLevel: fallback.thinkingLevel,\n\t\t\t\twarning: fallback.warning,\n\t\t\t\terror: undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (provider) {\n\t\tconst fallbackModel = buildFallbackModel(provider, pattern, availableModels);\n\t\tif (fallbackModel) {\n\t\t\tconst fallbackWarning = warning\n\t\t\t\t? `${warning} Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`\n\t\t\t\t: `Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`;\n\t\t\treturn {\n\t\t\t\tmodel: fallbackModel,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: fallbackWarning,\n\t\t\t\terror: undefined,\n\t\t\t\tisSyntheticFallback: true,\n\t\t\t};\n\t\t}\n\t}\n\n\tconst display = provider ? `${provider}/${pattern}` : cliModel;\n\treturn {\n\t\tmodel: undefined,\n\t\tthinkingLevel: undefined,\n\t\twarning,\n\t\terror: `Model \"${display}\" not found. Use --list-models to see available models.`,\n\t};\n}\n\nexport interface InitialModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel: ThinkingLevel;\n\tfallbackMessage: string | undefined;\n}\n\n/**\n * Find the initial model to use based on priority:\n * 1. CLI args (provider + model)\n * 2. First model from scoped models (if not continuing/resuming)\n * 3. Restored from session (if continuing/resuming)\n * 4. Saved default from settings\n * 5. First available model with valid API key\n */\nexport async function findInitialModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tscopedModels: ScopedModel[];\n\tisContinuing: boolean;\n\tdefaultProvider?: string;\n\tdefaultModelId?: string;\n\tdefaultThinkingLevel?: ThinkingLevel;\n\tmodelRegistry: ModelRegistry;\n}): Promise<InitialModelResult> {\n\tconst {\n\t\tcliProvider,\n\t\tcliModel,\n\t\tscopedModels,\n\t\tisContinuing,\n\t\tdefaultProvider,\n\t\tdefaultModelId,\n\t\tdefaultThinkingLevel,\n\t\tmodelRegistry,\n\t} = options;\n\n\tlet model: Model<Api> | undefined;\n\tlet thinkingLevel: ThinkingLevel = DEFAULT_THINKING_LEVEL;\n\n\t// 1. CLI args take priority\n\tif (cliProvider && cliModel) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider,\n\t\t\tcliModel,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.error) {\n\t\t\tconsole.error(chalk.red(resolved.error));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tif (resolved.model) {\n\t\t\treturn { model: resolved.model, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 2. Use first model from scoped models (skip if continuing/resuming)\n\tif (scopedModels.length > 0 && !isContinuing) {\n\t\treturn {\n\t\t\tmodel: scopedModels[0].model,\n\t\t\tthinkingLevel: scopedModels[0].thinkingLevel ?? defaultThinkingLevel ?? DEFAULT_THINKING_LEVEL,\n\t\t\tfallbackMessage: undefined,\n\t\t};\n\t}\n\n\t// 3. Try saved default from settings\n\tif (defaultProvider && defaultModelId) {\n\t\tconst found = modelRegistry.find(defaultProvider, defaultModelId);\n\t\tif (found) {\n\t\t\tmodel = found;\n\t\t\tif (defaultThinkingLevel) {\n\t\t\t\tthinkingLevel = defaultThinkingLevel;\n\t\t\t}\n\t\t\treturn { model, thinkingLevel, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 4. Try first available model with valid API key\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\treturn { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\treturn { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t}\n\n\t// 5. No model found\n\treturn { model: undefined, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n}\n\n/**\n * Restore model from session, with fallback to available models\n */\nexport async function restoreModelFromSession(\n\tsavedProvider: string,\n\tsavedModelId: string,\n\tcurrentModel: Model<Api> | undefined,\n\tshouldPrintMessages: boolean,\n\tmodelRegistry: ModelRegistry,\n): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {\n\tconst restoredModel = modelRegistry.find(savedProvider, savedModelId);\n\n\t// Check if restored model exists and has a valid API key\n\tconst hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;\n\n\tif (restoredModel && hasApiKey) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));\n\t\t}\n\t\treturn { model: restoredModel, fallbackMessage: undefined };\n\t}\n\n\t// Model not found or no API key - fall back\n\tconst reason = !restoredModel ? \"model no longer exists\" : \"no API key available\";\n\n\tif (shouldPrintMessages) {\n\t\tconsole.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));\n\t}\n\n\t// If we already have a model, use it as fallback\n\tif (currentModel) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`));\n\t\t}\n\t\treturn {\n\t\t\tmodel: currentModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`,\n\t\t};\n\t}\n\n\t// Try to find any available model\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tlet fallbackModel: Model<Api> | undefined;\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\tfallbackModel = match;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\tif (!fallbackModel) {\n\t\t\tfallbackModel = availableModels[0];\n\t\t}\n\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));\n\t\t}\n\n\t\treturn {\n\t\t\tmodel: fallbackModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`,\n\t\t};\n\t}\n\n\t// No models available\n\treturn { model: undefined, fallbackMessage: undefined };\n}\n"]}
@@ -30,6 +30,7 @@ export const defaultModelPerProvider = {
30
30
  opencode: "claude-opus-4-6",
31
31
  "opencode-go": "kimi-k2.5",
32
32
  "kimi-coding": "kimi-k2-thinking",
33
+ "kimi-coding-oauth": "kimi-for-coding",
33
34
  };
34
35
  // isAlias logic moved to @dreb/ai as isModelAlias
35
36
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"model-resolver.js","sourceRoot":"","sources":["../../src/core/model-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model resolution, scoping, and initial selection\n */\n\nimport type { ThinkingLevel } from \"@dreb/agent-core\";\nimport { type Api, findModelInList, type KnownProvider, type Model, modelsAreEqual } from \"@dreb/ai\";\nimport chalk from \"chalk\";\nimport { minimatch } from \"minimatch\";\nimport { isValidThinkingLevel } from \"../cli/args.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ModelRegistry } from \"./model-registry.js\";\n\n/** Default model IDs for each known provider */\nexport const defaultModelPerProvider: Record<KnownProvider, string> = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\tanthropic: \"claude-opus-4-6\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.2\",\n\t\"openai-codex\": \"gpt-5.4\",\n\tgoogle: \"gemini-2.5-pro\",\n\t\"google-gemini-cli\": \"gemini-2.5-pro\",\n\t\"google-antigravity\": \"gemini-3.1-pro-high\",\n\t\"google-vertex\": \"gemini-3-pro-preview\",\n\t\"github-copilot\": \"gpt-4o\",\n\topenrouter: \"openai/gpt-5.1-codex\",\n\t\"vercel-ai-gateway\": \"anthropic/claude-opus-4-6\",\n\txai: \"grok-4-fast-non-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"gpt-oss-120b\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\thuggingface: \"moonshotai/Kimi-K2.5\",\n\topencode: \"claude-opus-4-6\",\n\t\"opencode-go\": \"kimi-k2.5\",\n\t\"kimi-coding\": \"kimi-k2-thinking\",\n};\n\nexport interface ScopedModel {\n\tmodel: Model<Api>;\n\t/** Thinking level if explicitly specified in pattern (e.g., \"model:high\"), undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n}\n\n// isAlias logic moved to @dreb/ai as isModelAlias\n\n/**\n * Find an exact model reference match.\n * Supports either a bare model id or a canonical provider/modelId reference.\n * When matching by bare id, ambiguous matches across providers are rejected.\n */\nexport function findExactModelReferenceMatch(\n\tmodelReference: string,\n\tavailableModels: Model<Api>[],\n): Model<Api> | undefined {\n\tconst trimmedReference = modelReference.trim();\n\tif (!trimmedReference) {\n\t\treturn undefined;\n\t}\n\n\tconst normalizedReference = trimmedReference.toLowerCase();\n\n\tconst canonicalMatches = availableModels.filter(\n\t\t(model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,\n\t);\n\tif (canonicalMatches.length === 1) {\n\t\treturn canonicalMatches[0];\n\t}\n\tif (canonicalMatches.length > 1) {\n\t\treturn undefined;\n\t}\n\n\tconst slashIndex = trimmedReference.indexOf(\"/\");\n\tif (slashIndex !== -1) {\n\t\tconst provider = trimmedReference.substring(0, slashIndex).trim();\n\t\tconst modelId = trimmedReference.substring(slashIndex + 1).trim();\n\t\tif (provider && modelId) {\n\t\t\tconst providerMatches = availableModels.filter(\n\t\t\t\t(model) =>\n\t\t\t\t\tmodel.provider.toLowerCase() === provider.toLowerCase() &&\n\t\t\t\t\tmodel.id.toLowerCase() === modelId.toLowerCase(),\n\t\t\t);\n\t\t\tif (providerMatches.length === 1) {\n\t\t\t\treturn providerMatches[0];\n\t\t\t}\n\t\t\tif (providerMatches.length > 1) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);\n\treturn idMatches.length === 1 ? idMatches[0] : undefined;\n}\n\n/**\n * Try to match a pattern to a model from the available models list.\n *\n * Extends findModelInList() with support for canonical `provider/model`\n * references via findExactModelReferenceMatch.\n */\nfunction tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\t// Try canonical provider/model and cross-provider exact matches first\n\tconst exactMatch = findExactModelReferenceMatch(modelPattern, availableModels);\n\tif (exactMatch) return exactMatch;\n\n\t// Delegate fuzzy matching to @dreb/ai\n\treturn findModelInList(modelPattern, availableModels);\n}\n\nexport interface ParsedModelResult {\n\tmodel: Model<Api> | undefined;\n\t/** Thinking level if explicitly specified in pattern, undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n}\n\nfunction buildFallbackModel(provider: string, modelId: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\tconst providerModels = availableModels.filter((m) => m.provider === provider);\n\tif (providerModels.length === 0) return undefined;\n\n\tconst defaultId = defaultModelPerProvider[provider as KnownProvider];\n\tconst baseModel = defaultId\n\t\t? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0])\n\t\t: providerModels[0];\n\n\treturn {\n\t\t...baseModel,\n\t\tid: modelId,\n\t\tname: modelId,\n\t};\n}\n\n/**\n * Parse a pattern to extract model and thinking level.\n * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).\n *\n * Algorithm:\n * 1. Try to match full pattern as a model\n * 2. If found, return it with \"off\" thinking level\n * 3. If not found and has colons, split on last colon:\n * - If suffix is valid thinking level, use it and recurse on prefix\n * - If suffix is invalid, warn and recurse on prefix with \"off\"\n *\n * @internal Exported for testing\n */\nexport function parseModelPattern(\n\tpattern: string,\n\tavailableModels: Model<Api>[],\n\toptions?: { allowInvalidThinkingLevelFallback?: boolean },\n): ParsedModelResult {\n\t// Try exact match first\n\tconst exactMatch = tryMatchModel(pattern, availableModels);\n\tif (exactMatch) {\n\t\treturn { model: exactMatch, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\t// No match - try splitting on last colon if present\n\tconst lastColonIndex = pattern.lastIndexOf(\":\");\n\tif (lastColonIndex === -1) {\n\t\t// No colons, pattern simply doesn't match any model\n\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\tconst prefix = pattern.substring(0, lastColonIndex);\n\tconst suffix = pattern.substring(lastColonIndex + 1);\n\n\tif (isValidThinkingLevel(suffix)) {\n\t\t// Valid thinking level - recurse on prefix and use this level\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\t// Only use this thinking level if no warning from inner recursion\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: result.warning ? undefined : suffix,\n\t\t\t\twarning: result.warning,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t} else {\n\t\t// Invalid suffix\n\t\tconst allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;\n\t\tif (!allowFallback) {\n\t\t\t// In strict mode (CLI --model parsing), treat it as part of the model id and fail.\n\t\t\t// This avoids accidentally resolving to a different model.\n\t\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t\t}\n\n\t\t// Scope mode: recurse on prefix and warn\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: `Invalid thinking level \"${suffix}\" in pattern \"${pattern}\". Using default instead.`,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Resolve model patterns to actual Model objects with optional thinking levels\n * Format: \"pattern:level\" where :level is optional\n * For each pattern, finds all matching models and picks the best version:\n * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)\n * 2. If no alias, pick the latest dated version\n *\n * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).\n * The algorithm tries to match the full pattern first, then progressively\n * strips colon-suffixes to find a match.\n */\nexport async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {\n\tconst availableModels = await modelRegistry.getAvailable();\n\tconst scopedModels: ScopedModel[] = [];\n\n\tfor (const pattern of patterns) {\n\t\t// Check if pattern contains glob characters\n\t\tif (pattern.includes(\"*\") || pattern.includes(\"?\") || pattern.includes(\"[\")) {\n\t\t\t// Extract optional thinking level suffix (e.g., \"provider/*:high\")\n\t\t\tconst colonIdx = pattern.lastIndexOf(\":\");\n\t\t\tlet globPattern = pattern;\n\t\t\tlet thinkingLevel: ThinkingLevel | undefined;\n\n\t\t\tif (colonIdx !== -1) {\n\t\t\t\tconst suffix = pattern.substring(colonIdx + 1);\n\t\t\t\tif (isValidThinkingLevel(suffix)) {\n\t\t\t\t\tthinkingLevel = suffix;\n\t\t\t\t\tglobPattern = pattern.substring(0, colonIdx);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Match against \"provider/modelId\" format OR just model ID\n\t\t\t// This allows \"*sonnet*\" to match without requiring \"anthropic/*sonnet*\"\n\t\t\tconst matchingModels = availableModels.filter((m) => {\n\t\t\t\tconst fullId = `${m.provider}/${m.id}`;\n\t\t\t\treturn minimatch(fullId, globPattern, { nocase: true }) || minimatch(m.id, globPattern, { nocase: true });\n\t\t\t});\n\n\t\t\tif (matchingModels.length === 0) {\n\t\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const model of matchingModels) {\n\t\t\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);\n\n\t\tif (warning) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: ${warning}`));\n\t\t}\n\n\t\tif (!model) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Avoid duplicates\n\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t}\n\t}\n\n\treturn scopedModels;\n}\n\nexport interface ResolveCliModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n\t/**\n\t * Error message suitable for CLI display.\n\t * When set, model will be undefined.\n\t */\n\terror: string | undefined;\n\t/**\n\t * True when the model was constructed as a synthetic fallback (unknown model ID\n\t * with a known provider). Useful for subagent spawning where synthetic models\n\t * should be rejected in favor of trying the next fallback.\n\t */\n\tisSyntheticFallback?: boolean;\n}\n\n/**\n * Resolve a single model from CLI flags.\n *\n * Supports:\n * - --provider <provider> --model <pattern>\n * - --model <provider>/<pattern>\n * - Fuzzy matching (same rules as model scoping: exact id, then partial id/name)\n *\n * Note: This does not apply the thinking level by itself, but it may *parse* and\n * return a thinking level from \"<pattern>:<thinking>\" so the caller can apply it.\n */\nexport function resolveCliModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tmodelRegistry: ModelRegistry;\n}): ResolveCliModelResult {\n\tconst { cliProvider, cliModel, modelRegistry } = options;\n\n\tif (!cliModel) {\n\t\treturn { model: undefined, warning: undefined, error: undefined };\n\t}\n\n\t// Important: use *all* models here, not just models with pre-configured auth.\n\t// This allows \"--api-key\" to be used for first-time setup.\n\tconst availableModels = modelRegistry.getAll();\n\tif (availableModels.length === 0) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: \"No models available. Check your installation or add models to models.json.\",\n\t\t};\n\t}\n\n\t// Build canonical provider lookup (case-insensitive)\n\tconst providerMap = new Map<string, string>();\n\tfor (const m of availableModels) {\n\t\tproviderMap.set(m.provider.toLowerCase(), m.provider);\n\t}\n\n\tlet provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;\n\tif (cliProvider && !provider) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: `Unknown provider \"${cliProvider}\". Use --list-models to see available providers/models.`,\n\t\t};\n\t}\n\n\t// If no explicit --provider, try to interpret \"provider/model\" format first.\n\t// When the prefix before the first slash matches a known provider, prefer that\n\t// interpretation over matching models whose IDs literally contain slashes\n\t// (e.g. \"zai/glm-5\" should resolve to provider=zai, model=glm-5, not to a\n\t// vercel-ai-gateway model with id \"zai/glm-5\").\n\tlet pattern = cliModel;\n\tlet inferredProvider = false;\n\n\tif (!provider) {\n\t\tconst slashIndex = cliModel.indexOf(\"/\");\n\t\tif (slashIndex !== -1) {\n\t\t\tconst maybeProvider = cliModel.substring(0, slashIndex);\n\t\t\tconst canonical = providerMap.get(maybeProvider.toLowerCase());\n\t\t\tif (canonical) {\n\t\t\t\tprovider = canonical;\n\t\t\t\tpattern = cliModel.substring(slashIndex + 1);\n\t\t\t\tinferredProvider = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t// If no provider was inferred from the slash, try exact matches without provider inference.\n\t// This handles models whose IDs naturally contain slashes (e.g. OpenRouter-style IDs).\n\tif (!provider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t}\n\n\tif (cliProvider && provider) {\n\t\t// If both were provided, tolerate --model <provider>/<pattern> by stripping the provider prefix\n\t\tconst prefix = `${provider}/`;\n\t\tif (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {\n\t\t\tpattern = cliModel.substring(prefix.length);\n\t\t}\n\t}\n\n\tconst candidates = provider ? availableModels.filter((m) => m.provider === provider) : availableModels;\n\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, {\n\t\tallowInvalidThinkingLevelFallback: false,\n\t});\n\n\tif (model) {\n\t\treturn { model, thinkingLevel, warning, error: undefined };\n\t}\n\n\t// If we inferred a provider from the slash but found no match within that provider,\n\t// fall back to matching the full input as a raw model id across all models.\n\t// This handles OpenRouter-style IDs like \"openai/gpt-4o:extended\" where \"openai\"\n\t// looks like a provider but the full string is actually a model id on openrouter.\n\tif (inferredProvider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t\t// Also try parseModelPattern on the full input against all models\n\t\tconst fallback = parseModelPattern(cliModel, availableModels, {\n\t\t\tallowInvalidThinkingLevelFallback: false,\n\t\t});\n\t\tif (fallback.model) {\n\t\t\treturn {\n\t\t\t\tmodel: fallback.model,\n\t\t\t\tthinkingLevel: fallback.thinkingLevel,\n\t\t\t\twarning: fallback.warning,\n\t\t\t\terror: undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (provider) {\n\t\tconst fallbackModel = buildFallbackModel(provider, pattern, availableModels);\n\t\tif (fallbackModel) {\n\t\t\tconst fallbackWarning = warning\n\t\t\t\t? `${warning} Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`\n\t\t\t\t: `Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`;\n\t\t\treturn {\n\t\t\t\tmodel: fallbackModel,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: fallbackWarning,\n\t\t\t\terror: undefined,\n\t\t\t\tisSyntheticFallback: true,\n\t\t\t};\n\t\t}\n\t}\n\n\tconst display = provider ? `${provider}/${pattern}` : cliModel;\n\treturn {\n\t\tmodel: undefined,\n\t\tthinkingLevel: undefined,\n\t\twarning,\n\t\terror: `Model \"${display}\" not found. Use --list-models to see available models.`,\n\t};\n}\n\nexport interface InitialModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel: ThinkingLevel;\n\tfallbackMessage: string | undefined;\n}\n\n/**\n * Find the initial model to use based on priority:\n * 1. CLI args (provider + model)\n * 2. First model from scoped models (if not continuing/resuming)\n * 3. Restored from session (if continuing/resuming)\n * 4. Saved default from settings\n * 5. First available model with valid API key\n */\nexport async function findInitialModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tscopedModels: ScopedModel[];\n\tisContinuing: boolean;\n\tdefaultProvider?: string;\n\tdefaultModelId?: string;\n\tdefaultThinkingLevel?: ThinkingLevel;\n\tmodelRegistry: ModelRegistry;\n}): Promise<InitialModelResult> {\n\tconst {\n\t\tcliProvider,\n\t\tcliModel,\n\t\tscopedModels,\n\t\tisContinuing,\n\t\tdefaultProvider,\n\t\tdefaultModelId,\n\t\tdefaultThinkingLevel,\n\t\tmodelRegistry,\n\t} = options;\n\n\tlet model: Model<Api> | undefined;\n\tlet thinkingLevel: ThinkingLevel = DEFAULT_THINKING_LEVEL;\n\n\t// 1. CLI args take priority\n\tif (cliProvider && cliModel) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider,\n\t\t\tcliModel,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.error) {\n\t\t\tconsole.error(chalk.red(resolved.error));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tif (resolved.model) {\n\t\t\treturn { model: resolved.model, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 2. Use first model from scoped models (skip if continuing/resuming)\n\tif (scopedModels.length > 0 && !isContinuing) {\n\t\treturn {\n\t\t\tmodel: scopedModels[0].model,\n\t\t\tthinkingLevel: scopedModels[0].thinkingLevel ?? defaultThinkingLevel ?? DEFAULT_THINKING_LEVEL,\n\t\t\tfallbackMessage: undefined,\n\t\t};\n\t}\n\n\t// 3. Try saved default from settings\n\tif (defaultProvider && defaultModelId) {\n\t\tconst found = modelRegistry.find(defaultProvider, defaultModelId);\n\t\tif (found) {\n\t\t\tmodel = found;\n\t\t\tif (defaultThinkingLevel) {\n\t\t\t\tthinkingLevel = defaultThinkingLevel;\n\t\t\t}\n\t\t\treturn { model, thinkingLevel, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 4. Try first available model with valid API key\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\treturn { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\treturn { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t}\n\n\t// 5. No model found\n\treturn { model: undefined, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n}\n\n/**\n * Restore model from session, with fallback to available models\n */\nexport async function restoreModelFromSession(\n\tsavedProvider: string,\n\tsavedModelId: string,\n\tcurrentModel: Model<Api> | undefined,\n\tshouldPrintMessages: boolean,\n\tmodelRegistry: ModelRegistry,\n): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {\n\tconst restoredModel = modelRegistry.find(savedProvider, savedModelId);\n\n\t// Check if restored model exists and has a valid API key\n\tconst hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;\n\n\tif (restoredModel && hasApiKey) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));\n\t\t}\n\t\treturn { model: restoredModel, fallbackMessage: undefined };\n\t}\n\n\t// Model not found or no API key - fall back\n\tconst reason = !restoredModel ? \"model no longer exists\" : \"no API key available\";\n\n\tif (shouldPrintMessages) {\n\t\tconsole.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));\n\t}\n\n\t// If we already have a model, use it as fallback\n\tif (currentModel) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`));\n\t\t}\n\t\treturn {\n\t\t\tmodel: currentModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`,\n\t\t};\n\t}\n\n\t// Try to find any available model\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tlet fallbackModel: Model<Api> | undefined;\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\tfallbackModel = match;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\tif (!fallbackModel) {\n\t\t\tfallbackModel = availableModels[0];\n\t\t}\n\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));\n\t\t}\n\n\t\treturn {\n\t\t\tmodel: fallbackModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`,\n\t\t};\n\t}\n\n\t// No models available\n\treturn { model: undefined, fallbackMessage: undefined };\n}\n"]}
1
+ {"version":3,"file":"model-resolver.js","sourceRoot":"","sources":["../../src/core/model-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model resolution, scoping, and initial selection\n */\n\nimport type { ThinkingLevel } from \"@dreb/agent-core\";\nimport { type Api, findModelInList, type KnownProvider, type Model, modelsAreEqual } from \"@dreb/ai\";\nimport chalk from \"chalk\";\nimport { minimatch } from \"minimatch\";\nimport { isValidThinkingLevel } from \"../cli/args.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ModelRegistry } from \"./model-registry.js\";\n\n/** Default model IDs for each known provider */\nexport const defaultModelPerProvider: Record<KnownProvider, string> = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\tanthropic: \"claude-opus-4-6\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.2\",\n\t\"openai-codex\": \"gpt-5.4\",\n\tgoogle: \"gemini-2.5-pro\",\n\t\"google-gemini-cli\": \"gemini-2.5-pro\",\n\t\"google-antigravity\": \"gemini-3.1-pro-high\",\n\t\"google-vertex\": \"gemini-3-pro-preview\",\n\t\"github-copilot\": \"gpt-4o\",\n\topenrouter: \"openai/gpt-5.1-codex\",\n\t\"vercel-ai-gateway\": \"anthropic/claude-opus-4-6\",\n\txai: \"grok-4-fast-non-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"gpt-oss-120b\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\thuggingface: \"moonshotai/Kimi-K2.5\",\n\topencode: \"claude-opus-4-6\",\n\t\"opencode-go\": \"kimi-k2.5\",\n\t\"kimi-coding\": \"kimi-k2-thinking\",\n\t\"kimi-coding-oauth\": \"kimi-for-coding\",\n};\n\nexport interface ScopedModel {\n\tmodel: Model<Api>;\n\t/** Thinking level if explicitly specified in pattern (e.g., \"model:high\"), undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n}\n\n// isAlias logic moved to @dreb/ai as isModelAlias\n\n/**\n * Find an exact model reference match.\n * Supports either a bare model id or a canonical provider/modelId reference.\n * When matching by bare id, ambiguous matches across providers are rejected.\n */\nexport function findExactModelReferenceMatch(\n\tmodelReference: string,\n\tavailableModels: Model<Api>[],\n): Model<Api> | undefined {\n\tconst trimmedReference = modelReference.trim();\n\tif (!trimmedReference) {\n\t\treturn undefined;\n\t}\n\n\tconst normalizedReference = trimmedReference.toLowerCase();\n\n\tconst canonicalMatches = availableModels.filter(\n\t\t(model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,\n\t);\n\tif (canonicalMatches.length === 1) {\n\t\treturn canonicalMatches[0];\n\t}\n\tif (canonicalMatches.length > 1) {\n\t\treturn undefined;\n\t}\n\n\tconst slashIndex = trimmedReference.indexOf(\"/\");\n\tif (slashIndex !== -1) {\n\t\tconst provider = trimmedReference.substring(0, slashIndex).trim();\n\t\tconst modelId = trimmedReference.substring(slashIndex + 1).trim();\n\t\tif (provider && modelId) {\n\t\t\tconst providerMatches = availableModels.filter(\n\t\t\t\t(model) =>\n\t\t\t\t\tmodel.provider.toLowerCase() === provider.toLowerCase() &&\n\t\t\t\t\tmodel.id.toLowerCase() === modelId.toLowerCase(),\n\t\t\t);\n\t\t\tif (providerMatches.length === 1) {\n\t\t\t\treturn providerMatches[0];\n\t\t\t}\n\t\t\tif (providerMatches.length > 1) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);\n\treturn idMatches.length === 1 ? idMatches[0] : undefined;\n}\n\n/**\n * Try to match a pattern to a model from the available models list.\n *\n * Extends findModelInList() with support for canonical `provider/model`\n * references via findExactModelReferenceMatch.\n */\nfunction tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\t// Try canonical provider/model and cross-provider exact matches first\n\tconst exactMatch = findExactModelReferenceMatch(modelPattern, availableModels);\n\tif (exactMatch) return exactMatch;\n\n\t// Delegate fuzzy matching to @dreb/ai\n\treturn findModelInList(modelPattern, availableModels);\n}\n\nexport interface ParsedModelResult {\n\tmodel: Model<Api> | undefined;\n\t/** Thinking level if explicitly specified in pattern, undefined otherwise */\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n}\n\nfunction buildFallbackModel(provider: string, modelId: string, availableModels: Model<Api>[]): Model<Api> | undefined {\n\tconst providerModels = availableModels.filter((m) => m.provider === provider);\n\tif (providerModels.length === 0) return undefined;\n\n\tconst defaultId = defaultModelPerProvider[provider as KnownProvider];\n\tconst baseModel = defaultId\n\t\t? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0])\n\t\t: providerModels[0];\n\n\treturn {\n\t\t...baseModel,\n\t\tid: modelId,\n\t\tname: modelId,\n\t};\n}\n\n/**\n * Parse a pattern to extract model and thinking level.\n * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix).\n *\n * Algorithm:\n * 1. Try to match full pattern as a model\n * 2. If found, return it with \"off\" thinking level\n * 3. If not found and has colons, split on last colon:\n * - If suffix is valid thinking level, use it and recurse on prefix\n * - If suffix is invalid, warn and recurse on prefix with \"off\"\n *\n * @internal Exported for testing\n */\nexport function parseModelPattern(\n\tpattern: string,\n\tavailableModels: Model<Api>[],\n\toptions?: { allowInvalidThinkingLevelFallback?: boolean },\n): ParsedModelResult {\n\t// Try exact match first\n\tconst exactMatch = tryMatchModel(pattern, availableModels);\n\tif (exactMatch) {\n\t\treturn { model: exactMatch, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\t// No match - try splitting on last colon if present\n\tconst lastColonIndex = pattern.lastIndexOf(\":\");\n\tif (lastColonIndex === -1) {\n\t\t// No colons, pattern simply doesn't match any model\n\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t}\n\n\tconst prefix = pattern.substring(0, lastColonIndex);\n\tconst suffix = pattern.substring(lastColonIndex + 1);\n\n\tif (isValidThinkingLevel(suffix)) {\n\t\t// Valid thinking level - recurse on prefix and use this level\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\t// Only use this thinking level if no warning from inner recursion\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: result.warning ? undefined : suffix,\n\t\t\t\twarning: result.warning,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t} else {\n\t\t// Invalid suffix\n\t\tconst allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;\n\t\tif (!allowFallback) {\n\t\t\t// In strict mode (CLI --model parsing), treat it as part of the model id and fail.\n\t\t\t// This avoids accidentally resolving to a different model.\n\t\t\treturn { model: undefined, thinkingLevel: undefined, warning: undefined };\n\t\t}\n\n\t\t// Scope mode: recurse on prefix and warn\n\t\tconst result = parseModelPattern(prefix, availableModels, options);\n\t\tif (result.model) {\n\t\t\treturn {\n\t\t\t\tmodel: result.model,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: `Invalid thinking level \"${suffix}\" in pattern \"${pattern}\". Using default instead.`,\n\t\t\t};\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Resolve model patterns to actual Model objects with optional thinking levels\n * Format: \"pattern:level\" where :level is optional\n * For each pattern, finds all matching models and picks the best version:\n * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929)\n * 2. If no alias, pick the latest dated version\n *\n * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto).\n * The algorithm tries to match the full pattern first, then progressively\n * strips colon-suffixes to find a match.\n */\nexport async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {\n\tconst availableModels = await modelRegistry.getAvailable();\n\tconst scopedModels: ScopedModel[] = [];\n\n\tfor (const pattern of patterns) {\n\t\t// Check if pattern contains glob characters\n\t\tif (pattern.includes(\"*\") || pattern.includes(\"?\") || pattern.includes(\"[\")) {\n\t\t\t// Extract optional thinking level suffix (e.g., \"provider/*:high\")\n\t\t\tconst colonIdx = pattern.lastIndexOf(\":\");\n\t\t\tlet globPattern = pattern;\n\t\t\tlet thinkingLevel: ThinkingLevel | undefined;\n\n\t\t\tif (colonIdx !== -1) {\n\t\t\t\tconst suffix = pattern.substring(colonIdx + 1);\n\t\t\t\tif (isValidThinkingLevel(suffix)) {\n\t\t\t\t\tthinkingLevel = suffix;\n\t\t\t\t\tglobPattern = pattern.substring(0, colonIdx);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Match against \"provider/modelId\" format OR just model ID\n\t\t\t// This allows \"*sonnet*\" to match without requiring \"anthropic/*sonnet*\"\n\t\t\tconst matchingModels = availableModels.filter((m) => {\n\t\t\t\tconst fullId = `${m.provider}/${m.id}`;\n\t\t\t\treturn minimatch(fullId, globPattern, { nocase: true }) || minimatch(m.id, globPattern, { nocase: true });\n\t\t\t});\n\n\t\t\tif (matchingModels.length === 0) {\n\t\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const model of matchingModels) {\n\t\t\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);\n\n\t\tif (warning) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: ${warning}`));\n\t\t}\n\n\t\tif (!model) {\n\t\t\tconsole.warn(chalk.yellow(`Warning: No models match pattern \"${pattern}\"`));\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Avoid duplicates\n\t\tif (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {\n\t\t\tscopedModels.push({ model, thinkingLevel });\n\t\t}\n\t}\n\n\treturn scopedModels;\n}\n\nexport interface ResolveCliModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel?: ThinkingLevel;\n\twarning: string | undefined;\n\t/**\n\t * Error message suitable for CLI display.\n\t * When set, model will be undefined.\n\t */\n\terror: string | undefined;\n\t/**\n\t * True when the model was constructed as a synthetic fallback (unknown model ID\n\t * with a known provider). Useful for subagent spawning where synthetic models\n\t * should be rejected in favor of trying the next fallback.\n\t */\n\tisSyntheticFallback?: boolean;\n}\n\n/**\n * Resolve a single model from CLI flags.\n *\n * Supports:\n * - --provider <provider> --model <pattern>\n * - --model <provider>/<pattern>\n * - Fuzzy matching (same rules as model scoping: exact id, then partial id/name)\n *\n * Note: This does not apply the thinking level by itself, but it may *parse* and\n * return a thinking level from \"<pattern>:<thinking>\" so the caller can apply it.\n */\nexport function resolveCliModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tmodelRegistry: ModelRegistry;\n}): ResolveCliModelResult {\n\tconst { cliProvider, cliModel, modelRegistry } = options;\n\n\tif (!cliModel) {\n\t\treturn { model: undefined, warning: undefined, error: undefined };\n\t}\n\n\t// Important: use *all* models here, not just models with pre-configured auth.\n\t// This allows \"--api-key\" to be used for first-time setup.\n\tconst availableModels = modelRegistry.getAll();\n\tif (availableModels.length === 0) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: \"No models available. Check your installation or add models to models.json.\",\n\t\t};\n\t}\n\n\t// Build canonical provider lookup (case-insensitive)\n\tconst providerMap = new Map<string, string>();\n\tfor (const m of availableModels) {\n\t\tproviderMap.set(m.provider.toLowerCase(), m.provider);\n\t}\n\n\tlet provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;\n\tif (cliProvider && !provider) {\n\t\treturn {\n\t\t\tmodel: undefined,\n\t\t\twarning: undefined,\n\t\t\terror: `Unknown provider \"${cliProvider}\". Use --list-models to see available providers/models.`,\n\t\t};\n\t}\n\n\t// If no explicit --provider, try to interpret \"provider/model\" format first.\n\t// When the prefix before the first slash matches a known provider, prefer that\n\t// interpretation over matching models whose IDs literally contain slashes\n\t// (e.g. \"zai/glm-5\" should resolve to provider=zai, model=glm-5, not to a\n\t// vercel-ai-gateway model with id \"zai/glm-5\").\n\tlet pattern = cliModel;\n\tlet inferredProvider = false;\n\n\tif (!provider) {\n\t\tconst slashIndex = cliModel.indexOf(\"/\");\n\t\tif (slashIndex !== -1) {\n\t\t\tconst maybeProvider = cliModel.substring(0, slashIndex);\n\t\t\tconst canonical = providerMap.get(maybeProvider.toLowerCase());\n\t\t\tif (canonical) {\n\t\t\t\tprovider = canonical;\n\t\t\t\tpattern = cliModel.substring(slashIndex + 1);\n\t\t\t\tinferredProvider = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t// If no provider was inferred from the slash, try exact matches without provider inference.\n\t// This handles models whose IDs naturally contain slashes (e.g. OpenRouter-style IDs).\n\tif (!provider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t}\n\n\tif (cliProvider && provider) {\n\t\t// If both were provided, tolerate --model <provider>/<pattern> by stripping the provider prefix\n\t\tconst prefix = `${provider}/`;\n\t\tif (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {\n\t\t\tpattern = cliModel.substring(prefix.length);\n\t\t}\n\t}\n\n\tconst candidates = provider ? availableModels.filter((m) => m.provider === provider) : availableModels;\n\tconst { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, {\n\t\tallowInvalidThinkingLevelFallback: false,\n\t});\n\n\tif (model) {\n\t\treturn { model, thinkingLevel, warning, error: undefined };\n\t}\n\n\t// If we inferred a provider from the slash but found no match within that provider,\n\t// fall back to matching the full input as a raw model id across all models.\n\t// This handles OpenRouter-style IDs like \"openai/gpt-4o:extended\" where \"openai\"\n\t// looks like a provider but the full string is actually a model id on openrouter.\n\tif (inferredProvider) {\n\t\tconst lower = cliModel.toLowerCase();\n\t\tconst exact = availableModels.find(\n\t\t\t(m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower,\n\t\t);\n\t\tif (exact) {\n\t\t\treturn { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };\n\t\t}\n\t\t// Also try parseModelPattern on the full input against all models\n\t\tconst fallback = parseModelPattern(cliModel, availableModels, {\n\t\t\tallowInvalidThinkingLevelFallback: false,\n\t\t});\n\t\tif (fallback.model) {\n\t\t\treturn {\n\t\t\t\tmodel: fallback.model,\n\t\t\t\tthinkingLevel: fallback.thinkingLevel,\n\t\t\t\twarning: fallback.warning,\n\t\t\t\terror: undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (provider) {\n\t\tconst fallbackModel = buildFallbackModel(provider, pattern, availableModels);\n\t\tif (fallbackModel) {\n\t\t\tconst fallbackWarning = warning\n\t\t\t\t? `${warning} Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`\n\t\t\t\t: `Model \"${pattern}\" not found for provider \"${provider}\". Using custom model id.`;\n\t\t\treturn {\n\t\t\t\tmodel: fallbackModel,\n\t\t\t\tthinkingLevel: undefined,\n\t\t\t\twarning: fallbackWarning,\n\t\t\t\terror: undefined,\n\t\t\t\tisSyntheticFallback: true,\n\t\t\t};\n\t\t}\n\t}\n\n\tconst display = provider ? `${provider}/${pattern}` : cliModel;\n\treturn {\n\t\tmodel: undefined,\n\t\tthinkingLevel: undefined,\n\t\twarning,\n\t\terror: `Model \"${display}\" not found. Use --list-models to see available models.`,\n\t};\n}\n\nexport interface InitialModelResult {\n\tmodel: Model<Api> | undefined;\n\tthinkingLevel: ThinkingLevel;\n\tfallbackMessage: string | undefined;\n}\n\n/**\n * Find the initial model to use based on priority:\n * 1. CLI args (provider + model)\n * 2. First model from scoped models (if not continuing/resuming)\n * 3. Restored from session (if continuing/resuming)\n * 4. Saved default from settings\n * 5. First available model with valid API key\n */\nexport async function findInitialModel(options: {\n\tcliProvider?: string;\n\tcliModel?: string;\n\tscopedModels: ScopedModel[];\n\tisContinuing: boolean;\n\tdefaultProvider?: string;\n\tdefaultModelId?: string;\n\tdefaultThinkingLevel?: ThinkingLevel;\n\tmodelRegistry: ModelRegistry;\n}): Promise<InitialModelResult> {\n\tconst {\n\t\tcliProvider,\n\t\tcliModel,\n\t\tscopedModels,\n\t\tisContinuing,\n\t\tdefaultProvider,\n\t\tdefaultModelId,\n\t\tdefaultThinkingLevel,\n\t\tmodelRegistry,\n\t} = options;\n\n\tlet model: Model<Api> | undefined;\n\tlet thinkingLevel: ThinkingLevel = DEFAULT_THINKING_LEVEL;\n\n\t// 1. CLI args take priority\n\tif (cliProvider && cliModel) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider,\n\t\t\tcliModel,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.error) {\n\t\t\tconsole.error(chalk.red(resolved.error));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tif (resolved.model) {\n\t\t\treturn { model: resolved.model, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 2. Use first model from scoped models (skip if continuing/resuming)\n\tif (scopedModels.length > 0 && !isContinuing) {\n\t\treturn {\n\t\t\tmodel: scopedModels[0].model,\n\t\t\tthinkingLevel: scopedModels[0].thinkingLevel ?? defaultThinkingLevel ?? DEFAULT_THINKING_LEVEL,\n\t\t\tfallbackMessage: undefined,\n\t\t};\n\t}\n\n\t// 3. Try saved default from settings\n\tif (defaultProvider && defaultModelId) {\n\t\tconst found = modelRegistry.find(defaultProvider, defaultModelId);\n\t\tif (found) {\n\t\t\tmodel = found;\n\t\t\tif (defaultThinkingLevel) {\n\t\t\t\tthinkingLevel = defaultThinkingLevel;\n\t\t\t}\n\t\t\treturn { model, thinkingLevel, fallbackMessage: undefined };\n\t\t}\n\t}\n\n\t// 4. Try first available model with valid API key\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\treturn { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\treturn { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n\t}\n\n\t// 5. No model found\n\treturn { model: undefined, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };\n}\n\n/**\n * Restore model from session, with fallback to available models\n */\nexport async function restoreModelFromSession(\n\tsavedProvider: string,\n\tsavedModelId: string,\n\tcurrentModel: Model<Api> | undefined,\n\tshouldPrintMessages: boolean,\n\tmodelRegistry: ModelRegistry,\n): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {\n\tconst restoredModel = modelRegistry.find(savedProvider, savedModelId);\n\n\t// Check if restored model exists and has a valid API key\n\tconst hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;\n\n\tif (restoredModel && hasApiKey) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`));\n\t\t}\n\t\treturn { model: restoredModel, fallbackMessage: undefined };\n\t}\n\n\t// Model not found or no API key - fall back\n\tconst reason = !restoredModel ? \"model no longer exists\" : \"no API key available\";\n\n\tif (shouldPrintMessages) {\n\t\tconsole.error(chalk.yellow(`Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`));\n\t}\n\n\t// If we already have a model, use it as fallback\n\tif (currentModel) {\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`));\n\t\t}\n\t\treturn {\n\t\t\tmodel: currentModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`,\n\t\t};\n\t}\n\n\t// Try to find any available model\n\tconst availableModels = await modelRegistry.getAvailable();\n\n\tif (availableModels.length > 0) {\n\t\t// Try to find a default model from known providers\n\t\tlet fallbackModel: Model<Api> | undefined;\n\t\tfor (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {\n\t\t\tconst defaultId = defaultModelPerProvider[provider];\n\t\t\tconst match = availableModels.find((m) => m.provider === provider && m.id === defaultId);\n\t\t\tif (match) {\n\t\t\t\tfallbackModel = match;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// If no default found, use first available\n\t\tif (!fallbackModel) {\n\t\t\tfallbackModel = availableModels[0];\n\t\t}\n\n\t\tif (shouldPrintMessages) {\n\t\t\tconsole.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`));\n\t\t}\n\n\t\treturn {\n\t\t\tmodel: fallbackModel,\n\t\t\tfallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`,\n\t\t};\n\t}\n\n\t// No models available\n\treturn { model: undefined, fallbackMessage: undefined };\n}\n"]}
@@ -0,0 +1,11 @@
1
+ export interface SecretPattern {
2
+ name: string;
3
+ pattern: RegExp;
4
+ }
5
+ export interface ScrubResult {
6
+ scrubbed: string;
7
+ redactionCount: number;
8
+ }
9
+ export declare const DEFAULT_SECRET_PATTERNS: SecretPattern[];
10
+ export declare function scrubSecrets(text: string, extraPatterns?: SecretPattern[]): ScrubResult;
11
+ //# sourceMappingURL=secret-scrubber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-scrubber.d.ts","sourceRoot":"","sources":["../../src/core/secret-scrubber.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACvB;AAkCD,eAAO,MAAM,uBAAuB,EAAE,aAAa,EAAoD,CAAC;AAExG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,aAAa,EAAE,GAAG,WAAW,CAuCvF","sourcesContent":["export interface SecretPattern {\n\tname: string;\n\tpattern: RegExp;\n}\n\nexport interface ScrubResult {\n\tscrubbed: string;\n\tredactionCount: number;\n}\n\n// Multi-line patterns (processed first)\nconst pemPrivateKey =\n\t/-----BEGIN (?:RSA |EC |DSA |ENCRYPTED )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |DSA |ENCRYPTED )?PRIVATE KEY-----/g;\nconst opensshPrivateKey = /-----BEGIN OPENSSH PRIVATE KEY-----[\\s\\S]*?-----END OPENSSH PRIVATE KEY-----/g;\n\n// Single-line patterns\nconst awsAccessKey = /\\bAKIA[0-9A-Z]{16}\\b/g;\nconst githubToken =\n\t/ghp_[A-Za-z0-9_]{36,}|gho_[A-Za-z0-9_]{36,}|ghu_[A-Za-z0-9_]{36,}|ghs_[A-Za-z0-9_]{36,}|ghr_[A-Za-z0-9_]{36,}|github_pat_[A-Za-z0-9_]{22,}/g;\nconst gitlabToken = /glpat-[0-9a-zA-Z_-]{20,}/g;\nconst anthropicKey = /sk-ant-[a-zA-Z0-9_-]{90,}/g;\nconst openaiKey = /sk-(?!ant-)[a-zA-Z0-9_-]{20,}/g;\nconst slackToken = /xox[baprs]-[0-9a-zA-Z-]{10,}/g;\nconst stripeKey = /[sr]k_(?:test|live)_[0-9a-zA-Z]{24,}/g;\nconst urlCredentials = /(https?:\\/\\/)([^\\s:]+):([^\\s@]+)@/g;\n\nconst MULTILINE_PATTERNS: SecretPattern[] = [\n\t{ name: \"pem_private_key\", pattern: pemPrivateKey },\n\t{ name: \"openssh_private_key\", pattern: opensshPrivateKey },\n];\n\nconst SINGLELINE_PATTERNS: SecretPattern[] = [\n\t{ name: \"aws_access_key\", pattern: awsAccessKey },\n\t{ name: \"github_token\", pattern: githubToken },\n\t{ name: \"gitlab_token\", pattern: gitlabToken },\n\t{ name: \"anthropic_key\", pattern: anthropicKey },\n\t{ name: \"openai_key\", pattern: openaiKey },\n\t{ name: \"slack_token\", pattern: slackToken },\n\t{ name: \"stripe_key\", pattern: stripeKey },\n\t{ name: \"url_credentials\", pattern: urlCredentials },\n];\n\nexport const DEFAULT_SECRET_PATTERNS: SecretPattern[] = [...MULTILINE_PATTERNS, ...SINGLELINE_PATTERNS];\n\nexport function scrubSecrets(text: string, extraPatterns?: SecretPattern[]): ScrubResult {\n\tlet scrubbed = text;\n\tlet redactionCount = 0;\n\n\tfunction applyPattern(sp: SecretPattern): void {\n\t\t// Reset lastIndex since we reuse compiled regexes\n\t\tsp.pattern.lastIndex = 0;\n\n\t\tif (sp.name === \"url_credentials\") {\n\t\t\tscrubbed = scrubbed.replace(sp.pattern, (_match, protocol, user, _password) => {\n\t\t\t\tredactionCount++;\n\t\t\t\treturn `${protocol}${user}:<REDACTED:url_credentials>@`;\n\t\t\t});\n\t\t} else {\n\t\t\tscrubbed = scrubbed.replace(sp.pattern, () => {\n\t\t\t\tredactionCount++;\n\t\t\t\treturn `<REDACTED:${sp.name}>`;\n\t\t\t});\n\t\t}\n\t}\n\n\t// Multi-line patterns first\n\tfor (const sp of MULTILINE_PATTERNS) {\n\t\tapplyPattern(sp);\n\t}\n\n\t// Single-line patterns\n\tfor (const sp of SINGLELINE_PATTERNS) {\n\t\tapplyPattern(sp);\n\t}\n\n\t// Extra patterns last\n\tif (extraPatterns) {\n\t\tfor (const sp of extraPatterns) {\n\t\t\tapplyPattern(sp);\n\t\t}\n\t}\n\n\treturn { scrubbed, redactionCount };\n}\n"]}