opencode-codegraph 0.1.0 → 0.1.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.
- package/README.md +92 -0
- package/package.json +1 -1
- package/src/api.ts +98 -54
- package/src/index.ts +23 -22
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# opencode-codegraph
|
|
2
|
+
|
|
3
|
+
OpenCode plugin for [CodeGraph](https://codegraph.ru) CPG-powered code analysis.
|
|
4
|
+
|
|
5
|
+
Automatically enriches AI conversations with Code Property Graph data -- security findings, call graphs, complexity metrics, and taint analysis -- without manual tool invocation.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
// opencode.json
|
|
11
|
+
{
|
|
12
|
+
"plugin": ["opencode-codegraph"]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- [CodeGraph](https://codegraph.ru) installed with CPG database built
|
|
19
|
+
- CodeGraph API running (`uvicorn src.api.main:app --port 8000`)
|
|
20
|
+
- CodeGraph MCP server configured in `opencode.json`
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
### Auto-Enrichment
|
|
25
|
+
|
|
26
|
+
When you mention a file in chat, the plugin adds CPG context automatically:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
You: "Refactor src/api/routers/webhook.py"
|
|
30
|
+
|
|
31
|
+
Plugin injects:
|
|
32
|
+
### CPG context: src/api/routers/webhook.py
|
|
33
|
+
**12 methods** in file:
|
|
34
|
+
- `receive_github_webhook` CC=5 fan_in=0 fan_out=3 [entry]
|
|
35
|
+
- `_handle_push` CC=2 fan_in=4 fan_out=2
|
|
36
|
+
**2 security findings:**
|
|
37
|
+
- CWE-89 L42: SQL injection in query parameter
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### System Prompt
|
|
41
|
+
|
|
42
|
+
Every conversation includes a project summary with file count, top complexity hotspots, and open security findings.
|
|
43
|
+
|
|
44
|
+
### Post-Commit Updates
|
|
45
|
+
|
|
46
|
+
After `git commit`, the plugin triggers incremental CPG re-parsing via GoCPG and syncs the ChromaDB vector store.
|
|
47
|
+
|
|
48
|
+
### Custom Tools
|
|
49
|
+
|
|
50
|
+
| Tool | Description |
|
|
51
|
+
|------|-------------|
|
|
52
|
+
| `codegraph_review` | Security + impact analysis on current diff |
|
|
53
|
+
| `codegraph_explain_function` | Deep function analysis with call graph |
|
|
54
|
+
|
|
55
|
+
### Permissions
|
|
56
|
+
|
|
57
|
+
All `codegraph_*` MCP tools are auto-allowed -- no confirmation prompts.
|
|
58
|
+
|
|
59
|
+
## Custom Commands
|
|
60
|
+
|
|
61
|
+
Place in `.opencode/commands/`:
|
|
62
|
+
|
|
63
|
+
| Command | Description |
|
|
64
|
+
|---------|-------------|
|
|
65
|
+
| `/review` | CPG-powered code review |
|
|
66
|
+
| `/audit` | Full codebase audit (12 dimensions) |
|
|
67
|
+
| `/explain` | Function analysis with call graph |
|
|
68
|
+
| `/onboard` | Codebase understanding |
|
|
69
|
+
|
|
70
|
+
## Custom Agent
|
|
71
|
+
|
|
72
|
+
`.opencode/agents/codegraph.md` -- CPG-focused analysis agent. Switch with `/agent codegraph`.
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
| Variable | Default | Description |
|
|
77
|
+
|----------|---------|-------------|
|
|
78
|
+
| `CODEGRAPH_API_URL` | `http://localhost:8000` | CodeGraph API base URL |
|
|
79
|
+
| `CODEGRAPH_PROJECT` | (empty) | Default project ID |
|
|
80
|
+
|
|
81
|
+
## Hooks
|
|
82
|
+
|
|
83
|
+
| Hook | Purpose |
|
|
84
|
+
|------|---------|
|
|
85
|
+
| `experimental.chat.system.transform` | Inject project summary into system prompt |
|
|
86
|
+
| `chat.message` | Add CPG context for mentioned files |
|
|
87
|
+
| `tool.execute.after` | Trigger CPG update after git commit |
|
|
88
|
+
| `permission.ask` | Auto-allow `codegraph_*` tools |
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -6,24 +6,31 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export class CodeGraphAPI {
|
|
9
|
-
private baseUrl: string
|
|
10
|
-
|
|
11
|
-
constructor(baseUrl: string) {
|
|
12
|
-
|
|
9
|
+
private baseUrl: string
|
|
10
|
+
|
|
11
|
+
constructor(baseUrl: string) {
|
|
12
|
+
const trimmed = baseUrl.replace(/\/+$/, "")
|
|
13
|
+
this.baseUrl = trimmed.replace(/\/api\/v1$/i, "")
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Get project summary for system prompt injection.
|
|
17
18
|
* Returns a markdown block with file count, top hotspots, and open security findings.
|
|
18
19
|
*/
|
|
19
|
-
async getProjectSummary(projectId: string): Promise<string | null> {
|
|
20
|
-
const params =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
async getProjectSummary(projectId: string): Promise<string | null> {
|
|
21
|
+
const params = new URLSearchParams()
|
|
22
|
+
if (projectId) params.set("project_id", projectId)
|
|
23
|
+
const statsPath = params.toString() ? `/api/v1/cpg/stats?${params}` : "/api/v1/cpg/stats"
|
|
24
|
+
const findingsParams = new URLSearchParams(params)
|
|
25
|
+
findingsParams.set("category", "security")
|
|
26
|
+
findingsParams.set("limit", "10")
|
|
27
|
+
const findingsPath = `/api/v1/cpg/pattern-results?${findingsParams}`
|
|
28
|
+
|
|
29
|
+
// Fetch multiple endpoints in parallel
|
|
30
|
+
const [statsRes, findingsRes] = await Promise.allSettled([
|
|
31
|
+
this.fetch(statsPath),
|
|
32
|
+
this.fetch(findingsPath),
|
|
33
|
+
])
|
|
27
34
|
|
|
28
35
|
const stats =
|
|
29
36
|
statsRes.status === "fulfilled" ? statsRes.value : null
|
|
@@ -66,14 +73,16 @@ export class CodeGraphAPI {
|
|
|
66
73
|
* Returns markdown with methods, callers, complexity, and security findings.
|
|
67
74
|
*/
|
|
68
75
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const params = new URLSearchParams()
|
|
77
|
+
if (projectId) params.set("project_id", projectId)
|
|
78
|
+
params.set("filename", filePath)
|
|
79
|
+
const findingsParams = new URLSearchParams(params)
|
|
80
|
+
findingsParams.set("category", "security")
|
|
81
|
+
|
|
82
|
+
const [methodsRes, findingsRes] = await Promise.allSettled([
|
|
83
|
+
this.fetch(`/api/v1/cpg/methods?${params}`),
|
|
84
|
+
this.fetch(`/api/v1/cpg/pattern-results?${findingsParams.toString()}`),
|
|
85
|
+
])
|
|
77
86
|
|
|
78
87
|
const methods =
|
|
79
88
|
methodsRes.status === "fulfilled" ? methodsRes.value : null
|
|
@@ -161,14 +170,18 @@ export class CodeGraphAPI {
|
|
|
161
170
|
lines.push("")
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
if (result.complexity_changes?.length) {
|
|
165
|
-
lines.push("### Complexity Changes")
|
|
166
|
-
for (const c of result.complexity_changes) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
if (result.complexity_changes?.length) {
|
|
174
|
+
lines.push("### Complexity Changes")
|
|
175
|
+
for (const c of result.complexity_changes) {
|
|
176
|
+
if ((c.old_cc ?? 0) > 0) {
|
|
177
|
+
const delta = c.new_cc - c.old_cc
|
|
178
|
+
const sign = delta > 0 ? "+" : ""
|
|
179
|
+
lines.push(`- \`${c.method}\`: ${c.old_cc} -> ${c.new_cc} (${sign}${delta})`)
|
|
180
|
+
} else {
|
|
181
|
+
lines.push(`- \`${c.method}\`: current CC ${c.new_cc}`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
172
185
|
|
|
173
186
|
return lines.length > 2 ? lines.join("\n") : "CodeGraph review: no issues found."
|
|
174
187
|
} catch {
|
|
@@ -237,29 +250,60 @@ export class CodeGraphAPI {
|
|
|
237
250
|
}
|
|
238
251
|
}
|
|
239
252
|
|
|
240
|
-
// -------------------------------------------------------------------
|
|
241
|
-
// Private helpers
|
|
242
|
-
// -------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
private
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
253
|
+
// -------------------------------------------------------------------
|
|
254
|
+
// Private helpers
|
|
255
|
+
// -------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
private resolveUrl(path: string): string {
|
|
258
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`
|
|
259
|
+
return new URL(normalizedPath, `${this.baseUrl}/`).toString()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async parseJsonBody(response: Response): Promise<any> {
|
|
263
|
+
const text = await response.text()
|
|
264
|
+
if (!text) return null
|
|
265
|
+
return JSON.parse(text)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async fetch(path: string, init?: RequestInit): Promise<any> {
|
|
269
|
+
const headers: Record<string, string> = {
|
|
270
|
+
"Content-Type": "application/json",
|
|
271
|
+
...(init?.headers as Record<string, string> | undefined),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const response = await globalThis.fetch(this.resolveUrl(path), {
|
|
275
|
+
...init,
|
|
276
|
+
headers,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
if (response.status === 404 && path.startsWith("/api/v1/cpg/pattern-results")) {
|
|
280
|
+
const fallbackPath = path.replace(
|
|
281
|
+
"/api/v1/cpg/pattern-results",
|
|
282
|
+
"/api/v1/patterns/findings",
|
|
283
|
+
)
|
|
284
|
+
const fallbackResponse = await globalThis.fetch(this.resolveUrl(fallbackPath), {
|
|
285
|
+
...init,
|
|
286
|
+
headers,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if (fallbackResponse.ok) {
|
|
290
|
+
const payload = await this.parseJsonBody(fallbackResponse)
|
|
291
|
+
const findings = Array.isArray(payload?.findings) ? payload.findings : []
|
|
292
|
+
return {
|
|
293
|
+
results: findings.map((finding: any) => ({
|
|
294
|
+
...finding,
|
|
295
|
+
line: finding.line ?? finding.line_number ?? 0,
|
|
296
|
+
})),
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw new Error(`CodeGraph API ${fallbackResponse.status}: ${fallbackResponse.statusText}`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
throw new Error(`CodeGraph API ${response.status}: ${response.statusText}`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return this.parseJsonBody(response)
|
|
308
|
+
}
|
|
309
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -48,20 +48,20 @@ const codegraphPlugin: Plugin = async (input) => {
|
|
|
48
48
|
const files = extractFileRefs(output.parts)
|
|
49
49
|
if (files.length === 0) return
|
|
50
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
|
-
},
|
|
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
|
+
} as any)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// CodeGraph API not available — skip silently
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
65
|
|
|
66
66
|
// -----------------------------------------------------------------
|
|
67
67
|
// 3. Post-commit: trigger incremental CPG update
|
|
@@ -123,14 +123,15 @@ const codegraphPlugin: Plugin = async (input) => {
|
|
|
123
123
|
}),
|
|
124
124
|
},
|
|
125
125
|
|
|
126
|
-
// -----------------------------------------------------------------
|
|
127
|
-
// 5. Permissions: auto-allow codegraph_* MCP tools
|
|
128
|
-
// -----------------------------------------------------------------
|
|
129
|
-
"permission.ask": async (inp, output) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
// -----------------------------------------------------------------
|
|
127
|
+
// 5. Permissions: auto-allow codegraph_* MCP tools
|
|
128
|
+
// -----------------------------------------------------------------
|
|
129
|
+
"permission.ask": async (inp, output) => {
|
|
130
|
+
const toolName = (inp as any)?.tool as string | undefined
|
|
131
|
+
if (toolName?.startsWith("codegraph_")) {
|
|
132
|
+
output.status = "allow"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|