opencode-codegraph 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/package.json +35 -0
- package/src/api.ts +265 -0
- package/src/index.ts +137 -0
- package/src/util.ts +97 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-codegraph",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin for CodeGraph CPG-powered code analysis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"build": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"opencode",
|
|
19
|
+
"codegraph",
|
|
20
|
+
"cpg",
|
|
21
|
+
"code-analysis",
|
|
22
|
+
"security"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@opencode-ai/plugin": "^1.2.22"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.8.0",
|
|
30
|
+
"@types/node": "^22.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@opencode-ai/plugin": ">=1.2.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeGraph REST API client for the OpenCode plugin.
|
|
3
|
+
*
|
|
4
|
+
* Communicates with the CodeGraph API server (default: http://localhost:8000)
|
|
5
|
+
* to retrieve CPG data, trigger updates, and run analysis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class CodeGraphAPI {
|
|
9
|
+
private baseUrl: string
|
|
10
|
+
|
|
11
|
+
constructor(baseUrl: string) {
|
|
12
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get project summary for system prompt injection.
|
|
17
|
+
* Returns a markdown block with file count, top hotspots, and open security findings.
|
|
18
|
+
*/
|
|
19
|
+
async getProjectSummary(projectId: string): Promise<string | null> {
|
|
20
|
+
const params = projectId ? `?project_id=${encodeURIComponent(projectId)}` : ""
|
|
21
|
+
|
|
22
|
+
// Fetch multiple endpoints in parallel
|
|
23
|
+
const [statsRes, findingsRes] = await Promise.allSettled([
|
|
24
|
+
this.fetch(`/api/v1/cpg/stats${params}`),
|
|
25
|
+
this.fetch(`/api/v1/cpg/pattern-results${params}&category=security&limit=10`),
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const stats =
|
|
29
|
+
statsRes.status === "fulfilled" ? statsRes.value : null
|
|
30
|
+
const findings =
|
|
31
|
+
findingsRes.status === "fulfilled" ? findingsRes.value : null
|
|
32
|
+
|
|
33
|
+
if (!stats && !findings) return null
|
|
34
|
+
|
|
35
|
+
const lines: string[] = [
|
|
36
|
+
"## CodeGraph CPG Context",
|
|
37
|
+
"",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
if (stats) {
|
|
41
|
+
lines.push(
|
|
42
|
+
`- **Files**: ${stats.total_files ?? "?"}`,
|
|
43
|
+
`- **Methods**: ${stats.total_methods ?? "?"}`,
|
|
44
|
+
`- **Security findings**: ${stats.security_findings ?? "?"}`,
|
|
45
|
+
)
|
|
46
|
+
if (stats.top_complex_methods?.length) {
|
|
47
|
+
lines.push("", "### Top complex methods")
|
|
48
|
+
for (const m of stats.top_complex_methods.slice(0, 5)) {
|
|
49
|
+
lines.push(`- \`${m.name}\` (CC=${m.cyclomatic_complexity}, fan_in=${m.fan_in})`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (findings?.results?.length) {
|
|
55
|
+
lines.push("", "### Open security findings")
|
|
56
|
+
for (const f of findings.results.slice(0, 5)) {
|
|
57
|
+
lines.push(`- **${f.rule_id}**: ${f.message} (${f.filename}:${f.line})`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lines.join("\n")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get CPG context for a specific file.
|
|
66
|
+
* Returns markdown with methods, callers, complexity, and security findings.
|
|
67
|
+
*/
|
|
68
|
+
async getFileCPGContext(projectId: string, filePath: string): Promise<string | null> {
|
|
69
|
+
const params = new URLSearchParams()
|
|
70
|
+
if (projectId) params.set("project_id", projectId)
|
|
71
|
+
params.set("filename", filePath)
|
|
72
|
+
|
|
73
|
+
const [methodsRes, findingsRes] = await Promise.allSettled([
|
|
74
|
+
this.fetch(`/api/v1/cpg/methods?${params}`),
|
|
75
|
+
this.fetch(`/api/v1/cpg/pattern-results?${params}&category=security`),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
const methods =
|
|
79
|
+
methodsRes.status === "fulfilled" ? methodsRes.value : null
|
|
80
|
+
const findings =
|
|
81
|
+
findingsRes.status === "fulfilled" ? findingsRes.value : null
|
|
82
|
+
|
|
83
|
+
if (!methods?.results?.length && !findings?.results?.length) return null
|
|
84
|
+
|
|
85
|
+
const lines: string[] = [
|
|
86
|
+
`### CPG context: \`${filePath}\``,
|
|
87
|
+
"",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
if (methods?.results?.length) {
|
|
91
|
+
lines.push(`**${methods.results.length} methods** in file:`)
|
|
92
|
+
for (const m of methods.results.slice(0, 15)) {
|
|
93
|
+
const flags: string[] = []
|
|
94
|
+
if (m.is_entry_point) flags.push("entry")
|
|
95
|
+
if (m.is_external) flags.push("external")
|
|
96
|
+
if (m.fan_in === 0 && !m.is_entry_point && !m.is_external && !m.is_test) flags.push("DEAD")
|
|
97
|
+
const flagStr = flags.length ? ` [${flags.join(", ")}]` : ""
|
|
98
|
+
lines.push(
|
|
99
|
+
`- \`${m.name}\` CC=${m.cyclomatic_complexity} fan_in=${m.fan_in} fan_out=${m.fan_out}${flagStr}`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (findings?.results?.length) {
|
|
105
|
+
lines.push("", `**${findings.results.length} security findings:**`)
|
|
106
|
+
for (const f of findings.results.slice(0, 10)) {
|
|
107
|
+
lines.push(`- **${f.rule_id}** L${f.line}: ${f.message}`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return lines.join("\n")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Trigger incremental CPG update after a git commit.
|
|
116
|
+
*/
|
|
117
|
+
async triggerIncrementalUpdate(projectId: string, directory: string): Promise<void> {
|
|
118
|
+
await this.fetch("/api/v1/webhooks/local", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
event_type: "push",
|
|
122
|
+
project_id: projectId,
|
|
123
|
+
directory,
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Review code changes using CodeGraph analysis.
|
|
130
|
+
*/
|
|
131
|
+
async reviewChanges(projectId: string, diff: string, baseRef: string): Promise<string> {
|
|
132
|
+
try {
|
|
133
|
+
const result = await this.fetch("/api/v1/review", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
project_id: projectId,
|
|
137
|
+
diff,
|
|
138
|
+
base_ref: baseRef,
|
|
139
|
+
}),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (!result) return "CodeGraph review: no results."
|
|
143
|
+
|
|
144
|
+
const lines: string[] = ["## CodeGraph Review Results", ""]
|
|
145
|
+
|
|
146
|
+
if (result.security_findings?.length) {
|
|
147
|
+
lines.push(`### Security (${result.security_findings.length} findings)`)
|
|
148
|
+
for (const f of result.security_findings) {
|
|
149
|
+
lines.push(`- **${f.rule_id}**: ${f.message} (${f.filename}:${f.line})`)
|
|
150
|
+
}
|
|
151
|
+
lines.push("")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (result.impact_analysis) {
|
|
155
|
+
lines.push("### Impact Analysis")
|
|
156
|
+
lines.push(`- Affected methods: ${result.impact_analysis.affected_methods ?? 0}`)
|
|
157
|
+
lines.push(`- Affected callers: ${result.impact_analysis.affected_callers ?? 0}`)
|
|
158
|
+
if (result.impact_analysis.taint_paths?.length) {
|
|
159
|
+
lines.push(`- Taint paths: ${result.impact_analysis.taint_paths.length}`)
|
|
160
|
+
}
|
|
161
|
+
lines.push("")
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (result.complexity_changes?.length) {
|
|
165
|
+
lines.push("### Complexity Changes")
|
|
166
|
+
for (const c of result.complexity_changes) {
|
|
167
|
+
const delta = c.new_cc - c.old_cc
|
|
168
|
+
const sign = delta > 0 ? "+" : ""
|
|
169
|
+
lines.push(`- \`${c.method}\`: ${c.old_cc} -> ${c.new_cc} (${sign}${delta})`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return lines.length > 2 ? lines.join("\n") : "CodeGraph review: no issues found."
|
|
174
|
+
} catch {
|
|
175
|
+
return "CodeGraph review: API not available. Run `codegraph-api` first."
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Explain a function using CPG data.
|
|
181
|
+
*/
|
|
182
|
+
async explainFunction(projectId: string, functionName: string): Promise<string> {
|
|
183
|
+
try {
|
|
184
|
+
const params = new URLSearchParams({ name: functionName })
|
|
185
|
+
if (projectId) params.set("project_id", projectId)
|
|
186
|
+
|
|
187
|
+
const result = await this.fetch(`/api/v1/cpg/method-detail?${params}`)
|
|
188
|
+
if (!result) return `Function '${functionName}' not found in CPG.`
|
|
189
|
+
|
|
190
|
+
const lines: string[] = [
|
|
191
|
+
`## \`${result.name}\``,
|
|
192
|
+
"",
|
|
193
|
+
`| Metric | Value |`,
|
|
194
|
+
`|--------|-------|`,
|
|
195
|
+
`| File | \`${result.filename}\` |`,
|
|
196
|
+
`| Line | ${result.line_number} |`,
|
|
197
|
+
`| Cyclomatic complexity | ${result.cyclomatic_complexity} |`,
|
|
198
|
+
`| Fan-in (callers) | ${result.fan_in} |`,
|
|
199
|
+
`| Fan-out (callees) | ${result.fan_out} |`,
|
|
200
|
+
`| LOC | ${result.loc} |`,
|
|
201
|
+
`| Parameters | ${result.parameters} |`,
|
|
202
|
+
`| Entry point | ${result.is_entry_point ? "Yes" : "No"} |`,
|
|
203
|
+
`| External | ${result.is_external ? "Yes" : "No"} |`,
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
if (result.callers?.length) {
|
|
207
|
+
lines.push("", "### Callers")
|
|
208
|
+
for (const c of result.callers.slice(0, 20)) {
|
|
209
|
+
lines.push(`- \`${c.caller_name}\` (${c.caller_filename}:${c.caller_line})`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (result.callees?.length) {
|
|
214
|
+
lines.push("", "### Callees")
|
|
215
|
+
for (const c of result.callees.slice(0, 20)) {
|
|
216
|
+
lines.push(`- \`${c.callee_name}\` (${c.callee_filename}:${c.callee_line})`)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result.security_findings?.length) {
|
|
221
|
+
lines.push("", "### Security Findings")
|
|
222
|
+
for (const f of result.security_findings) {
|
|
223
|
+
lines.push(`- **${f.rule_id}**: ${f.message}`)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (result.taint_paths?.length) {
|
|
228
|
+
lines.push("", "### Taint Paths")
|
|
229
|
+
for (const t of result.taint_paths.slice(0, 5)) {
|
|
230
|
+
lines.push(`- ${t.source} -> ${t.sink} (${t.path_length} hops)`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return lines.join("\n")
|
|
235
|
+
} catch {
|
|
236
|
+
return `CodeGraph: API not available. Run \`codegraph-api\` first.`
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// -------------------------------------------------------------------
|
|
241
|
+
// Private helpers
|
|
242
|
+
// -------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
private async fetch(path: string, init?: RequestInit): Promise<any> {
|
|
245
|
+
const url = `${this.baseUrl}${path}`
|
|
246
|
+
const headers: Record<string, string> = {
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
...(init?.headers as Record<string, string> | undefined),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const response = await globalThis.fetch(url, {
|
|
252
|
+
...init,
|
|
253
|
+
headers,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(`CodeGraph API ${response.status}: ${response.statusText}`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const text = await response.text()
|
|
261
|
+
if (!text) return null
|
|
262
|
+
|
|
263
|
+
return JSON.parse(text)
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode plugin for CodeGraph CPG-powered code analysis.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Auto-enrichment of chat messages with CPG context for mentioned files
|
|
6
|
+
* - System prompt injection with project summary (hotspots, security findings)
|
|
7
|
+
* - Post-commit CPG incremental update trigger
|
|
8
|
+
* - Custom tools: codegraph_review, codegraph_explain_function
|
|
9
|
+
* - Auto-allow for codegraph_* MCP tools
|
|
10
|
+
* - Toast notifications on CPG update completion
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
14
|
+
import { tool } from "@opencode-ai/plugin/tool"
|
|
15
|
+
|
|
16
|
+
import { CodeGraphAPI } from "./api"
|
|
17
|
+
import { extractFileRefs, isGitCommit } from "./util"
|
|
18
|
+
|
|
19
|
+
const codegraphPlugin: Plugin = async (input) => {
|
|
20
|
+
const { client, directory, $ } = input
|
|
21
|
+
|
|
22
|
+
// Detect CodeGraph API URL from environment or default
|
|
23
|
+
const apiUrl = process.env.CODEGRAPH_API_URL || "http://localhost:8000"
|
|
24
|
+
const api = new CodeGraphAPI(apiUrl)
|
|
25
|
+
|
|
26
|
+
// Detect project ID from environment or opencode.json
|
|
27
|
+
const projectId = process.env.CODEGRAPH_PROJECT || ""
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
// -----------------------------------------------------------------
|
|
31
|
+
// 1. System prompt: inject CPG project summary
|
|
32
|
+
// -----------------------------------------------------------------
|
|
33
|
+
"experimental.chat.system.transform": async (_inp, output) => {
|
|
34
|
+
try {
|
|
35
|
+
const summary = await api.getProjectSummary(projectId)
|
|
36
|
+
if (summary) {
|
|
37
|
+
output.system.push(summary)
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// CodeGraph API not available — skip silently
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// -----------------------------------------------------------------
|
|
45
|
+
// 2. Chat message: auto-enrich with CPG context for mentioned files
|
|
46
|
+
// -----------------------------------------------------------------
|
|
47
|
+
"chat.message": async (_inp, output) => {
|
|
48
|
+
const files = extractFileRefs(output.parts)
|
|
49
|
+
if (files.length === 0) return
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
for (const file of files.slice(0, 5)) {
|
|
53
|
+
const context = await api.getFileCPGContext(projectId, file)
|
|
54
|
+
if (context) {
|
|
55
|
+
output.parts.push({
|
|
56
|
+
type: "text",
|
|
57
|
+
text: context,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// CodeGraph API not available — skip silently
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// -----------------------------------------------------------------
|
|
67
|
+
// 3. Post-commit: trigger incremental CPG update
|
|
68
|
+
// -----------------------------------------------------------------
|
|
69
|
+
"tool.execute.after": async (inp, output) => {
|
|
70
|
+
if (inp.tool !== "bash") return
|
|
71
|
+
if (!isGitCommit(output.output)) return
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await api.triggerIncrementalUpdate(projectId, directory)
|
|
75
|
+
// Notify user via output metadata (visible in OpenCode UI)
|
|
76
|
+
output.title = "CodeGraph: CPG update triggered"
|
|
77
|
+
output.metadata = {
|
|
78
|
+
...output.metadata,
|
|
79
|
+
codegraph_cpg_update: "triggered",
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Best-effort — don't break the workflow
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// -----------------------------------------------------------------
|
|
87
|
+
// 4. Custom tools
|
|
88
|
+
// -----------------------------------------------------------------
|
|
89
|
+
tool: {
|
|
90
|
+
codegraph_review: tool({
|
|
91
|
+
description:
|
|
92
|
+
"Run CodeGraph security + impact analysis on current changes. " +
|
|
93
|
+
"Analyzes git diff using CPG call graph, taint analysis, and pattern matching.",
|
|
94
|
+
args: {
|
|
95
|
+
base_ref: tool.schema
|
|
96
|
+
.string()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Base git ref to compare against (default: HEAD~1)"),
|
|
99
|
+
},
|
|
100
|
+
async execute(args) {
|
|
101
|
+
const baseRef = args.base_ref || "HEAD~1"
|
|
102
|
+
const diff = await $`git diff ${baseRef}`.quiet().text()
|
|
103
|
+
if (!diff.trim()) {
|
|
104
|
+
return "No changes found."
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await api.reviewChanges(projectId, diff, baseRef)
|
|
108
|
+
return result
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
codegraph_explain_function: tool({
|
|
113
|
+
description:
|
|
114
|
+
"Deep analysis of a function using CPG. Shows callers, callees, " +
|
|
115
|
+
"complexity, taint paths, and security findings.",
|
|
116
|
+
args: {
|
|
117
|
+
name: tool.schema.string().describe("Function or method name to analyze"),
|
|
118
|
+
},
|
|
119
|
+
async execute(args) {
|
|
120
|
+
const result = await api.explainFunction(projectId, args.name)
|
|
121
|
+
return result
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// -----------------------------------------------------------------
|
|
127
|
+
// 5. Permissions: auto-allow codegraph_* MCP tools
|
|
128
|
+
// -----------------------------------------------------------------
|
|
129
|
+
"permission.ask": async (inp, output) => {
|
|
130
|
+
if (inp.tool?.startsWith("codegraph_")) {
|
|
131
|
+
output.status = "allow"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default codegraphPlugin
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the CodeGraph OpenCode plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract file references from message parts.
|
|
7
|
+
* Looks for file paths in text parts (e.g., `src/api/main.py`, @filename patterns).
|
|
8
|
+
*/
|
|
9
|
+
export function extractFileRefs(parts: Array<{ type: string; text?: string }>): string[] {
|
|
10
|
+
const files: string[] = []
|
|
11
|
+
const seen = new Set<string>()
|
|
12
|
+
|
|
13
|
+
// Common file path patterns
|
|
14
|
+
const patterns = [
|
|
15
|
+
// Explicit @file references (OpenCode convention)
|
|
16
|
+
/(?<!\w)@(\.?[\w/\\.-]+\.\w+)/g,
|
|
17
|
+
// Backtick-quoted paths
|
|
18
|
+
/`([\w/\\.-]+\.\w{1,10})`/g,
|
|
19
|
+
// Common source file patterns in plain text
|
|
20
|
+
/\b((?:src|lib|app|tests?|pkg|cmd|internal)\/[\w/\\.-]+\.\w{1,10})\b/g,
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (part.type !== "text" || !part.text) continue
|
|
25
|
+
|
|
26
|
+
for (const pattern of patterns) {
|
|
27
|
+
// Reset lastIndex for global regex
|
|
28
|
+
pattern.lastIndex = 0
|
|
29
|
+
let match
|
|
30
|
+
while ((match = pattern.exec(part.text)) !== null) {
|
|
31
|
+
const file = match[1]
|
|
32
|
+
if (file && !seen.has(file) && isSourceFile(file)) {
|
|
33
|
+
seen.add(file)
|
|
34
|
+
files.push(file)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return files
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a shell command output indicates a git commit was made.
|
|
45
|
+
*/
|
|
46
|
+
export function isGitCommit(output: string): boolean {
|
|
47
|
+
if (!output) return false
|
|
48
|
+
|
|
49
|
+
// Common git commit success patterns
|
|
50
|
+
return (
|
|
51
|
+
/\[[\w/.-]+\s+[a-f0-9]+\]/.test(output) || // [main abc1234] commit message
|
|
52
|
+
/^\s*create mode/m.test(output) || // create mode 100644
|
|
53
|
+
/^\s*\d+ files? changed/m.test(output) // 3 files changed, 42 insertions
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// File extensions recognized as source code
|
|
58
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
59
|
+
"py",
|
|
60
|
+
"go",
|
|
61
|
+
"js",
|
|
62
|
+
"ts",
|
|
63
|
+
"jsx",
|
|
64
|
+
"tsx",
|
|
65
|
+
"java",
|
|
66
|
+
"kt",
|
|
67
|
+
"kts",
|
|
68
|
+
"c",
|
|
69
|
+
"h",
|
|
70
|
+
"cpp",
|
|
71
|
+
"hpp",
|
|
72
|
+
"cc",
|
|
73
|
+
"cxx",
|
|
74
|
+
"cs",
|
|
75
|
+
"php",
|
|
76
|
+
"rb",
|
|
77
|
+
"rs",
|
|
78
|
+
"swift",
|
|
79
|
+
"sql",
|
|
80
|
+
"yaml",
|
|
81
|
+
"yml",
|
|
82
|
+
"json",
|
|
83
|
+
"toml",
|
|
84
|
+
"xml",
|
|
85
|
+
"html",
|
|
86
|
+
"css",
|
|
87
|
+
"scss",
|
|
88
|
+
"md",
|
|
89
|
+
"proto",
|
|
90
|
+
"graphql",
|
|
91
|
+
"gql",
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
function isSourceFile(path: string): boolean {
|
|
95
|
+
const ext = path.split(".").pop()?.toLowerCase()
|
|
96
|
+
return ext ? SOURCE_EXTENSIONS.has(ext) : false
|
|
97
|
+
}
|