codekin 0.3.7 → 0.4.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.
Files changed (50) hide show
  1. package/README.md +2 -1
  2. package/dist/assets/index-BAdQqYEY.js +182 -0
  3. package/dist/assets/index-CeZYNLWt.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +8 -2
  6. package/server/dist/approval-manager.d.ts +44 -8
  7. package/server/dist/approval-manager.js +262 -23
  8. package/server/dist/approval-manager.js.map +1 -1
  9. package/server/dist/claude-process.d.ts +16 -0
  10. package/server/dist/claude-process.js +35 -10
  11. package/server/dist/claude-process.js.map +1 -1
  12. package/server/dist/commit-event-handler.d.ts +41 -0
  13. package/server/dist/commit-event-handler.js +99 -0
  14. package/server/dist/commit-event-handler.js.map +1 -0
  15. package/server/dist/commit-event-hooks.d.ts +35 -0
  16. package/server/dist/commit-event-hooks.js +177 -0
  17. package/server/dist/commit-event-hooks.js.map +1 -0
  18. package/server/dist/crypto-utils.js +10 -5
  19. package/server/dist/crypto-utils.js.map +1 -1
  20. package/server/dist/diff-parser.d.ts +23 -0
  21. package/server/dist/diff-parser.js +236 -0
  22. package/server/dist/diff-parser.js.map +1 -0
  23. package/server/dist/session-manager.d.ts +25 -8
  24. package/server/dist/session-manager.js +364 -29
  25. package/server/dist/session-manager.js.map +1 -1
  26. package/server/dist/session-routes.js +101 -4
  27. package/server/dist/session-routes.js.map +1 -1
  28. package/server/dist/stepflow-handler.js +17 -3
  29. package/server/dist/stepflow-handler.js.map +1 -1
  30. package/server/dist/tsconfig.tsbuildinfo +1 -1
  31. package/server/dist/types.d.ts +62 -1
  32. package/server/dist/upload-routes.d.ts +1 -1
  33. package/server/dist/upload-routes.js +40 -13
  34. package/server/dist/upload-routes.js.map +1 -1
  35. package/server/dist/webhook-workspace.js +5 -2
  36. package/server/dist/webhook-workspace.js.map +1 -1
  37. package/server/dist/workflow-loader.d.ts +6 -0
  38. package/server/dist/workflow-loader.js +11 -0
  39. package/server/dist/workflow-loader.js.map +1 -1
  40. package/server/dist/workflow-routes.d.ts +5 -1
  41. package/server/dist/workflow-routes.js +48 -7
  42. package/server/dist/workflow-routes.js.map +1 -1
  43. package/server/dist/ws-message-handler.js +20 -0
  44. package/server/dist/ws-message-handler.js.map +1 -1
  45. package/server/dist/ws-server.js +19 -3
  46. package/server/dist/ws-server.js.map +1 -1
  47. package/server/workflows/commit-review.md +22 -0
  48. package/server/workflows/docs-audit.weekly.md +97 -0
  49. package/dist/assets/index-Dc76fIdG.js +0 -174
  50. package/dist/assets/index-DoL2Uppj.css +0 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Commit-review event dispatcher.
