triage-trip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # `ai-blackbox`
2
+
3
+ > Autonomous AI Regression Triage & Provenance CLI. Pinpoint breaking contract changes introduced by AI coding tools in seconds.
4
+
5
+ <p align="center">
6
+ <img src="./assets/demo.gif" alt="AI-Blackbox Demo" width="700">
7
+ </p>
8
+ ---
9
+
10
+ ## The Problem
11
+
12
+ AI coding assistants (Cursor, GitHub Copilot, Windsurf) dramatically accelerate drafting and refactoring code. However, they frequently cause **silent contract drift**:
13
+
14
+ * An AI renames an object key or subtle schema attribute (e.g., `userId` ➔ `user_id`).
15
+ * Standard linters and compilers pass because the syntax is completely valid.
16
+ * Downstream modules crash 2–3 commits later with cryptic runtime errors (`TypeError: Cannot read properties of undefined`).
17
+ * Developers spend 20–30 minutes manually inspecting multi-file diffs, running `git bisect`, and searching for what changed.
18
+
19
+ ---
20
+
21
+ ## What `ai-blackbox` Does
22
+
23
+ `ai-blackbox` connects the dots across your development environment to trace the exact lineage of a regression:
24
+
25
+ ```text
26
+ [ User Prompt in IDE ] ➔ [ AI Generated Diff ] ➔ [ Git Commit ] ➔ [ Runtime Error ]
27
+ │
28
+ ai-blackbox triage
29
+ │
30
+ ▼
31
+ • Exact Root Cause & Commit
32
+ • Attributed AI Prompt
33
+ • Semantic Contract Shift
34
+ • Clean Unified Diff Patch
35
+
36
+ ```
37
+
38
+ 1. **Inspects Git History:** Automatically reads uncommitted working changes and recent commits using `simple-git`.
39
+ 2. **Extracts AI Session Provenance:** Scans local IDE storage (e.g., Cursor's local SQLite database `state.vscdb`) to retrieve recent prompts and user instructions.
40
+ 3. **Triages Semantic Drift:** Correlates the runtime stack trace with recent AST modifications and prompts via the OpenAI API to deliver an instant diagnostic report.
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ ### Prerequisites
47
+
48
+ * **Node.js** (v18 or higher, ESM native)
49
+ * An active **OpenAI API Key**
50
+
51
+ ### Local Setup
52
+
53
+ ```bash
54
+ # Clone the repository
55
+ git clone https://github.com/your-username/ai-blackbox.git
56
+ cd ai-blackbox
57
+
58
+ # Install dependencies
59
+ npm install
60
+
61
+ # Link executable globally (optional for direct CLI access)
62
+ npm link
63
+
64
+ ```
65
+
66
+ Set your OpenAI API key in your terminal environment or add it to a `.env` file:
67
+
68
+ ```bash
69
+ export OPENAI_API_KEY="sk-..."
70
+
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Quickstart & Usage
76
+
77
+ Run the triage engine directly against any runtime error message or stack trace:
78
+
79
+ ```bash
80
+ # Via npm script
81
+ node bin/cli.js triage "TypeError: Cannot read properties of undefined (reading 'toUpperCase')"
82
+
83
+ # Or globally (if linked)
84
+ ai-blackbox triage "TypeError: Cannot read properties of undefined (reading 'toUpperCase')"
85
+
86
+ ```
87
+
88
+ ### Options
89
+
90
+ * `<query_or_error>` *(required)*: The error message or failing test description.
91
+ * `-c, --commits <number>`: Number of recent commits to analyze (default: `5`).
92
+
93
+ ---
94
+
95
+ ## Example Output
96
+
97
+ ```markdown
98
+ ■ AI-Blackbox Triage Engine
99
+
100
+ ✔ Root cause isolated.
101
+
102
+ 1. **Root Cause**: The error occurs in `app.js` at line 6, where `profile.userId` is accessed.
103
+ The breaking change was introduced in commit `b83c577` ("refactor: update user schema"),
104
+ which modified the `getUserProfile` function in `user.js`.
105
+
106
+ 2. **Attributed Prompt**: "Rename userId to user_id across the returned profile object."
107
+
108
+ 3. **The Shift**: The `getUserProfile` function now returns `user_id` instead of `userId`,
109
+ causing `profile.userId` to evaluate to `undefined`.
110
+
111
+ 4. **Patch**:
112
+ --- a/app.js
113
+ +++ b/app.js
114
+ @@ -6,2 +6,2 @@
115
+ - console.log(`Loading dashboard for user ID: ${profile.userId.toUpperCase()}`);
116
+ + console.log(`Loading dashboard for user ID: ${profile.user_id.toUpperCase()}`);
117
+
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Project Structure
123
+
124
+ ```text
125
+ ai-blackbox/
126
+ ├── bin/
127
+ │ └── cli.js # CLI commands & terminal output formatting (Commander, Ora, Picocolors)
128
+ ├── src/
129
+ │ ├── git.js # Working tree and commit diff extraction (simple-git)
130
+ │ ├── cursor.js # Cursor SQLite database parser (better-sqlite3)
131
+ │ └── triage.js # OpenAI triage diagnostic engine (GPT-4o)
132
+ ├── package.json
133
+ └── README.md
134
+
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Roadmap
140
+
141
+ * [x] Git diff & commit stream extraction
142
+ * [x] Local SQLite prompt extraction for Cursor Composer
143
+ * [x] OpenAI regression diagnostic engine
144
+ * [ ] Auto-patch application flag (`--fix` via `git apply`)
145
+ * [ ] Multi-IDE adapters (GitHub Copilot Chat, Windsurf Cascade, Claude Code)
146
+ * [ ] GitHub Action runner for automated PR failure triage
package/bin/cli.js ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import pc from 'picocolors';
5
+ import ora from 'ora';
6
+ import { confirm } from '@inquirer/prompts';
7
+ import 'dotenv/config';
8
+ import { getRecentChanges } from '../src/git.js';
9
+ import { triageRegression } from '../src/triage.js';
10
+ import { getRecentCursorPrompts } from '../src/cursor.js';
11
+ import { extractDiff, applyDiff } from '../src/patch.js';
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('triage-trip')
17
+ .description('Autonomous triage for regressions introduced by AI edits')
18
+ .version('0.1.0');
19
+
20
+ program
21
+ .command('triage')
22
+ .description('Diagnose why your build or code is failing')
23
+ .argument('<query_or_error>', 'The error message or description of failure')
24
+ .option('-c, --commits <number>', 'Number of recent commits to analyze', '5')
25
+ .option('-y, --yes', 'Automatically apply the patch without asking', false)
26
+ .action(async (errorInput, options) => {
27
+ console.log(pc.bold(pc.cyan('\n■ AI-Blackbox Triage Engine\n')));
28
+
29
+ const spinner = ora('Reading recent git changes & Cursor prompts...').start();
30
+ let diagnosis = '';
31
+
32
+ try {
33
+ const commitCount = parseInt(options.commits, 10);
34
+ const [changes, prompts] = await Promise.all([
35
+ getRecentChanges(commitCount),
36
+ Promise.resolve().then(() => getRecentCursorPrompts(10))
37
+ ]);
38
+
39
+ spinner.text = 'Isolating structural contract shifts and AST regressions...';
40
+ diagnosis = await triageRegression(errorInput, changes, prompts);
41
+
42
+ spinner.succeed(pc.green('Root cause isolated.'));
43
+ console.log('\n' + diagnosis + '\n');
44
+ } catch (err) {
45
+ spinner.fail(pc.red('Triage failed: ' + err.message));
46
+ process.exit(1);
47
+ }
48
+
49
+ // Attempt to extract patch from LLM output
50
+ const patch = extractDiff(diagnosis);
51
+
52
+ if (patch) {
53
+ let shouldApply = options.yes;
54
+
55
+ if (!shouldApply) {
56
+ shouldApply = await confirm({
57
+ message: 'Apply this patch to your working tree now?',
58
+ default: true,
59
+ });
60
+ }
61
+
62
+ if (shouldApply) {
63
+ const patchSpinner = ora('Applying patch via git apply...').start();
64
+ try {
65
+ await applyDiff(patch);
66
+ patchSpinner.succeed(pc.green('Patch applied successfully to working tree.'));
67
+ console.log(pc.dim('Run `git diff` to review or `git commit` to save the fix.\n'));
68
+ } catch (err) {
69
+ patchSpinner.fail(pc.red(err.message));
70
+ }
71
+ }
72
+ }
73
+ });
74
+
75
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "triage-trip",
3
+ "version": "0.1.0",
4
+ "description": "Autonomous CLI to pinpoint and patch regressions introduced by AI code assistants",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "cursor",
11
+ "ai",
12
+ "triage",
13
+ "git",
14
+ "debugging"
15
+ ],
16
+ "author": "Anuj Prakash",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/arules3/ai-blackbox.git"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "src"
25
+ ],
26
+ "type": "module",
27
+ "bin": {
28
+ "triage-trip": "bin/cli.js"
29
+ },
30
+ "dependencies": {
31
+ "@anthropic-ai/sdk": "^0.131.0",
32
+ "@babel/parser": "^7.29.9",
33
+ "@babel/traverse": "^7.29.8",
34
+ "@inquirer/prompts": "^8.7.2",
35
+ "better-sqlite3": "^13.0.3",
36
+ "commander": "^15.0.0",
37
+ "dotenv": "^18.0.5",
38
+ "openai": "^7.27.0",
39
+ "ora": "^9.4.1",
40
+ "picocolors": "^1.1.1",
41
+ "simple-git": "^4.0.2"
42
+ }
43
+ }
package/src/cursor.js ADDED
@@ -0,0 +1,176 @@
1
+ // src/cursor.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import Database from 'better-sqlite3';
6
+
7
+ function getCursorUserDir() {
8
+ const home = os.homedir();
9
+ if (process.platform === 'darwin') {
10
+ return path.join(home, 'Library', 'Application Support', 'Cursor', 'User');
11
+ }
12
+ if (process.platform === 'win32') {
13
+ return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Cursor', 'User');
14
+ }
15
+ return path.join(home, '.config', 'Cursor', 'User');
16
+ }
17
+
18
+ export function findCurrentWorkspaceDb(cwd = process.cwd()) {
19
+ const userDir = getCursorUserDir();
20
+ const workspaceStorageDir = path.join(userDir, 'workspaceStorage');
21
+ if (!fs.existsSync(workspaceStorageDir)) return null;
22
+
23
+ try {
24
+ const entries = fs.readdirSync(workspaceStorageDir);
25
+ let bestMatch = null;
26
+ const normalizedCwd = path.resolve(cwd).toLowerCase();
27
+
28
+ for (const entry of entries) {
29
+ const fullPath = path.join(workspaceStorageDir, entry);
30
+ const jsonPath = path.join(fullPath, 'workspace.json');
31
+ const dbPath = path.join(fullPath, 'state.vscdb');
32
+
33
+ if (fs.existsSync(jsonPath) && fs.existsSync(dbPath)) {
34
+ try {
35
+ const metadata = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
36
+ if (metadata.folder) {
37
+ let folderPath = decodeURIComponent(metadata.folder);
38
+ if (folderPath.startsWith('file://')) {
39
+ folderPath = folderPath.replace('file://', '');
40
+ }
41
+ const normalizedFolder = path.resolve(folderPath).toLowerCase();
42
+
43
+ // Exact folder match takes highest priority
44
+ if (normalizedFolder === normalizedCwd) {
45
+ return dbPath;
46
+ }
47
+ }
48
+ } catch { }
49
+ }
50
+ }
51
+ } catch { }
52
+ return null;
53
+ }
54
+
55
+ // Inside src/cursor.js - update getRecentCursorPrompts:
56
+
57
+ export function getRecentCursorPrompts(limit = 5) {
58
+ const userDir = getCursorUserDir();
59
+ const workspaceDbPath = findCurrentWorkspaceDb();
60
+ const globalDbPath = path.join(userDir, 'globalStorage', 'state.vscdb');
61
+
62
+ if (!fs.existsSync(globalDbPath)) return [];
63
+
64
+ let targetComposerIds = [];
65
+ if (workspaceDbPath && fs.existsSync(workspaceDbPath)) {
66
+ try {
67
+ const wsDb = new Database(workspaceDbPath, { readonly: true, fileMustExist: true });
68
+ const row = wsDb.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
69
+ if (row?.value) {
70
+ const parsed = JSON.parse(row.value);
71
+ if (Array.isArray(parsed.selectedComposerIds)) {
72
+ targetComposerIds.push(...parsed.selectedComposerIds);
73
+ }
74
+ if (Array.isArray(parsed.lastFocusedComposerIds)) {
75
+ targetComposerIds.push(...parsed.lastFocusedComposerIds);
76
+ }
77
+ }
78
+ wsDb.close();
79
+ } catch { }
80
+ }
81
+
82
+ targetComposerIds = [...new Set(targetComposerIds)];
83
+
84
+ try {
85
+ const gDb = new Database(globalDbPath, { readonly: true, fileMustExist: true });
86
+ let rows = [];
87
+
88
+ if (targetComposerIds.length > 0) {
89
+ const placeholders = targetComposerIds.map(() => 'key LIKE ?').join(' OR ');
90
+ const params = targetComposerIds.map((id) => `bubbleId:${id}:%`);
91
+ rows = gDb
92
+ .prepare(`SELECT key, value FROM cursorDiskKV WHERE ${placeholders}`)
93
+ .all(...params);
94
+ }
95
+
96
+ if (rows.length === 0) {
97
+ rows = gDb
98
+ .prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' ORDER BY rowid DESC LIMIT 200")
99
+ .all();
100
+ }
101
+
102
+ const prompts = [];
103
+
104
+ for (const row of rows) {
105
+ try {
106
+ const data = JSON.parse(row.value);
107
+ let text = null;
108
+
109
+ if (typeof data.text === 'string') {
110
+ text = data.text;
111
+ } else if (Array.isArray(data.richText)) {
112
+ text = data.richText.map((n) => n.text || '').join(' ');
113
+ }
114
+
115
+ if (text) {
116
+ text = text.trim();
117
+
118
+ const isAssistant =
119
+ data.type === 2 ||
120
+ data.type === 'ai' ||
121
+ data.type === 'assistant' ||
122
+ data.bubbleType === 'ai' ||
123
+ data.bubbleType === 'assistant' ||
124
+ text.startsWith('#') ||
125
+ text.startsWith('```');
126
+
127
+ const isUser =
128
+ data.type === 1 ||
129
+ data.type === 'user' ||
130
+ data.bubbleType === 'user';
131
+
132
+ // Only keep user prompts, skip assistant replies & system notifications
133
+ if (isUser && !text.startsWith('<timestamp>') && !text.startsWith('<system_') && text.length > 2) {
134
+ // Normalize timestamp to integer epoch
135
+ const ts = data.createdAt || data.timestamp || data.clientTimestamp || 0;
136
+ const timeNum = typeof ts === 'string' ? new Date(ts).getTime() : Number(ts);
137
+
138
+ prompts.push({
139
+ prompt: text,
140
+ timestamp: timeNum || 0,
141
+ });
142
+ } else if (!isAssistant && !isUser && text.length > 2 && text.length < 500 && !text.startsWith('<timestamp>') && !text.startsWith('<system_')) {
143
+ const ts = data.createdAt || data.timestamp || data.clientTimestamp || 0;
144
+ const timeNum = typeof ts === 'string' ? new Date(ts).getTime() : Number(ts);
145
+
146
+ prompts.push({
147
+ prompt: text,
148
+ timestamp: timeNum || 0,
149
+ });
150
+ }
151
+ }
152
+ } catch { }
153
+ }
154
+
155
+ gDb.close();
156
+
157
+ // 1. Sort strictly descending by timestamp (newest first)
158
+ prompts.sort((a, b) => b.timestamp - a.timestamp);
159
+
160
+ // 2. Deduplicate keeping only the latest occurrence
161
+ const uniquePrompts = [];
162
+ for (const item of prompts) {
163
+ if (!uniquePrompts.some((p) => p.prompt === item.prompt)) {
164
+ uniquePrompts.push({
165
+ prompt: item.prompt,
166
+ timestamp: item.timestamp ? new Date(item.timestamp).toISOString() : null,
167
+ });
168
+ }
169
+ }
170
+
171
+ return uniquePrompts.slice(0, limit);
172
+ } catch (err) {
173
+ return [];
174
+ }
175
+ }
176
+
package/src/git.js ADDED
@@ -0,0 +1,67 @@
1
+ import { simpleGit } from 'simple-git';
2
+
3
+ const git = simpleGit();
4
+
5
+ /**
6
+ * Truncates diff text if it exceeds max character limit to prevent LLM token blowup.
7
+ */
8
+ function sanitizeDiff(diffStr, maxChars = 4000) {
9
+ if (!diffStr) return null;
10
+ if (diffStr.length > maxChars) {
11
+ return diffStr.slice(0, maxChars) + '\n\n... [diff truncated for length] ...';
12
+ }
13
+ return diffStr;
14
+ }
15
+
16
+ export async function getRecentChanges(maxCommits = 3) {
17
+ const isRepo = await git.checkIsRepo();
18
+ if (!isRepo) {
19
+ throw new Error('Not inside a valid Git repository.');
20
+ }
21
+
22
+ const status = await git.status();
23
+
24
+ // 1. Capture both unstaged and staged uncommitted diffs (excluding noisy lockfiles)
25
+ const lockfileIgnores = [
26
+ ':!package-lock.json',
27
+ ':!yarn.lock',
28
+ ':!pnpm-lock.yaml',
29
+ ':!bun.lockb',
30
+ ];
31
+
32
+ const unstagedDiff = await git.diff(['--', '.', ...lockfileIgnores]);
33
+ const stagedDiff = await git.diff(['--cached', '--', '.', ...lockfileIgnores]);
34
+
35
+ let combinedWorkingDiff = '';
36
+ if (stagedDiff) combinedWorkingDiff += `--- STAGED CHANGES ---\n${stagedDiff}\n`;
37
+ if (unstagedDiff) combinedWorkingDiff += `--- UNSTAGED CHANGES ---\n${unstagedDiff}\n`;
38
+
39
+ // 2. Fetch commit history with bounded diff size
40
+ const log = await git.log({ maxCount: maxCommits });
41
+ const commitDiffs = await Promise.all(
42
+ log.all.map(async (c) => {
43
+ const rawDiff = await git.show([
44
+ c.hash,
45
+ '-p',
46
+ '--',
47
+ '.',
48
+ ...lockfileIgnores,
49
+ ]);
50
+
51
+ return {
52
+ hash: c.hash.slice(0, 7),
53
+ author: c.author_name,
54
+ date: c.date,
55
+ message: c.message,
56
+ diff: sanitizeDiff(rawDiff),
57
+ };
58
+ })
59
+ );
60
+
61
+ return {
62
+ branch: status.current,
63
+ untrackedFiles: status.not_added, // Tells the LLM if newly created files exist
64
+ workingDiff: sanitizeDiff(combinedWorkingDiff) || null,
65
+ recentCommits: commitDiffs,
66
+ };
67
+ }
package/src/patch.js ADDED
@@ -0,0 +1,120 @@
1
+ // src/patch.js
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { spawn } from 'child_process';
5
+
6
+ export function extractDiff(llmOutput) {
7
+ const diffMatch =
8
+ llmOutput.match(/```diff\s*([\s\S]*?)```/) ||
9
+ llmOutput.match(/(--- [^\n]+\n\+\+\+ [^\n]+[\s\S]*)/);
10
+
11
+ if (!diffMatch) return null;
12
+ let rawDiff = diffMatch[1] || diffMatch[0];
13
+
14
+ rawDiff = rawDiff.replace(/\u00A0/g, ' ').replace(/\r\n/g, '\n');
15
+ rawDiff = rawDiff.replace(/^index [0-9a-f]+\.\.[0-9a-f]+.*$\n?/m, '');
16
+
17
+ if (!rawDiff.endsWith('\n')) {
18
+ rawDiff += '\n';
19
+ }
20
+
21
+ return rawDiff.trim() + '\n';
22
+ }
23
+
24
+ export async function applyDiff(diffString) {
25
+ const flags = [
26
+ '--recount',
27
+ '--ignore-space-change',
28
+ '--ignore-whitespace',
29
+ '--whitespace=nowarn',
30
+ '-C1'
31
+ ];
32
+
33
+ // 1. Try git apply
34
+ const attempt = await runGitApply(diffString, flags);
35
+ if (attempt.success) return true;
36
+
37
+ // 2. Try git apply with 3way
38
+ const attempt3Way = await runGitApply(diffString, [...flags, '--3way']);
39
+ if (attempt3Way.success) return true;
40
+
41
+ // 3. Fallback: Direct line search & replace on disk
42
+ const applied = fallbackManualPatch(diffString);
43
+ if (applied) return true;
44
+
45
+ throw new Error(`Patch cannot apply cleanly:\n${attempt.stderr}`);
46
+ }
47
+
48
+ function runGitApply(diffContent, flags = []) {
49
+ return new Promise((resolve) => {
50
+ const git = spawn('git', ['apply', ...flags, '-'], {
51
+ stdio: ['pipe', 'pipe', 'pipe']
52
+ });
53
+
54
+ let stderr = '';
55
+ git.stderr.on('data', (chunk) => {
56
+ stderr += chunk.toString();
57
+ });
58
+
59
+ git.on('close', (code) => {
60
+ resolve({
61
+ success: code === 0,
62
+ stderr: stderr.trim()
63
+ });
64
+ });
65
+
66
+ git.stdin.write(diffContent);
67
+ git.stdin.end();
68
+ });
69
+ }
70
+
71
+ function fallbackManualPatch(diffString) {
72
+ try {
73
+ const fileMatch = diffString.match(/\+\+\+ b\/(.+)/);
74
+ if (!fileMatch) return false;
75
+
76
+ const relativePath = fileMatch[1].trim();
77
+ const filePath = path.resolve(process.cwd(), relativePath);
78
+ if (!fs.existsSync(filePath)) return false;
79
+
80
+ const fileContent = fs.readFileSync(filePath, 'utf8');
81
+
82
+ // Extract removed (-) and added (+) lines from the hunk
83
+ const lines = diffString.split('\n');
84
+ let oldLine = null;
85
+ let newLine = null;
86
+
87
+ for (const line of lines) {
88
+ if (line.startsWith('-') && !line.startsWith('---')) {
89
+ oldLine = line.slice(1).trim();
90
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
91
+ newLine = line.slice(1).trim();
92
+ }
93
+ }
94
+
95
+ if (!oldLine || !newLine) return false;
96
+
97
+ // Scan file lines and replace the matching content while preserving original indentation
98
+ const fileLines = fileContent.split('\n');
99
+ let replaced = false;
100
+
101
+ for (let i = 0; i < fileLines.length; i++) {
102
+ if (fileLines[i].includes(oldLine)) {
103
+ const indentMatch = fileLines[i].match(/^\s*/);
104
+ const indent = indentMatch ? indentMatch[0] : '';
105
+ fileLines[i] = indent + newLine;
106
+ replaced = true;
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (replaced) {
112
+ fs.writeFileSync(filePath, fileLines.join('\n'), 'utf8');
113
+ return true;
114
+ }
115
+
116
+ return false;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
package/src/triage.js ADDED
@@ -0,0 +1,69 @@
1
+ // src/triage.js
2
+ import OpenAI from 'openai';
3
+ import Anthropic from '@anthropic-ai/sdk';
4
+
5
+ export async function triageRegression(errorDescription, gitContext, cursorPrompts = []) {
6
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
7
+ const openaiKey = process.env.OPENAI_API_KEY;
8
+
9
+ if (!anthropicKey && !openaiKey) {
10
+ throw new Error('Please set either ANTHROPIC_API_KEY or OPENAI_API_KEY in your environment.');
11
+ }
12
+
13
+ const systemInstructions = `
14
+ You are an expert developer tool agent specializing in Git diff analysis, AST regression diagnosis, and code patch generation.
15
+
16
+ When analyzing the regression:
17
+ - Focus on contract/schema shifts between callers and callees (e.g. key renames in returned objects, changed parameter signatures).
18
+ - Prefer aligning property access to match the actual returned schema (e.g. renaming \`profile.userId\` to \`profile.user_id\`) rather than applying defensive null checks or fallbacks like ternary operators or 'UNKNOWN', unless explicitly intended.
19
+
20
+ When generating the "Patch" section:
21
+ - Always use a valid, standard unified diff format enclosed in \`\`\`diff.
22
+ - Include proper headers (--- a/path/to/file.js, +++ b/path/to/file.js).
23
+ - Ensure @@ -line,count +line,count @@ markers accurately match the context.
24
+ - Include 1-2 lines of unchanged surrounding context above and below the change.
25
+ - Only touch lines that actually exist in the file.
26
+ - Do not invent comments in context lines.
27
+ `.trim();
28
+
29
+ const userPrompt = `
30
+ User problem / error trace:
31
+ "${errorDescription}"
32
+
33
+ Recent Cursor Composer Prompts:
34
+ ${JSON.stringify(cursorPrompts, null, 2)}
35
+
36
+ Recent Repository Git Stream (Working diff + Commits):
37
+ ${JSON.stringify(gitContext, null, 2)}
38
+
39
+ Provide a strict, concise triage output formatted with terminal-friendly Markdown:
40
+ 1. **Root Cause**: Pinpoint the file, line, and commit/working diff where the breaking change occurred.
41
+ 2. **Attributed Prompt**: Name the specific Cursor prompt that likely caused this diff (if found in the list; otherwise state none).
42
+ 3. **The Shift**: Explain what broke (e.g., schema key rename, dropped parameter, async/await omission).
43
+ 4. **Patch**: Provide the exact unified diff adhering strictly to the system diff formatting instructions.
44
+ `;
45
+
46
+ // 1. Prefer Anthropic if key exists
47
+ if (anthropicKey) {
48
+ const anthropic = new Anthropic({ apiKey: anthropicKey });
49
+ const response = await anthropic.messages.create({
50
+ model: 'claude-3-5-sonnet-20241022',
51
+ max_tokens: 1500,
52
+ system: systemInstructions,
53
+ messages: [{ role: 'user', content: userPrompt }]
54
+ });
55
+ return response.content[0].text;
56
+ }
57
+
58
+ // 2. Fallback to OpenAI
59
+ const openai = new OpenAI({ apiKey: openaiKey });
60
+ const response = await openai.chat.completions.create({
61
+ model: 'gpt-4o-mini',
62
+ temperature: 0.1,
63
+ messages: [
64
+ { role: 'system', content: systemInstructions },
65
+ { role: 'user', content: userPrompt }
66
+ ]
67
+ });
68
+ return response.choices[0].message.content;
69
+ }