@vibecheckai/cli 3.2.0 → 3.2.2

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.
Files changed (60) hide show
  1. package/bin/runners/lib/agent-firewall/change-packet/builder.js +214 -0
  2. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  3. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  4. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  5. package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
  6. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  7. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  8. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  9. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
  10. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  11. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
  12. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  13. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  14. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  15. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  16. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  17. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  18. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  19. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  20. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  21. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  22. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  23. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  24. package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
  25. package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
  26. package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
  27. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  28. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  29. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
  30. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
  31. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
  32. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  33. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  34. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  35. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  36. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  37. package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
  38. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  39. package/bin/runners/lib/analysis-core.js +198 -180
  40. package/bin/runners/lib/analyzers.js +1119 -536
  41. package/bin/runners/lib/cli-output.js +236 -210
  42. package/bin/runners/lib/detectors-v2.js +547 -785
  43. package/bin/runners/lib/fingerprint.js +377 -0
  44. package/bin/runners/lib/route-truth.js +1167 -322
  45. package/bin/runners/lib/scan-output.js +144 -738
  46. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  47. package/bin/runners/lib/terminal-ui.js +188 -770
  48. package/bin/runners/lib/truth.js +1004 -321
  49. package/bin/runners/lib/unified-output.js +162 -158
  50. package/bin/runners/runAgent.js +161 -0
  51. package/bin/runners/runFirewall.js +134 -0
  52. package/bin/runners/runFirewallHook.js +56 -0
  53. package/bin/runners/runScan.js +113 -10
  54. package/bin/runners/runShip.js +7 -8
  55. package/bin/runners/runTruth.js +89 -0
  56. package/mcp-server/agent-firewall-interceptor.js +164 -0
  57. package/mcp-server/index.js +347 -313
  58. package/mcp-server/truth-context.js +131 -90
  59. package/mcp-server/truth-firewall-tools.js +1412 -1045
  60. package/package.json +1 -1
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Git Pre-Commit Hook
3
+ *
4
+ * Validates all staged changes against firewall policy before allowing commit.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const { execSync } = require("child_process");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { interceptFileWrite } = require("../interceptor/base");
13
+ const { loadPolicy } = require("../policy/loader");
14
+
15
+ /**
16
+ * Run pre-commit validation
17
+ * @param {string} projectRoot - Project root directory
18
+ * @returns {object} Validation result
19
+ */
20
+ async function validatePreCommit(projectRoot) {
21
+ const policy = loadPolicy(projectRoot);
22
+
23
+ // Only enforce in enforce mode
24
+ if (policy.mode !== "enforce") {
25
+ return { allowed: true, message: "Firewall in observe mode - commit allowed" };
26
+ }
27
+
28
+ try {
29
+ // Get staged files
30
+ const stagedFiles = getStagedFiles(projectRoot);
31
+
32
+ if (stagedFiles.length === 0) {
33
+ return { allowed: true, message: "No files staged" };
34
+ }
35
+
36
+ console.log(`🛡️ Agent Firewall: Validating ${stagedFiles.length} staged file(s)...\n`);
37
+
38
+ const violations = [];
39
+ const blockedFiles = [];
40
+
41
+ // Validate each staged file
42
+ for (const filePath of stagedFiles) {
43
+ const fileAbs = path.join(projectRoot, filePath);
44
+
45
+ if (!fs.existsSync(fileAbs)) {
46
+ continue; // File was deleted, skip
47
+ }
48
+
49
+ // Get old content from git
50
+ let oldContent = null;
51
+ try {
52
+ oldContent = execSync(
53
+ `git show :${filePath}`,
54
+ { cwd: projectRoot, encoding: "utf8", stdio: "pipe" }
55
+ );
56
+ } catch {
57
+ // File not in git (new file), that's okay
58
+ }
59
+
60
+ // Get new content
61
+ const newContent = fs.readFileSync(fileAbs, "utf8");
62
+
63
+ // Intercept the change
64
+ const result = await interceptFileWrite({
65
+ projectRoot,
66
+ agentId: "git-pre-commit",
67
+ intent: `Git commit: ${filePath}`,
68
+ filePath: filePath,
69
+ content: newContent,
70
+ oldContent: oldContent
71
+ });
72
+
73
+ if (!result.allowed) {
74
+ violations.push(...(result.violations || []));
75
+ blockedFiles.push({
76
+ file: filePath,
77
+ violations: result.violations || [],
78
+ unblockPlan: result.unblockPlan
79
+ });
80
+ }
81
+ }
82
+
83
+ if (blockedFiles.length > 0) {
84
+ console.error("\n❌ COMMIT BLOCKED by Agent Firewall\n");
85
+ console.error("Violations detected:\n");
86
+
87
+ blockedFiles.forEach(({ file, violations: fileViolations }) => {
88
+ console.error(` ${file}:`);
89
+ fileViolations.forEach(v => {
90
+ console.error(` - ${v.rule}: ${v.message}`);
91
+ });
92
+ console.error("");
93
+ });
94
+
95
+ // Show unblock plans
96
+ const allPlans = blockedFiles
97
+ .filter(bf => bf.unblockPlan && bf.unblockPlan.steps.length > 0)
98
+ .flatMap(bf => bf.unblockPlan.steps);
99
+
100
+ if (allPlans.length > 0) {
101
+ console.error("To fix:");
102
+ const uniqueSteps = new Map();
103
+ allPlans.forEach(step => {
104
+ const key = `${step.action}:${step.file}`;
105
+ if (!uniqueSteps.has(key)) {
106
+ uniqueSteps.set(key, step);
107
+ }
108
+ });
109
+ Array.from(uniqueSteps.values()).forEach((step, i) => {
110
+ console.error(` ${i + 1}. ${step.action.toUpperCase()}: ${step.file}`);
111
+ console.error(` ${step.description}`);
112
+ });
113
+ console.error("");
114
+ }
115
+
116
+ return {
117
+ allowed: false,
118
+ message: `Commit blocked: ${blockedFiles.length} file(s) have violations`,
119
+ blockedFiles,
120
+ violations
121
+ };
122
+ }
123
+
124
+ console.log("✅ All staged files validated - commit allowed\n");
125
+ return { allowed: true, message: "All files validated" };
126
+
127
+ } catch (error) {
128
+ console.error(`Error validating commit: ${error.message}`);
129
+ // In case of error, allow commit (fail open)
130
+ return { allowed: true, message: `Validation error: ${error.message}` };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get list of staged files
136
+ * @param {string} projectRoot - Project root directory
137
+ * @returns {array} Array of staged file paths
138
+ */
139
+ function getStagedFiles(projectRoot) {
140
+ try {
141
+ const output = execSync(
142
+ "git diff --cached --name-only --diff-filter=ACMR",
143
+ { cwd: projectRoot, encoding: "utf8", stdio: "pipe" }
144
+ );
145
+ return output
146
+ .trim()
147
+ .split("\n")
148
+ .filter(Boolean)
149
+ .filter(file => {
150
+ // Only check code files
151
+ const ext = path.extname(file);
152
+ return [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"].includes(ext);
153
+ });
154
+ } catch (error) {
155
+ // Not a git repo or no staged files
156
+ return [];
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ validatePreCommit,
162
+ getStagedFiles
163
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Cursor IDE Extension Hook
3
+ *
4
+ * Intercepts file writes at the IDE level for Cursor.
5
+ * Uses Cursor's extension API to hook into file save events.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const { interceptFileWrite } = require("../interceptor/base");
11
+ const { loadPolicy } = require("../policy/loader");
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+
15
+ /**
16
+ * Cursor extension hook
17
+ * This would be called by a Cursor extension
18
+ */
19
+ class CursorFirewallHook {
20
+ constructor(projectRoot) {
21
+ this.projectRoot = projectRoot;
22
+ this.isEnabled = false;
23
+ }
24
+
25
+ /**
26
+ * Enable the hook
27
+ */
28
+ enable() {
29
+ this.isEnabled = true;
30
+ this.createCursorConfig();
31
+ }
32
+
33
+ /**
34
+ * Create Cursor configuration file
35
+ */
36
+ createCursorConfig() {
37
+ const cursorDir = path.join(this.projectRoot, ".cursor");
38
+ if (!fs.existsSync(cursorDir)) {
39
+ fs.mkdirSync(cursorDir, { recursive: true });
40
+ }
41
+
42
+ const configFile = path.join(cursorDir, "firewall-hook.json");
43
+ const config = {
44
+ enabled: true,
45
+ mode: "enforce",
46
+ interceptOnSave: true,
47
+ interceptOnCreate: true,
48
+ interceptOnEdit: false // Only on save/create, not every keystroke
49
+ };
50
+
51
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
52
+ }
53
+
54
+ /**
55
+ * Intercept file save (called by Cursor extension)
56
+ */
57
+ async interceptSave(filePath, content, oldContent) {
58
+ if (!this.isEnabled) {
59
+ return { allowed: true };
60
+ }
61
+
62
+ const policy = loadPolicy(this.projectRoot);
63
+
64
+ // In observe mode, just log
65
+ if (policy.mode === "observe") {
66
+ const result = await interceptFileWrite({
67
+ projectRoot: this.projectRoot,
68
+ agentId: "cursor-ide",
69
+ intent: "File save in Cursor",
70
+ filePath: filePath,
71
+ content: content,
72
+ oldContent: oldContent
73
+ });
74
+
75
+ if (result.violations && result.violations.length > 0) {
76
+ console.log(`📊 OBSERVE: ${filePath} - violations logged`);
77
+ }
78
+
79
+ return { allowed: true };
80
+ }
81
+
82
+ // In enforce mode, block violations
83
+ const result = await interceptFileWrite({
84
+ projectRoot: this.projectRoot,
85
+ agentId: "cursor-ide",
86
+ intent: "File save in Cursor",
87
+ filePath: filePath,
88
+ content: content,
89
+ oldContent: oldContent
90
+ });
91
+
92
+ if (!result.allowed) {
93
+ return {
94
+ allowed: false,
95
+ message: result.message,
96
+ violations: result.violations,
97
+ unblockPlan: result.unblockPlan
98
+ };
99
+ }
100
+
101
+ return { allowed: true };
102
+ }
103
+ }
104
+
105
+ module.exports = {
106
+ CursorFirewallHook
107
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * VS Code Extension Hook
3
+ *
4
+ * Intercepts file writes at the IDE level for VS Code.
5
+ * Uses VS Code's extension API.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const { interceptFileWrite } = require("../interceptor/base");
11
+ const { loadPolicy } = require("../policy/loader");
12
+
13
+ /**
14
+ * VS Code extension hook
15
+ * This would be called by a VS Code extension
16
+ */
17
+ class VSCodeFirewallHook {
18
+ constructor(projectRoot) {
19
+ this.projectRoot = projectRoot;
20
+ this.isEnabled = false;
21
+ }
22
+
23
+ /**
24
+ * Enable the hook
25
+ */
26
+ enable() {
27
+ this.isEnabled = true;
28
+ }
29
+
30
+ /**
31
+ * Intercept file save (called by VS Code extension)
32
+ */
33
+ async interceptSave(filePath, content, oldContent) {
34
+ if (!this.isEnabled) {
35
+ return { allowed: true };
36
+ }
37
+
38
+ const policy = loadPolicy(this.projectRoot);
39
+
40
+ const result = await interceptFileWrite({
41
+ projectRoot: this.projectRoot,
42
+ agentId: "vscode-ide",
43
+ intent: "File save in VS Code",
44
+ filePath: filePath,
45
+ content: content,
46
+ oldContent: oldContent
47
+ });
48
+
49
+ if (policy.mode === "observe") {
50
+ return { allowed: true }; // Always allow in observe mode
51
+ }
52
+
53
+ if (!result.allowed) {
54
+ return {
55
+ allowed: false,
56
+ message: result.message,
57
+ violations: result.violations,
58
+ unblockPlan: result.unblockPlan
59
+ };
60
+ }
61
+
62
+ return { allowed: true };
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ VSCodeFirewallHook
68
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Windsurf IDE Extension Hook
3
+ *
4
+ * Intercepts file writes at the IDE level for Windsurf.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const { interceptFileWrite } = require("../interceptor/base");
10
+ const { loadPolicy } = require("../policy/loader");
11
+
12
+ /**
13
+ * Windsurf extension hook
14
+ */
15
+ class WindsurfFirewallHook {
16
+ constructor(projectRoot) {
17
+ this.projectRoot = projectRoot;
18
+ this.isEnabled = false;
19
+ }
20
+
21
+ /**
22
+ * Enable the hook
23
+ */
24
+ enable() {
25
+ this.isEnabled = true;
26
+ }
27
+
28
+ /**
29
+ * Intercept file save
30
+ */
31
+ async interceptSave(filePath, content, oldContent) {
32
+ if (!this.isEnabled) {
33
+ return { allowed: true };
34
+ }
35
+
36
+ const policy = loadPolicy(this.projectRoot);
37
+
38
+ const result = await interceptFileWrite({
39
+ projectRoot: this.projectRoot,
40
+ agentId: "windsurf-ide",
41
+ intent: "File save in Windsurf",
42
+ filePath: filePath,
43
+ content: content,
44
+ oldContent: oldContent
45
+ });
46
+
47
+ if (policy.mode === "observe") {
48
+ return { allowed: true };
49
+ }
50
+
51
+ if (!result.allowed) {
52
+ return {
53
+ allowed: false,
54
+ message: result.message,
55
+ violations: result.violations,
56
+ unblockPlan: result.unblockPlan
57
+ };
58
+ }
59
+
60
+ return { allowed: true };
61
+ }
62
+ }
63
+
64
+ module.exports = {
65
+ WindsurfFirewallHook
66
+ };
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Base Interceptor
3
+ *
4
+ * Common logic for all IDE interceptors.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const path = require("path");
10
+ const fs = require("fs");
11
+ const { extractClaims } = require("../claims/extractor");
12
+ const { resolveEvidence } = require("../evidence/resolver");
13
+ const { evaluatePolicy } = require("../policy/engine");
14
+ const { buildChangePacket, buildMultiFileChangePacket } = require("../change-packet/builder");
15
+ const { storePacket } = require("../change-packet/store");
16
+ const { generateUnblockPlan } = require("../unblock/planner");
17
+ const { loadPolicy } = require("../policy/loader");
18
+
19
+ /**
20
+ * Intercept a file write attempt
21
+ * @param {object} params
22
+ * @param {string} params.projectRoot - Project root directory
23
+ * @param {string} params.agentId - Agent identifier
24
+ * @param {string} params.intent - Agent intent message
25
+ * @param {string} params.filePath - File path (relative to project root)
26
+ * @param {string} params.content - New file content
27
+ * @param {string} params.oldContent - Old file content (optional)
28
+ * @returns {object} Interception result
29
+ */
30
+ async function interceptFileWrite({
31
+ projectRoot,
32
+ agentId,
33
+ intent,
34
+ filePath,
35
+ content,
36
+ oldContent = null
37
+ }) {
38
+ // Load policy
39
+ const policy = loadPolicy(projectRoot);
40
+
41
+ // Generate diff
42
+ const diff = generateDiff(oldContent, content);
43
+
44
+ // Extract claims - write content to temp file if file doesn't exist
45
+ const fileAbs = path.join(projectRoot, filePath);
46
+ let tempFile = null;
47
+
48
+ if (!fs.existsSync(fileAbs) && content) {
49
+ // Write content to temp file for extraction
50
+ const tempDir = path.join(projectRoot, ".vibecheck", "temp");
51
+ if (!fs.existsSync(tempDir)) {
52
+ fs.mkdirSync(tempDir, { recursive: true });
53
+ }
54
+ tempFile = path.join(tempDir, path.basename(filePath));
55
+ fs.writeFileSync(tempFile, content, "utf8");
56
+ }
57
+
58
+ const fileToExtract = fs.existsSync(fileAbs) ? fileAbs : (tempFile || fileAbs);
59
+ const { claims } = extractClaims({
60
+ repoRoot: projectRoot,
61
+ changedFilesAbs: [fileToExtract]
62
+ });
63
+
64
+ // Clean up temp file
65
+ if (tempFile && fs.existsSync(tempFile)) {
66
+ fs.unlinkSync(tempFile);
67
+ }
68
+
69
+ // Resolve evidence
70
+ const evidence = resolveEvidence(projectRoot, claims);
71
+
72
+ // Build file info
73
+ const files = [{
74
+ path: filePath,
75
+ linesChanged: calculateLinesChanged(diff),
76
+ domain: classifyFileDomain(filePath)
77
+ }];
78
+
79
+ // Evaluate policy
80
+ const verdict = evaluatePolicy({
81
+ policy,
82
+ claims,
83
+ evidence,
84
+ files,
85
+ intent
86
+ });
87
+
88
+ // Generate unblock plan if blocked
89
+ let unblockPlan = null;
90
+ if (verdict.decision === "BLOCK") {
91
+ unblockPlan = generateUnblockPlan({
92
+ violations: verdict.violations,
93
+ claims,
94
+ projectRoot
95
+ });
96
+ }
97
+
98
+ // Build change packet
99
+ const packet = buildChangePacket({
100
+ agentId,
101
+ intent,
102
+ diff,
103
+ filePath,
104
+ claims,
105
+ evidence,
106
+ verdict,
107
+ unblockPlan,
108
+ policy
109
+ });
110
+
111
+ // Store packet if policy requires it
112
+ if (policy.output?.write_change_packets !== false) {
113
+ storePacket(projectRoot, packet);
114
+ }
115
+
116
+ // Return interception result
117
+ return {
118
+ allowed: verdict.decision === "ALLOW",
119
+ verdict: verdict.decision,
120
+ violations: verdict.violations,
121
+ unblockPlan,
122
+ packetId: packet.id,
123
+ message: verdict.message
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Intercept multiple file writes
129
+ */
130
+ async function interceptMultiFileWrite({
131
+ projectRoot,
132
+ agentId,
133
+ intent,
134
+ changes
135
+ }) {
136
+ // Load policy
137
+ const policy = loadPolicy(projectRoot);
138
+
139
+ // Extract claims from all files
140
+ const allClaims = [];
141
+ const allFiles = [];
142
+
143
+ for (const change of changes) {
144
+ const fileAbs = path.join(projectRoot, change.filePath);
145
+ const { claims } = extractClaims({
146
+ repoRoot: projectRoot,
147
+ changedFilesAbs: [fileAbs]
148
+ });
149
+
150
+ allClaims.push(...claims.map(c => ({ ...c, file: change.filePath })));
151
+ allFiles.push({
152
+ path: change.filePath,
153
+ linesChanged: calculateLinesChanged(change.diff),
154
+ domain: classifyFileDomain(change.filePath)
155
+ });
156
+ }
157
+
158
+ // Resolve evidence
159
+ const evidence = resolveEvidence(projectRoot, allClaims);
160
+
161
+ // Evaluate policy
162
+ const verdict = evaluatePolicy({
163
+ policy,
164
+ claims: allClaims,
165
+ evidence,
166
+ files: allFiles,
167
+ intent
168
+ });
169
+
170
+ // Generate unblock plan if blocked
171
+ let unblockPlan = null;
172
+ if (verdict.decision === "BLOCK") {
173
+ unblockPlan = generateUnblockPlan({
174
+ violations: verdict.violations,
175
+ claims: allClaims,
176
+ projectRoot
177
+ });
178
+ }
179
+
180
+ // Build change packet
181
+ const packet = buildMultiFileChangePacket({
182
+ agentId,
183
+ intent,
184
+ changes: changes.map(c => ({
185
+ filePath: c.filePath,
186
+ diff: c.diff,
187
+ claims: allClaims.filter(cl => cl.file === c.filePath)
188
+ })),
189
+ evidence,
190
+ verdict,
191
+ unblockPlan,
192
+ policy
193
+ });
194
+
195
+ // Store packet if policy requires it
196
+ if (policy.output?.write_change_packets !== false) {
197
+ storePacket(projectRoot, packet);
198
+ }
199
+
200
+ // Return interception result
201
+ return {
202
+ allowed: verdict.decision === "ALLOW",
203
+ verdict: verdict.decision,
204
+ violations: verdict.violations,
205
+ unblockPlan,
206
+ packetId: packet.id,
207
+ message: verdict.message
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Generate unified diff
213
+ */
214
+ function generateDiff(oldContent, newContent) {
215
+ if (!oldContent) {
216
+ return {
217
+ before: "",
218
+ after: newContent,
219
+ unified: generateUnifiedDiff("", newContent)
220
+ };
221
+ }
222
+
223
+ return {
224
+ before: oldContent,
225
+ after: newContent,
226
+ unified: generateUnifiedDiff(oldContent, newContent)
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Generate unified diff format
232
+ */
233
+ function generateUnifiedDiff(oldContent, newContent) {
234
+ const oldLines = oldContent.split(/\r?\n/);
235
+ const newLines = newContent.split(/\r?\n/);
236
+
237
+ const diff = [];
238
+ let i = 0, j = 0;
239
+
240
+ while (i < oldLines.length || j < newLines.length) {
241
+ if (i >= oldLines.length) {
242
+ diff.push(`+${newLines[j]}`);
243
+ j++;
244
+ } else if (j >= newLines.length) {
245
+ diff.push(`-${oldLines[i]}`);
246
+ i++;
247
+ } else if (oldLines[i] === newLines[j]) {
248
+ diff.push(` ${oldLines[i]}`);
249
+ i++;
250
+ j++;
251
+ } else {
252
+ // Try to find matching line ahead
253
+ let found = false;
254
+ for (let k = j + 1; k < Math.min(j + 10, newLines.length); k++) {
255
+ if (oldLines[i] === newLines[k]) {
256
+ // Add new lines
257
+ for (let l = j; l < k; l++) {
258
+ diff.push(`+${newLines[l]}`);
259
+ }
260
+ j = k;
261
+ found = true;
262
+ break;
263
+ }
264
+ }
265
+
266
+ if (!found) {
267
+ diff.push(`-${oldLines[i]}`);
268
+ diff.push(`+${newLines[j]}`);
269
+ i++;
270
+ j++;
271
+ }
272
+ }
273
+ }
274
+
275
+ return diff.join("\n");
276
+ }
277
+
278
+ /**
279
+ * Calculate lines changed from diff
280
+ */
281
+ function calculateLinesChanged(diff) {
282
+ if (!diff || !diff.unified) return 0;
283
+ return diff.unified.split('\n').filter(line =>
284
+ line.startsWith('+') || line.startsWith('-')
285
+ ).length;
286
+ }
287
+
288
+ /**
289
+ * Classify file domain
290
+ */
291
+ function classifyFileDomain(filePath) {
292
+ const s = filePath.toLowerCase();
293
+ if (s.includes("auth")) return "auth";
294
+ if (s.includes("stripe") || s.includes("payment")) return "payments";
295
+ if (s.includes("routes") || s.includes("router") || s.includes("api")) return "routes";
296
+ if (s.includes("schema") || s.includes("contract") || s.includes("openapi")) return "contracts";
297
+ if (s.includes("ui") || s.includes("components") || s.includes("pages")) return "ui";
298
+ return "general";
299
+ }
300
+
301
+ module.exports = {
302
+ interceptFileWrite,
303
+ interceptMultiFileWrite
304
+ };