git-trace 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.
Files changed (48) hide show
  1. package/.tracerc.example +38 -0
  2. package/README.md +136 -0
  3. package/bun.lock +511 -0
  4. package/bunchee.config.ts +11 -0
  5. package/cli/index.ts +251 -0
  6. package/cli/parser.ts +76 -0
  7. package/cli/tsconfig.json +6 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +858 -0
  10. package/dist/config.cjs +66 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +63 -0
  13. package/dist/highlight/index.cjs +770 -0
  14. package/dist/highlight/index.d.ts +26 -0
  15. package/dist/highlight/index.js +766 -0
  16. package/dist/index.cjs +849 -0
  17. package/dist/index.d.ts +52 -0
  18. package/dist/index.js +845 -0
  19. package/examples/demo/App.tsx +78 -0
  20. package/examples/demo/index.html +12 -0
  21. package/examples/demo/main.tsx +10 -0
  22. package/examples/demo/mockData.ts +170 -0
  23. package/examples/demo/styles.css +103 -0
  24. package/examples/demo/tsconfig.json +21 -0
  25. package/examples/demo/tsconfig.node.json +10 -0
  26. package/examples/demo/vite.config.ts +20 -0
  27. package/package.json +58 -0
  28. package/src/Trace.tsx +717 -0
  29. package/src/cache.ts +118 -0
  30. package/src/config.ts +51 -0
  31. package/src/entries/config.ts +7 -0
  32. package/src/entries/gitea.ts +4 -0
  33. package/src/entries/github.ts +5 -0
  34. package/src/entries/gitlab.ts +4 -0
  35. package/src/gitea.ts +58 -0
  36. package/src/github.ts +100 -0
  37. package/src/gitlab.ts +65 -0
  38. package/src/highlight/highlight.ts +119 -0
  39. package/src/highlight/index.ts +4 -0
  40. package/src/host.ts +32 -0
  41. package/src/index.ts +6 -0
  42. package/src/patterns.ts +6 -0
  43. package/src/shared.ts +108 -0
  44. package/src/themes.ts +98 -0
  45. package/src/types.ts +72 -0
  46. package/test/e2e.html +424 -0
  47. package/tsconfig.json +18 -0
  48. package/vercel.json +4 -0