3
+ *
4
+ * Receives commit events from git post-commit hooks and dispatches
5
+ * commit-review workflow runs after passing a multi-layer filter chain
6
+ * for cycle prevention and deduplication.
7
+ *
8
+ * Filter chain (defense in depth):
9
+ * 1. Branch filter — reject if `codekin/reports`
10
+ * 2. Message filter — reject if message starts with any workflow commitMessage prefix
11
+ * 3. Config lookup — reject if no enabled `commit-review` workflow for this repo
12
+ * 4. Commit hash dedup — in-memory Map with 1h TTL, reject duplicates
13
+ * 5. Concurrency cap — max 1 running commit-review per repo
14
+ */
15
+ export interface CommitEvent {
16
+ repoPath: string;
17
+ branch: string;
18
+ commitHash: string;
19
+ commitMessage: string;
20
+ author: string;
21
+ }
22
+ export interface CommitEventResult {
23
+ accepted: boolean;
24
+ reason?: string;
25
+ runId?: string;
26
+ }
27
+ export declare class CommitEventHandler {
28
+ /** commitHash → timestamp of when it was recorded. */
29
+ private seenCommits;
30
+ private cleanupTimer;
31
+ constructor();
32
+ /**
33
+ * Process a commit event through the filter chain.
34
+ * Returns accepted=true if a workflow run was dispatched.
35
+ */
36
+ handle(event: CommitEvent): Promise<CommitEventResult>;
37
+ /** Remove dedup entries older than the TTL. */
38
+ private pruneExpired;
39
+ /** Clean up resources. */
40
+ shutdown(): void;
41
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Commit-review event dispatcher.
3
+ *
4
+ * Receives commit events from git post-commit hooks and dispatches
5
+ * commit-review workflow runs after passing a multi-layer filter chain
6
+ * for cycle prevention and deduplication.
7
+ *
8
+ * Filter chain (defense in depth):
9
+ * 1. Branch filter — reject if `codekin/reports`
10
+ * 2. Message filter — reject if message starts with any workflow commitMessage prefix
11
+ * 3. Config lookup — reject if no enabled `commit-review` workflow for this repo
12
+ * 4. Commit hash dedup — in-memory Map with 1h TTL, reject duplicates
13
+ * 5. Concurrency cap — max 1 running commit-review per repo
14
+ */
15
+ import { getWorkflowEngine } from './workflow-engine.js';
16
+ import { loadWorkflowConfig } from './workflow-config.js';
17
+ import { getWorkflowCommitPrefixes } from './workflow-loader.js';
18
+ // ---------------------------------------------------------------------------
19
+ // CommitEventHandler
20
+ // ---------------------------------------------------------------------------
21
+ /** TTL for commit hash deduplication entries (1 hour). */
22
+ const DEDUP_TTL_MS = 60 * 60 * 1000;
23
+ /** Interval for cleaning up expired dedup entries (10 minutes). */
24
+ const DEDUP_CLEANUP_INTERVAL_MS = 10 * 60 * 1000;
25
+ export class CommitEventHandler {
26
+ /** commitHash → timestamp of when it was recorded. */
27
+ seenCommits = new Map();
28
+ cleanupTimer;
29
+ constructor() {
30
+ // Periodically prune expired dedup entries
31
+ this.cleanupTimer = setInterval(() => this.pruneExpired(), DEDUP_CLEANUP_INTERVAL_MS);
32
+ }
33
+ /**
34
+ * Process a commit event through the filter chain.
35
+ * Returns accepted=true if a workflow run was dispatched.
36
+ */
37
+ async handle(event) {
38
+ // Layer 1: Branch filter
39
+ if (event.branch === 'codekin/reports') {
40
+ return { accepted: false, reason: 'Rejected: reports branch' };
41
+ }
42
+ // Layer 2: Message filter — reject if message starts with any workflow commit prefix
43
+ const prefixes = getWorkflowCommitPrefixes();
44
+ for (const prefix of prefixes) {
45
+ if (event.commitMessage.startsWith(prefix)) {
46
+ return { accepted: false, reason: `Rejected: workflow commit message (${prefix})` };
47
+ }
48
+ }
49
+ // Layer 3: Config lookup — find an enabled commit-review workflow for this repo
50
+ const config = loadWorkflowConfig();
51
+ const repoConfig = config.reviewRepos.find(r => r.repoPath === event.repoPath && r.enabled && r.kind === 'commit-review');
52
+ if (!repoConfig) {
53
+ return { accepted: false, reason: 'Rejected: no enabled commit-review config for this repo' };
54
+ }
55
+ // Layer 4: Commit hash dedup
56
+ if (this.seenCommits.has(event.commitHash)) {
57
+ return { accepted: false, reason: 'Rejected: duplicate commit hash' };
58
+ }
59
+ this.seenCommits.set(event.commitHash, Date.now());
60
+ // Layer 5: Concurrency cap — max 1 running commit-review per repo
61
+ const engine = getWorkflowEngine();
62
+ const activeRuns = engine.listRuns({ kind: 'commit-review', status: 'running', limit: 100 });
63
+ const hasActiveRun = activeRuns.some(run => run.input.repoPath === event.repoPath);
64
+ if (hasActiveRun) {
65
+ return { accepted: false, reason: 'Rejected: commit-review already running for this repo' };
66
+ }
67
+ // All filters passed — dispatch the workflow
68
+ try {
69
+ const run = await engine.startRun('commit-review', {
70
+ repoPath: event.repoPath,
71
+ repoName: repoConfig.name,
72
+ commitHash: event.commitHash,
73
+ customPrompt: repoConfig.customPrompt,
74
+ model: repoConfig.model,
75
+ });
76
+ console.log(`[commit-event] Dispatched commit-review run ${run.id} for ${event.repoPath} (${event.commitHash.slice(0, 8)})`);
77
+ return { accepted: true, runId: run.id };
78
+ }
79
+ catch (err) {
80
+ const msg = err instanceof Error ? err.message : String(err);
81
+ console.error(`[commit-event] Failed to start run for ${event.repoPath}:`, msg);
82
+ return { accepted: false, reason: `Failed to start run: ${msg}` };
83
+ }
84
+ }
85
+ /** Remove dedup entries older than the TTL. */
86
+ pruneExpired() {
87
+ const cutoff = Date.now() - DEDUP_TTL_MS;
88
+ for (const [hash, ts] of this.seenCommits) {
89
+ if (ts < cutoff)
90
+ this.seenCommits.delete(hash);
91
+ }
92
+ }
93
+ /** Clean up resources. */
94
+ shutdown() {
95
+ clearInterval(this.cleanupTimer);
96
+ this.seenCommits.clear();
97
+ }
98
+ }
99
+ //# sourceMappingURL=commit-event-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-event-handler.js","sourceRoot":"","sources":["../commit-event-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Git hook installation manager for commit-review event dispatching.
3
+ *
4
+ * Manages the codekin section inside each repo's post-commit hook.
5
+ * The hook script is inserted between BEGIN/END markers so it can
6
+ * coexist with other hooks and be cleanly removed.
7
+ *
8
+ * Also manages ~/.codekin/hook-config.json which provides the server
9
+ * URL and auth token to the shell hook script.
10
+ */
11
+ export interface HookConfig {
12
+ serverUrl: string;
13
+ authToken: string;
14
+ }
15
+ /**
16
+ * Write ~/.codekin/hook-config.json with the server URL and auth token.
17
+ * File permissions are set to 0600 (owner read/write only) since it contains a secret.
18
+ */
19
+ export declare function ensureHookConfig(authToken: string, serverUrl: string): void;
20
+ /**
21
+ * Install the codekin post-commit hook section into a repo.
22
+ * If the hook file already exists, the codekin section is appended (or replaced).
23
+ * If not, a new file is created with a shebang + the section.
24
+ */
25
+ export declare function installCommitHook(repoPath: string): boolean;
26
+ /**
27
+ * Remove the codekin section from a repo's post-commit hook.
28
+ * If the file only contains the codekin section (+ shebang), removes the file entirely.
29
+ */
30
+ export declare function uninstallCommitHook(repoPath: string): boolean;
31
+ /**
32
+ * Sync commit hooks with the current workflow config.
33
+ * Installs hooks for enabled commit-review repos, uninstalls for disabled/removed ones.
34
+ */
35
+ export declare function syncCommitHooks(): void;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Git hook installation manager for commit-review event dispatching.
3
+ *
4
+ * Manages the codekin section inside each repo's post-commit hook.
5
+ * The hook script is inserted between BEGIN/END markers so it can
6
+ * coexist with other hooks and be cleanly removed.
7
+ *
8
+ * Also manages ~/.codekin/hook-config.json which provides the server
9
+ * URL and auth token to the shell hook script.
10
+ */
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
12
+ import { execFileSync } from 'child_process';
13
+ import { homedir } from 'os';
14
+ import { dirname, join } from 'path';
15
+ import { loadWorkflowConfig } from './workflow-config.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Constants
18
+ // ---------------------------------------------------------------------------
19
+ const BEGIN_MARKER = '# BEGIN CODEKIN COMMIT HOOK';
20
+ const END_MARKER = '# END CODEKIN COMMIT HOOK';
21
+ const HOOK_CONFIG_PATH = join(homedir(), '.codekin', 'hook-config.json');
22
+ // Resolve the hook script path relative to this file.
23
+ // In compiled mode (dist/), the shell script is at ../server/commit-event-hook.sh
24
+ // In source mode, it's a sibling file.
25
+ import { fileURLToPath } from 'url';
26
+ const __ownDir = dirname(fileURLToPath(import.meta.url));
27
+ const HOOK_SCRIPT_SOURCE = existsSync(join(__ownDir, 'commit-event-hook.sh'))
28
+ ? join(__ownDir, 'commit-event-hook.sh')
29
+ : join(__ownDir, '..', 'server', 'commit-event-hook.sh');
30
+ /**
31
+ * Write ~/.codekin/hook-config.json with the server URL and auth token.
32
+ * File permissions are set to 0600 (owner read/write only) since it contains a secret.
33
+ */
34
+ export function ensureHookConfig(authToken, serverUrl) {
35
+ const dir = dirname(HOOK_CONFIG_PATH);
36
+ if (!existsSync(dir))
37
+ mkdirSync(dir, { recursive: true });
38
+ const config = { serverUrl, authToken };
39
+ writeFileSync(HOOK_CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
40
+ console.log(`[commit-hooks] Hook config written to ${HOOK_CONFIG_PATH}`);
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // Hook installation
44
+ // ---------------------------------------------------------------------------
45
+ /**
46
+ * Resolve the hooks directory for a given repo.
47
+ * Respects `core.hooksPath` if configured, otherwise uses `.git/hooks/`.
48
+ */
49
+ function getHooksDir(repoPath) {
50
+ try {
51
+ const customPath = execFileSync('git', ['config', '--get', 'core.hooksPath'], { cwd: repoPath, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
52
+ if (customPath)
53
+ return customPath;
54
+ }
55
+ catch {
56
+ // Not set — use default
57
+ }
58
+ return join(repoPath, '.git', 'hooks');
59
+ }
60
+ /**
61
+ * Generate the codekin hook section that will be inserted into post-commit.
62
+ * Sources the shared hook script so updates to the script are picked up
63
+ * without reinstalling hooks.
64
+ */
65
+ function generateHookSection() {
66
+ return [
67
+ BEGIN_MARKER,
68
+ `# Installed by Codekin — do not edit this section manually`,
69
+ `if [ -f "${HOOK_SCRIPT_SOURCE}" ]; then`,
70
+ ` . "${HOOK_SCRIPT_SOURCE}"`,
71
+ `fi`,
72
+ END_MARKER,
73
+ ].join('\n');
74
+ }
75
+ /**
76
+ * Install the codekin post-commit hook section into a repo.
77
+ * If the hook file already exists, the codekin section is appended (or replaced).
78
+ * If not, a new file is created with a shebang + the section.
79
+ */
80
+ export function installCommitHook(repoPath) {
81
+ const hooksDir = getHooksDir(repoPath);
82
+ const hookPath = join(hooksDir, 'post-commit');
83
+ if (!existsSync(hooksDir)) {
84
+ mkdirSync(hooksDir, { recursive: true });
85
+ }
86
+ const section = generateHookSection();
87
+ let content;
88
+ if (existsSync(hookPath)) {
89
+ const existing = readFileSync(hookPath, 'utf-8');
90
+ // Already installed — replace existing section
91
+ if (existing.includes(BEGIN_MARKER)) {
92
+ const re = new RegExp(`${escapeRegex(BEGIN_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}`);
93
+ content = existing.replace(re, section);
94
+ }
95
+ else {
96
+ // Append to existing hook
97
+ content = existing.trimEnd() + '\n\n' + section + '\n';
98
+ }
99
+ }
100
+ else {
101
+ // New file
102
+ content = `#!/bin/sh\n\n${section}\n`;
103
+ }
104
+ writeFileSync(hookPath, content, 'utf-8');
105
+ chmodSync(hookPath, 0o755);
106
+ console.log(`[commit-hooks] Installed post-commit hook at ${hookPath}`);
107
+ return true;
108
+ }
109
+ /**
110
+ * Remove the codekin section from a repo's post-commit hook.
111
+ * If the file only contains the codekin section (+ shebang), removes the file entirely.
112
+ */
113
+ export function uninstallCommitHook(repoPath) {
114
+ const hooksDir = getHooksDir(repoPath);
115
+ const hookPath = join(hooksDir, 'post-commit');
116
+ if (!existsSync(hookPath))
117
+ return false;
118
+ const existing = readFileSync(hookPath, 'utf-8');
119
+ if (!existing.includes(BEGIN_MARKER))
120
+ return false;
121
+ const re = new RegExp(`\\n?${escapeRegex(BEGIN_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`);
122
+ const cleaned = existing.replace(re, '\n').trim();
123
+ // If only the shebang remains, the hook is effectively empty
124
+ if (!cleaned || cleaned === '#!/bin/sh' || cleaned === '#!/bin/bash') {
125
+ // Remove the now-empty hook file
126
+ unlinkSync(hookPath);
127
+ console.log(`[commit-hooks] Removed empty post-commit hook at ${hookPath}`);
128
+ }
129
+ else {
130
+ writeFileSync(hookPath, cleaned + '\n', 'utf-8');
131
+ console.log(`[commit-hooks] Removed codekin section from ${hookPath}`);
132
+ }
133
+ return true;
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ // Sync: install/uninstall hooks based on current config
137
+ // ---------------------------------------------------------------------------
138
+ /**
139
+ * Sync commit hooks with the current workflow config.
140
+ * Installs hooks for enabled commit-review repos, uninstalls for disabled/removed ones.
141
+ */
142
+ export function syncCommitHooks() {
143
+ const config = loadWorkflowConfig();
144
+ const commitReviewRepos = new Set();
145
+ // Install hooks for enabled commit-review repos
146
+ for (const repo of config.reviewRepos) {
147
+ if (repo.kind === 'commit-review' && repo.enabled) {
148
+ commitReviewRepos.add(repo.repoPath);
149
+ try {
150
+ if (existsSync(join(repo.repoPath, '.git'))) {
151
+ installCommitHook(repo.repoPath);
152
+ }
153
+ }
154
+ catch (err) {
155
+ console.warn(`[commit-hooks] Failed to install hook for ${repo.repoPath}:`, err);
156
+ }
157
+ }
158
+ }
159
+ // Uninstall hooks for repos that are no longer configured for commit-review
160
+ for (const repo of config.reviewRepos) {
161
+ if (!commitReviewRepos.has(repo.repoPath)) {
162
+ try {
163
+ uninstallCommitHook(repo.repoPath);
164
+ }
165
+ catch {
166
+ // Silently ignore — hook may not exist
167
+ }
168
+ }
169
+ }
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // Helpers
173
+ // ---------------------------------------------------------------------------
174
+ function escapeRegex(str) {
175
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
176
+ }
177
+ //# sourceMappingURL=commit-event-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-event-hooks.js","sourceRoot":"","sources":["../commit-event-hooks.ts"],"names":[],"mappings":"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"}
@@ -9,11 +9,16 @@ import crypto from 'crypto';
9
9
  * URL-embedded passwords, and common API key formats.
10
10
  */
11
11
  const SECRET_PATTERNS = [
12
- [/Bearer\s+\S+/gi, 'Bearer [REDACTED]'],
13
- [/Authorization:\s*\S+/gi, 'Authorization: [REDACTED]'],
14
- [/(https?:\/\/[^:]+):([^@]+)@/gi, '$1:[REDACTED]@'],
15
- [/\b(sk-|pk-|api[_-]?key[=:]\s*)\S+/gi, '$1[REDACTED]'],
16
- [/(password|passwd|pwd|secret|token)[=:]\s*\S+/gi, '$1=[REDACTED]'],
12
+ // Bearer tokens — match across whitespace/special chars until end-of-line or quote
13
+ [/Bearer\s+[^\s"']+/gi, 'Bearer [REDACTED]'],
14
+ // Authorization header values (Basic, Bearer, Token, etc.)
15
+ [/Authorization:\s*[^\s"'\r\n]+/gi, 'Authorization: [REDACTED]'],
16
+ // URL-embedded credentials — handle percent-encoded and special chars in password
17
+ [/(https?:\/\/[^:@\s]+):([^@\s]+)@/gi, '$1:[REDACTED]@'],
18
+ // Common API key prefixes (Stripe sk_live_, GitHub ghp_/gho_/ghs_, etc.)
19
+ [/\b(sk-|pk-|sk_live_|sk_test_|ghp_|gho_|ghs_|glpat-|xox[bpsa]-|api[_-]?key[=:]\s*)\S+/gi, '$1[REDACTED]'],
20
+ // Key-value secrets in configs/logs
21
+ [/(password|passwd|pwd|secret|token|credential|auth_token|access_key|private_key)[=:]\s*\S+/gi, '$1=[REDACTED]'],
17
22
  ];
18
23
  export function redactSecrets(input) {
19
24
  let result = input;
@@ -1 +1 @@
1
- {"version":3,"file":"crypto-utils.js","sourceRoot":"","sources":["../crypto-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAA;AAE3B;;;;GAIG;AACH,MAAM,eAAe,GAA4B;IAC/C,CAAC,gBAAgB,EAAE,mBAAmB,CAAC;IACvC,CAAC,wBAAwB,EAAE,2BAA2B,CAAC;IACvD,CAAC,+BAA+B,EAAE,gBAAgB,CAAC;IACnD,CAAC,qCAAqC,EAAE,cAAc,CAAC;IACvD,CAAC,gDAAgD,EAAE,eAAe,CAAC;CACpE,CAAA;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,KAAK,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,eAAe,EAAE,CAAC;QACrD,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAC/C,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,SAAiB,EAAE,MAAc;IACpF,MAAM,QAAQ,GAAG,SAAS,GAAG,MAAM;SAChC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC;SACf,MAAM,CAAC,KAAK,CAAC,CAAA;IAEhB,IAAI,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACtD,OAAO,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,SAAiB;IACvE,OAAO,MAAM;SACV,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC;SACjC,MAAM,CAAC,WAAW,SAAS,EAAE,CAAC;SAC9B,MAAM,CAAC,KAAK,CAAC,CAAA;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,SAAiB,EAAE,cAAsB;IAC/F,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAC3D,IAAI,cAAc,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC3D,OAAO,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;AACnF,CAAC"}
1
+ {"version":3,"file":"crypto-utils.js","sourceRoot":"","sources":["../crypto-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAA;AAE3B;;;;GAIG;AACH,MAAM,eAAe,GAA4B;IAC/C,mFAAmF;IACnF,CAAC,qBAAqB,EAAE,mBAAmB,CAAC;IAC5C,2DAA2D;IAC3D,CAAC,iCAAiC,EAAE,2BAA2B,CAAC;IAChE,kFAAkF;IAClF,CAAC,oCAAoC,EAAE,gBAAgB,CAAC;IACxD,yEAAyE;IACzE,CAAC,wFAAwF,EAAE,cAAc,CAAC;IAC1G,oCAAoC;IACpC,CAAC,6FAA6F,EAAE,eAAe,CAAC;CACjH,CAAA;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,KAAK,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,eAAe,EAAE,CAAC;QACrD,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAC/C,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,SAAiB,EAAE,MAAc;IACpF,MAAM,QAAQ,GAAG,SAAS,GAAG,MAAM;SAChC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC;SACf,MAAM,CAAC,KAAK,CAAC,CAAA;IAEhB,IAAI,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACtD,OAAO,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,SAAiB;IACvE,OAAO,MAAM;SACV,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC;SACjC,MAAM,CAAC,WAAW,SAAS,EAAE,CAAC;SAC9B,MAAM,CAAC,KAAK,CAAC,CAAA;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,SAAiB,EAAE,cAAsB;IAC/F,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAC3D,IAAI,cAAc,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC3D,OAAO,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;AACnF,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Parses raw `git diff` output into structured DiffFile[] objects.
3
+ *
4
+ * Handles unified diff format with rename detection, binary files,
5
+ * and optional size-based truncation.
6
+ */
7
+ import type { DiffFile } from './types.js';
8
+ export interface ParseDiffResult {
9
+ files: DiffFile[];
10
+ truncated: boolean;
11
+ truncationReason?: string;
12
+ }
13
+ /**
14
+ * Parse raw unified diff output into structured file/hunk/line objects.
15
+ * If `maxBytes` is provided and the input exceeds it, parsing stops early
16
+ * and `truncated` is set to true.
17
+ */
18
+ export declare function parseDiff(raw: string, maxBytes?: number): ParseDiffResult;
19
+ /**
20
+ * Generate a synthetic diff for an untracked file, treating it as fully added.
21
+ * Reads the file content and produces a single hunk with all lines as additions.
22
+ */
23
+ export declare function createUntrackedFileDiff(relativePath: string, content: string): DiffFile;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Parses raw `git diff` output into structured DiffFile[] objects.
3
+ *
4
+ * Handles unified diff format with rename detection, binary files,
5
+ * and optional size-based truncation.
6
+ */
7
+ /** 2 MB default cap on raw diff output before truncation. */
8
+ const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
9
+ /**
10
+ * Parse raw unified diff output into structured file/hunk/line objects.
11
+ * If `maxBytes` is provided and the input exceeds it, parsing stops early
12
+ * and `truncated` is set to true.
13
+ */
14
+ export function parseDiff(raw, maxBytes = DEFAULT_MAX_BYTES) {
15
+ let truncated = false;
16
+ let truncationReason;
17
+ let input = raw;
18
+ if (Buffer.byteLength(raw, 'utf-8') > maxBytes) {
19
+ truncated = true;
20
+ truncationReason = `Diff output exceeded ${Math.round(maxBytes / (1024 * 1024))} MB limit`;
21
+ // Truncate to roughly maxBytes — cut at last newline before limit
22
+ const buf = Buffer.from(raw, 'utf-8');
23
+ const sliced = buf.subarray(0, maxBytes).toString('utf-8');
24
+ const lastNewline = sliced.lastIndexOf('\n');
25
+ input = lastNewline > 0 ? sliced.slice(0, lastNewline) : sliced;
26
+ }
27
+ const files = [];
28
+ // Split on diff headers: "diff --git a/... b/..."
29
+ const fileSections = input.split(/^diff --git /m);
30
+ for (let i = 1; i < fileSections.length; i++) {
31
+ const section = fileSections[i];
32
+ const file = parseFileSection(section);
33
+ if (file)
34
+ files.push(file);
35
+ }
36
+ return { files, truncated, truncationReason };
37
+ }
38
+ function parseFileSection(section) {
39
+ const lines = section.split('\n');
40
+ if (lines.length === 0)
41
+ return null;
42
+ // First line: "a/path b/path"
43
+ const headerLine = lines[0];
44
+ const pathMatch = headerLine.match(/^a\/(.+?) b\/(.+?)$/);
45
+ if (!pathMatch)
46
+ return null;
47
+ const bPath = pathMatch[2];
48
+ let status = 'modified';
49
+ let oldPath;
50
+ let isBinary = false;
51
+ const hunks = [];
52
+ let additions = 0;
53
+ let deletions = 0;
54
+ let lineIdx = 1;
55
+ // Parse extended header lines (new file, deleted file, rename, binary, etc.)
56
+ while (lineIdx < lines.length) {
57
+ const line = lines[lineIdx];
58
+ if (line.startsWith('new file mode')) {
59
+ status = 'added';
60
+ }
61
+ else if (line.startsWith('deleted file mode')) {
62
+ status = 'deleted';
63
+ }
64
+ else if (line.startsWith('rename from ')) {
65
+ status = 'renamed';
66
+ oldPath = line.slice('rename from '.length);
67
+ }
68
+ else if (line.startsWith('similarity index') || line.startsWith('dissimilarity index')) {
69
+ // part of rename/copy header, skip
70
+ }
71
+ else if (line.startsWith('rename to ')) {
72
+ // already captured via bPath
73
+ }
74
+ else if (line.startsWith('index ')) {
75
+ // index line, skip
76
+ }
77
+ else if (line === 'GIT binary patch' || line.startsWith('Binary files ')) {
78
+ isBinary = true;
79
+ }
80
+ else if (line.startsWith('--- ') || line.startsWith('+++ ')) {
81
+ // file marker lines — skip but keep going for hunks
82
+ }
83
+ else if (line.startsWith('@@')) {
84
+ // Start of hunks — break out to hunk parsing
85
+ break;
86
+ }
87
+ else if (line.startsWith('old mode') || line.startsWith('new mode')) {
88
+ // mode change, skip
89
+ }
90
+ else {
91
+ // Unknown header line or empty — if we hit content, stop
92
+ if (!line.startsWith('\\') && line !== '') {
93
+ break;
94
+ }
95
+ }
96
+ lineIdx++;
97
+ }
98
+ // Parse hunks
99
+ if (!isBinary) {
100
+ while (lineIdx < lines.length) {
101
+ const line = lines[lineIdx];
102
+ if (line.startsWith('@@')) {
103
+ const hunkResult = parseHunk(lines, lineIdx);
104
+ if (hunkResult) {
105
+ hunks.push(hunkResult.hunk);
106
+ additions += hunkResult.additions;
107
+ deletions += hunkResult.deletions;
108
+ lineIdx = hunkResult.nextIdx;
109
+ }
110
+ else {
111
+ lineIdx++;
112
+ }
113
+ }
114
+ else {
115
+ lineIdx++;
116
+ }
117
+ }
118
+ }
119
+ return {
120
+ path: bPath,
121
+ status,
122
+ oldPath: status === 'renamed' ? oldPath : undefined,
123
+ isBinary,
124
+ additions,
125
+ deletions,
126
+ hunks,
127
+ };
128
+ }
129
+ function parseHunk(lines, startIdx) {
130
+ const headerLine = lines[startIdx];
131
+ // Parse "@@ -10,7 +10,8 @@ optional context"
132
+ const hunkMatch = headerLine.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
133
+ if (!hunkMatch)
134
+ return null;
135
+ const oldStart = parseInt(hunkMatch[1], 10);
136
+ const oldLines = hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1;
137
+ const newStart = parseInt(hunkMatch[3], 10);
138
+ const newLines = hunkMatch[4] !== undefined ? parseInt(hunkMatch[4], 10) : 1;
139
+ const diffLines = [];
140
+ let additions = 0;
141
+ let deletions = 0;
142
+ let oldLineNo = oldStart;
143
+ let newLineNo = newStart;
144
+ let idx = startIdx + 1;
145
+ while (idx < lines.length) {
146
+ const line = lines[idx];
147
+ // Next hunk or next file
148
+ if (line.startsWith('@@') || line.startsWith('diff --git '))
149
+ break;
150
+ if (line.startsWith('+')) {
151
+ diffLines.push({
152
+ type: 'add',
153
+ content: line.slice(1),
154
+ newLineNo: newLineNo,
155
+ });
156
+ additions++;
157
+ newLineNo++;
158
+ }
159
+ else if (line.startsWith('-')) {
160
+ diffLines.push({
161
+ type: 'delete',
162
+ content: line.slice(1),
163
+ oldLineNo: oldLineNo,
164
+ });
165
+ deletions++;
166
+ oldLineNo++;
167
+ }
168
+ else if (line.startsWith(' ')) {
169
+ diffLines.push({
170
+ type: 'context',
171
+ content: line.slice(1),
172
+ oldLineNo: oldLineNo,
173
+ newLineNo: newLineNo,
174
+ });
175
+ oldLineNo++;
176
+ newLineNo++;
177
+ }
178
+ else if (line.startsWith('\\')) {
179
+ // "" — skip
180
+ }
181
+ else if (line === '') {
182
+ // Unified diff always prefixes lines with ' ', '+', or '-'.
183
+ // An empty string means end-of-section (from split at file boundaries).
184
+ break;
185
+ }
186
+ else {
187
+ break;
188
+ }
189
+ idx++;
190
+ }
191
+ return {
192
+ hunk: {
193
+ header: headerLine,
194
+ oldStart,
195
+ oldLines,
196
+ newStart,
197
+ newLines,
198
+ lines: diffLines,
199
+ },
200
+ additions,
201
+ deletions,
202
+ nextIdx: idx,
203
+ };
204
+ }
205
+ /**
206
+ * Generate a synthetic diff for an untracked file, treating it as fully added.
207
+ * Reads the file content and produces a single hunk with all lines as additions.
208
+ */
209
+ export function createUntrackedFileDiff(relativePath, content) {
210
+ const fileLines = content.split('\n');
211
+ // Remove trailing empty line from split if file ends with newline
212
+ if (fileLines.length > 0 && fileLines[fileLines.length - 1] === '') {
213
+ fileLines.pop();
214
+ }
215
+ const diffLines = fileLines.map((line, i) => ({
216
+ type: 'add',
217
+ content: line,
218
+ newLineNo: i + 1,
219
+ }));
220
+ return {
221
+ path: relativePath,
222
+ status: 'added',
223
+ isBinary: false,
224
+ additions: fileLines.length,
225
+ deletions: 0,
226
+ hunks: fileLines.length > 0 ? [{
227
+ header: `@@ -0,0 +1,${fileLines.length} @@`,
228
+ oldStart: 0,
229
+ oldLines: 0,
230
+ newStart: 1,
231
+ newLines: fileLines.length,
232
+ lines: diffLines,
233
+ }] : [],
234
+ };
235
+ }
236
+ //# sourceMappingURL=diff-parser.js.map