package/src/cache.ts ADDED
@@ -0,0 +1,118 @@
1
+ // Local cache for GitHub API responses to avoid rate limits
2
+
3
+ import type { Commit } from './types'
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'fs'
5
+ import { join } from 'path'
6
+ import { homedir } from 'os'
7
+
8
+ const CACHE_DIR = join(homedir(), '.trace-cache')
9
+ const CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours
10
+
11
+ export interface CacheEntry {
12
+ data: Commit[]
13
+ timestamp: number
14
+ repo: string
15
+ file: string
16
+ last: number
17
+ }
18
+
19
+ function sanitizePath(path: string): string {
20
+ // Remove ../, ./, and replace slashes with dashes
21
+ return path
22
+ .replace(/\.\.\//g, '')
23
+ .replace(/\.\//g, '')
24
+ .replace(/[\/\\]/g, '-')
25
+ .replace(/[^a-zA-Z0-9._-]/g, '_')
26
+ }
27
+
28
+ function getCacheKey(repo: string, file: string, last: number): string {
29
+ const key = `${sanitizePath(repo)}-${sanitizePath(file)}-${last}.json`
30
+ return join(CACHE_DIR, key)
31
+ }
32
+
33
+ export function getCached(repo: string, file: string, last: number): Commit[] | null {
34
+ const cachePath = getCacheKey(repo, file, last)
35
+
36
+ if (!existsSync(cachePath)) {
37
+ return null
38
+ }
39
+
40
+ try {
41
+ const content = readFileSync(cachePath, 'utf-8')
42
+ const entry: CacheEntry = JSON.parse(content)
43
+
44
+ // Check if cache is expired
45
+ const now = Date.now()
46
+ if (now - entry.timestamp > CACHE_TTL) {
47
+ return null
48
+ }
49
+
50
+ return entry.data
51
+ } catch {
52
+ return null
53
+ }
54
+ }
55
+
56
+ export function setCached(repo: string, file: string, last: number, data: Commit[]): void {
57
+ const cachePath = getCacheKey(repo, file, last)
58
+
59
+ try {
60
+ // Ensure cache directory exists
61
+ if (!existsSync(CACHE_DIR)) {
62
+ mkdirSync(CACHE_DIR, { recursive: true })
63
+ }
64
+
65
+ const entry: CacheEntry = {
66
+ data,
67
+ timestamp: Date.now(),
68
+ repo,
69
+ file,
70
+ last
71
+ }
72
+
73
+ writeFileSync(cachePath, JSON.stringify(entry, null, 2))
74
+ } catch {
75
+ // Silently fail - caching is optional
76
+ }
77
+ }
78
+
79
+ export function clearCache(): void {
80
+ try {
81
+ rmSync(CACHE_DIR, { recursive: true, force: true })
82
+ } catch {
83
+ // Ignore
84
+ }
85
+ }
86
+
87
+ export function getCacheSize(): number {
88
+ try {
89
+ if (!existsSync(CACHE_DIR)) return 0
90
+ const files = readdirSync(CACHE_DIR)
91
+ return files.length
92
+ } catch {
93
+ return 0
94
+ }
95
+ }
96
+
97
+ export function getCacheInfo(): CacheEntry[] {
98
+ try {
99
+ if (!existsSync(CACHE_DIR)) return []
100
+
101
+ const files = readdirSync(CACHE_DIR)
102
+ const entries: CacheEntry[] = []
103
+
104
+ for (const file of files) {
105
+ try {
106
+ const content = readFileSync(join(CACHE_DIR, file), 'utf-8')
107
+ const entry: CacheEntry = JSON.parse(content)
108
+ entries.push(entry)
109
+ } catch {
110
+ // Skip invalid files
111
+ }
112
+ }
113
+
114
+ return entries
115
+ } catch {
116
+ return []
117
+ }
118
+ }
package/src/config.ts ADDED
@@ -0,0 +1,51 @@
1
+ // Config file support for .tracerc
2
+
3
+ import { existsSync, readFileSync } from 'fs'
4
+ import { join } from 'path'
5
+ import { homedir } from 'os'
6
+ import { DEFAULT_PATTERNS } from './patterns'
7
+
8
+ export interface TraceConfig {
9
+ aiPatterns?: {
10
+ emails?: string[]
11
+ messages?: string[]
12
+ }
13
+ last?: number
14
+ }
15
+
16
+ const DEFAULT: TraceConfig = {
17
+ aiPatterns: DEFAULT_PATTERNS,
18
+ last: 10
19
+ }
20
+
21
+ let cached: TraceConfig | null = null
22
+
23
+ export function loadConfig(cwd?: string): TraceConfig {
24
+ if (cached) return cached
25
+
26
+ const paths = cwd ? [join(cwd, '.tracerc'), join(homedir(), '.tracerc')] : [join(homedir(), '.tracerc')]
27
+
28
+ for (const path of paths) {
29
+ if (existsSync(path)) {
30
+ try {
31
+ const user = JSON.parse(readFileSync(path, 'utf-8'))
32
+ const merged: TraceConfig = { ...DEFAULT, ...user }
33
+ cached = merged
34
+ return cached
35
+ } catch {
36
+ // Invalid config, use defaults
37
+ }
38
+ }
39
+ }
40
+
41
+ cached = DEFAULT
42
+ return cached
43
+ }
44
+
45
+ export function getAIPatterns(config?: TraceConfig) {
46
+ const patterns = (config || loadConfig()).aiPatterns
47
+ return {
48
+ emails: patterns?.emails || DEFAULT.aiPatterns!.emails!,
49
+ messages: patterns?.messages || DEFAULT.aiPatterns!.messages!
50
+ }
51
+ }
@@ -0,0 +1,7 @@
1
+ // Config utilities — node only
2
+
3
+ export {
4
+ loadConfig,
5
+ getAIPatterns
6
+ } from '../config'
7
+ export type { TraceConfig } from '../config'
@@ -0,0 +1,4 @@
1
+ // Gitea adapter entry point
2
+
3
+ export { fetchCommits } from '../gitea'
4
+ export type { Commit } from '../types'
@@ -0,0 +1,5 @@
1
+ // GitHub adapter entry point
2
+
3
+ export { fetchCommits } from '../github'
4
+ export { clearCache, getCacheSize } from '../cache'
5
+ export type { Commit } from '../types'
@@ -0,0 +1,4 @@
1
+ // GitLab adapter entry point
2
+
3
+ export { fetchCommits } from '../gitlab'
4
+ export type { Commit } from '../types'
package/src/gitea.ts ADDED
@@ -0,0 +1,58 @@
1
+ // Gitea API adapter — fetches commits with diffs from repositories
2
+
3
+ import type { Commit } from './types'
4
+ import { parseDiff, detectAI, formatRelativeTime, shortHash, firstLine } from './shared'
5
+
6
+ const GITEA_API = 'https://gitea.com/api/v1'
7
+
8
+ export async function fetchCommits(
9
+ owner: string,
10
+ repo: string,
11
+ file: string,
12
+ last: number = 10,
13
+ token?: string,
14
+ baseUrl?: string
15
+ ): Promise<Commit[]> {
16
+ const api = baseUrl || GITEA_API
17
+
18
+ const headers: Record<string, string> = { 'Accept': 'application/json' }
19
+ if (token) headers['Authorization'] = `token ${token}`
20
+
21
+ // Fetch commits list
22
+ const listUrl = `${api}/repos/${owner}/${repo}/commits?path=${encodeURIComponent(file)}&limit=${last}`
23
+ const listRes = await fetch(listUrl, { headers })
24
+
25
+ if (!listRes.ok) {
26
+ if (listRes.status === 401) throw new Error('Gitea authentication failed')
27
+ if (listRes.status === 404) throw new Error(`Not found: ${owner}/${repo}/${file}`)
28
+ throw new Error(`Gitea API error: ${listRes.status}`)
29
+ }
30
+
31
+ const commitsList = await listRes.json()
32
+
33
+ // Fetch diffs in parallel
34
+ const results = await Promise.allSettled(
35
+ commitsList.map(async (c: any) => {
36
+ const diffUrl = `${api}/repos/${owner}/${repo}/git/commits/${c.sha}/diff`
37
+ const diffRes = await fetch(diffUrl, { headers })
38
+ if (!diffRes.ok) return null
39
+ const diffText = await diffRes.text()
40
+ return parseCommit(c, diffText)
41
+ })
42
+ )
43
+
44
+ return results
45
+ .map(r => r.status === 'fulfilled' ? r.value : null)
46
+ .filter((c): c is Commit => c !== null)
47
+ }
48
+
49
+ function parseCommit(data: any, diffText: string): Commit {
50
+ return {
51
+ hash: shortHash(data.sha),
52
+ message: firstLine(data.commit.message),
53
+ author: data.author?.login || data.commit.author?.name || 'Unknown',
54
+ authorType: detectAI(data.author?.login, data.commit.author?.email, data.commit.message),
55
+ time: formatRelativeTime(data.commit.author.date),
56
+ lines: parseDiff(diffText)
57
+ }
58
+ }
package/src/github.ts ADDED
@@ -0,0 +1,100 @@
1
+ // GitHub API adapter — fetches commits with diffs from repositories
2
+
3
+ import type { Commit, GitHubCommit } from './types'
4
+ import type { TraceConfig } from './config'
5
+ import { getCached, setCached } from './cache'
6
+ import { loadConfig, getAIPatterns } from './config'
7
+ import { parseDiff, detectAI, formatRelativeTime, shortHash, firstLine } from './shared'
8
+
9
+ const GITHUB_API = 'https://api.github.com'
10
+
11
+ export async function fetchCommits(
12
+ owner: string,
13
+ repo: string,
14
+ file: string,
15
+ last: number = 10,
16
+ token?: string,
17
+ useCache: boolean = true,
18
+ onCacheHit?: () => void,
19
+ onCacheWrite?: () => void
20
+ ): Promise<Commit[]> {
21
+ const repoKey = `${owner}/${repo}`
22
+ const config = loadConfig()
23
+
24
+ // Check cache
25
+ if (useCache) {
26
+ const cached = getCached(repoKey, file, last)
27
+ if (cached) {
28
+ onCacheHit?.()
29
+ return cached as Commit[]
30
+ }
31
+ }
32
+
33
+ const headers: Record<string, string> = { 'Accept': 'application/vnd.github.v3+json' }
34
+ if (token) headers['Authorization'] = `Bearer ${token}`
35
+
36
+ // Fetch commits list
37
+ const listUrl = `${GITHUB_API}/repos/${owner}/${repo}/commits?path=${encodeURIComponent(file)}&per_page=${last}`
38
+ const listRes = await fetch(listUrl, { headers })
39
+
40
+ if (!listRes.ok) {
41
+ if (listRes.status === 403) {
42
+ const reset = listRes.headers.get('X-RateLimit-Reset')
43
+ const resetTime = reset ? new Date(parseInt(reset) * 1000).toLocaleTimeString() : 'unknown'
44
+ throw new Error(`Rate limit exceeded. Resets at ${resetTime}`)
45
+ }
46
+ if (listRes.status === 404) throw new Error(`Not found: ${owner}/${repo}/${file}`)
47
+ throw new Error(`GitHub API error: ${listRes.status}`)
48
+ }
49
+
50
+ const commitsList = await listRes.json()
51
+
52
+ // Fetch diffs in parallel
53
+ const results = await Promise.allSettled(
54
+ commitsList.map(async (c: { sha: string }) => {
55
+ const commitUrl = `${GITHUB_API}/repos/${owner}/${repo}/commits/${c.sha}`
56
+ const commitRes = await fetch(commitUrl, { headers })
57
+ if (!commitRes.ok) return null
58
+ const detail = await commitRes.json() as GitHubCommit
59
+ return parseCommit(detail, config)
60
+ })
61
+ )
62
+
63
+ const commits = results
64
+ .map(r => r.status === 'fulfilled' ? r.value : null)
65
+ .filter((c): c is Commit => c !== null)
66
+
67
+ // Cache results
68
+ if (useCache && commits.length > 0) {
69
+ setCached(repoKey, file, last, commits)
70
+ onCacheWrite?.()
71
+ }
72
+
73
+ return commits
74
+ }
75
+
76
+ export { clearCache, getCacheSize } from './cache'
77
+
78
+ function parseCommit(data: GitHubCommit, config?: TraceConfig): Commit {
79
+ const lines: Commit['lines'] = []
80
+ const aiPatterns = getAIPatterns(config)
81
+
82
+ for (const file of data.files || []) {
83
+ if (!file.patch) continue
84
+ lines.push(...parseDiff(file.patch))
85
+ }
86
+
87
+ return {
88
+ hash: shortHash(data.sha),
89
+ message: firstLine(data.commit.message),
90
+ author: data.author?.login || data.commit.author?.name || 'Unknown',
91
+ authorType: detectAI(
92
+ data.author?.login,
93
+ data.commit.author?.email,
94
+ data.commit.message,
95
+ aiPatterns
96
+ ),
97
+ time: formatRelativeTime(data.commit.author.date),
98
+ lines
99
+ }
100
+ }
package/src/gitlab.ts ADDED
@@ -0,0 +1,65 @@
1
+ // GitLab API adapter — fetches commits with diffs from repositories
2
+
3
+ import type { Commit } from './types'
4
+ import { parseDiff, detectAI, formatRelativeTime, shortHash, firstLine } from './shared'
5
+
6
+ const GITLAB_API = 'https://gitlab.com/api/v4'
7
+
8
+ export async function fetchCommits(
9
+ owner: string,
10
+ repo: string,
11
+ file: string,
12
+ last: number = 10,
13
+ token?: string,
14
+ baseUrl?: string
15
+ ): Promise<Commit[]> {
16
+ const api = baseUrl || GITLAB_API
17
+ const projectId = encodeURIComponent(`${owner}/${repo}`)
18
+
19
+ const headers: Record<string, string> = {}
20
+ if (token) headers['PRIVATE-TOKEN'] = token
21
+
22
+ // Fetch commits list
23
+ const listUrl = `${api}/projects/${projectId}/repository/commits?path=${encodeURIComponent(file)}&per_page=${last}`
24
+ const listRes = await fetch(listUrl, { headers })
25
+
26
+ if (!listRes.ok) {
27
+ if (listRes.status === 401) throw new Error('GitLab authentication failed')
28
+ if (listRes.status === 404) throw new Error(`Not found: ${owner}/${repo}/${file}`)
29
+ throw new Error(`GitLab API error: ${listRes.status}`)
30
+ }
31
+
32
+ const commitsList = await listRes.json()
33
+
34
+ // Fetch diffs in parallel
35
+ const results = await Promise.allSettled(
36
+ commitsList.map(async (c: any) => {
37
+ const diffUrl = `${api}/projects/${projectId}/repository/commits/${c.id}/diff?per_page=100`
38
+ const diffRes = await fetch(diffUrl, { headers })
39
+ if (!diffRes.ok) return null
40
+ const diff = await diffRes.json()
41
+ return parseCommit(c, diff)
42
+ })
43
+ )
44
+
45
+ return results
46
+ .map(r => r.status === 'fulfilled' ? r.value : null)
47
+ .filter((c): c is Commit => c !== null)
48
+ }
49
+
50
+ function parseCommit(data: any, diffData: any[]): Commit {
51
+ const lines: Commit['lines'] = []
52
+ for (const file of diffData) {
53
+ if (!file.diff) continue
54
+ lines.push(...parseDiff(file.diff))
55
+ }
56
+
57
+ return {
58
+ hash: shortHash(data.id),
59
+ message: firstLine(data.title),
60
+ author: data.author_name || 'Unknown',
61
+ authorType: detectAI(data.author_name, data.author_email, data.title),
62
+ time: formatRelativeTime(data.created_at),
63
+ lines
64
+ }
65
+ }
@@ -0,0 +1,119 @@
1
+ // Syntax highlighting using sugar-high (~1KB, zero dependencies)
2
+
3
+ import { highlight } from 'sugar-high'
4
+
5
+ const MAX_CACHE_SIZE = 100
6
+
7
+ /**
8
+ * Simple LRU (Least Recently Used) cache implementation
9
+ * Evicts oldest entries when capacity is reached
10
+ */
11
+ export class LRUCache<K, V> {
12
+ private cache = new Map<K, V>()
13
+
14
+ get(key: K): V | undefined {
15
+ if (!this.cache.has(key)) return undefined
16
+ const value = this.cache.get(key)!
17
+ // Move to end (most recently used)
18
+ this.cache.delete(key)
19
+ this.cache.set(key, value)
20
+ return value
21
+ }
22
+
23
+ set(key: K, value: V): void {
24
+ // Remove oldest entry if at capacity
25
+ if (this.cache.size >= MAX_CACHE_SIZE) {
26
+ const firstKey = this.cache.keys().next().value as K | undefined
27
+ if (firstKey !== undefined) {
28
+ this.cache.delete(firstKey)
29
+ }
30
+ }
31
+ this.cache.set(key, value)
32
+ }
33
+
34
+ clear(): void {
35
+ this.cache.clear()
36
+ }
37
+ }
38
+
39
+ // LRU cache for highlighted code to avoid re-calculating
40
+ const highlightCache = new LRUCache<string, string>()
41
+
42
+ /**
43
+ * Highlight code using sugar-high
44
+ * Returns HTML string with token classes (sh__token--*)
45
+ * Results are cached for performance - same input produces same output
46
+ *
47
+ * SECURITY: sugar-high tokenizes code but does NOT escape HTML.
48
+ * Caller must escape user input BEFORE calling this function.
49
+ */
50
+ export function highlightCode(code: string): string {
51
+ if (!code) return ''
52
+
53
+ const cached = highlightCache.get(code)
54
+ if (cached) return cached
55
+
56
+ const result = highlight(code)
57
+ highlightCache.set(code, result)
58
+ return result
59
+ }
60
+
61
+ /**
62
+ * Clear the highlight cache (useful for testing or memory management)
63
+ */
64
+ export function clearHighlightCache(): void {
65
+ highlightCache.clear()
66
+ }
67
+
68
+ /**
69
+ * Get language from file extension
70
+ * Useful for language-specific handling
71
+ */
72
+ const LANGUAGE_MAP: Record<string, string> = {
73
+ 'js': 'javascript',
74
+ 'jsx': 'javascript',
75
+ 'ts': 'typescript',
76
+ 'tsx': 'typescript',
77
+ 'py': 'python',
78
+ 'rs': 'rust',
79
+ 'go': 'go',
80
+ 'java': 'java',
81
+ 'c': 'c',
82
+ 'h': 'c',
83
+ 'cpp': 'cpp',
84
+ 'cc': 'cpp',
85
+ 'cxx': 'cpp',
86
+ 'hpp': 'cpp',
87
+ 'cs': 'csharp',
88
+ 'php': 'php',
89
+ 'rb': 'ruby',
90
+ 'swift': 'swift',
91
+ 'kt': 'kotlin',
92
+ 'scala': 'scala',
93
+ 'sh': 'shell',
94
+ 'bash': 'shell',
95
+ 'zsh': 'shell',
96
+ 'fish': 'shell',
97
+ 'json': 'json',
98
+ 'yaml': 'yaml',
99
+ 'yml': 'yaml',
100
+ 'toml': 'toml',
101
+ 'xml': 'xml',
102
+ 'html': 'html',
103
+ 'htm': 'html',
104
+ 'css': 'css',
105
+ 'scss': 'scss',
106
+ 'sass': 'sass',
107
+ 'md': 'markdown',
108
+ 'mdx': 'markdown',
109
+ 'sql': 'sql',
110
+ 'graphql': 'graphql',
111
+ 'gql': 'graphql',
112
+ 'vue': 'vue',
113
+ 'svelte': 'svelte'
114
+ }
115
+
116
+ export function getLanguageFromPath(filePath: string): string {
117
+ const ext = filePath.split('.').pop()?.toLowerCase()
118
+ return LANGUAGE_MAP[ext || ''] || 'text'
119
+ }
@@ -0,0 +1,4 @@
1
+ // Syntax highlighting module — optional import for enhanced diff display
2
+
3
+ export { highlightCode, clearHighlightCache, getLanguageFromPath } from './highlight'
4
+ export type { LRUCache } from './highlight'
package/src/host.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Git host detection for CLI — auto-detects GitHub, GitLab, Gitea from repo strings
2
+
3
+ export type GitHost = 'github' | 'gitlab' | 'gitea'
4
+
5
+ export interface DetectedHost {
6
+ host: GitHost
7
+ owner: string
8
+ repo: string
9
+ }
10
+
11
+ // Detect host from repo URL or owner/repo string
12
+ export function detectHost(input: string): DetectedHost | null {
13
+ // GitHub: owner/repo
14
+ const githubMatch = input.match(/^([\w-]+)\/([\w.-]+)$/)
15
+ if (githubMatch) {
16
+ return { host: 'github', owner: githubMatch[1], repo: githubMatch[2] }
17
+ }
18
+
19
+ // GitLab URL: gitlab.com/owner/repo or gitlab.com/owner/repo.git
20
+ const gitlabMatch = input.match(/gitlab\.com\/([\w-]+)\/([\w.-]+)/)
21
+ if (gitlabMatch) {
22
+ return { host: 'gitlab', owner: gitlabMatch[1], repo: gitlabMatch[2].replace('.git', '') }
23
+ }
24
+
25
+ // Gitea URL: gitea.com/owner/repo or codeberg.org/owner/repo (runs Gitea)
26
+ const giteaMatch = input.match(/(?:gitea\.com|codeberg\.org|notabug\.org)\/([\w-]+)\/([\w.-]+)/)
27
+ if (giteaMatch) {
28
+ return { host: 'gitea', owner: giteaMatch[1], repo: giteaMatch[2].replace('.git', '') }
29
+ }
30
+
31
+ return null
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Core component — zero dependency, browser-safe
2
+ // Syntax highlighting available as optional import: import 'trace/highlight'
3
+
4
+ export { Trace } from './Trace'
5
+ export { themes, themeToVars } from './themes'
6
+ export type { Commit, DiffLine, TraceProps, Theme, ThemeColors, FilterOptions, AuthorTypeFilter } from './types'
@@ -0,0 +1,6 @@
1
+ // Default AI detection patterns — browser-safe, no Node.js dependencies
2
+
3
+ export const DEFAULT_PATTERNS = {
4
+ emails: ['noreply@cursor.sh', 'claude@anthropic.com', 'bot@github.com', 'copilot', 'cursor'],
5
+ messages: ['Co-Authored-By: Claude', 'Co-Authored-By: Cursor', 'Generated-by:', '[skip-human-review]', 'AI-generated']
6
+ }