opencode-codegraph 0.1.3 → 0.1.5
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/CHANGELOG.md +23 -0
- package/README.md +23 -18
- package/package.json +3 -2
- package/src/api.ts +75 -3
- package/src/index.ts +26 -4
- package/src/util.ts +62 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.5 - 2026-03-20
|
|
4
|
+
|
|
5
|
+
- add unified next-action summary to review-trace status output
|
|
6
|
+
- surface next-action guidance in post-commit OpenCode plugin metadata and review-trace summaries
|
|
7
|
+
- document the next-action workflow in plugin and integration docs
|
|
8
|
+
|
|
9
|
+
## 0.1.4 - 2026-03-20
|
|
10
|
+
|
|
11
|
+
- add conversational pre-edit guidance when a chat message suggests code modification
|
|
12
|
+
- append durable review-trace summary after `git commit` when trace artifacts are available
|
|
13
|
+
- add unified `/status` workflow for freshness plus latest review-trace inspection in the surrounding OpenCode setup
|
|
14
|
+
|
|
15
|
+
## 0.1.3 - 2026-03-20
|
|
16
|
+
|
|
17
|
+
- surface durable review-trace summary back into OpenCode after commit
|
|
18
|
+
- add machine-readable `review_trace_status.py --json`
|
|
19
|
+
|
|
20
|
+
## 0.1.2 - 2026-03-20
|
|
21
|
+
|
|
22
|
+
- fix `pattern-results` API path handling in the plugin client
|
|
23
|
+
- align local OpenCode workspace installation with the published plugin flow
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Automatically enriches AI conversations with Code Property Graph data -- securit
|
|
|
23
23
|
|
|
24
24
|
### Auto-Enrichment
|
|
25
25
|
|
|
26
|
-
When you mention a file in chat, the plugin adds CPG context automatically:
|
|
26
|
+
When you mention a file in chat, the plugin adds CPG context automatically:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
You: "Refactor src/api/routers/webhook.py"
|
|
@@ -32,18 +32,20 @@ Plugin injects:
|
|
|
32
32
|
### CPG context: src/api/routers/webhook.py
|
|
33
33
|
**12 methods** in file:
|
|
34
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
|
-
```
|
|
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
|
+
If the message also suggests an edit intent (`refactor`, `fix`, `modify`, `update`, etc.), the plugin appends a pre-edit warning block with complexity, fan-out, dead-code, and security hints for the referenced file.
|
|
39
41
|
|
|
40
42
|
### System Prompt
|
|
41
43
|
|
|
42
44
|
Every conversation includes a project summary with file count, top complexity hotspots, and open security findings.
|
|
43
45
|
|
|
44
|
-
### Post-Commit Updates
|
|
45
|
-
|
|
46
|
-
After `git commit`, the plugin triggers incremental CPG re-parsing via GoCPG and syncs the ChromaDB vector store.
|
|
46
|
+
### Post-Commit Updates
|
|
47
|
+
|
|
48
|
+
After `git commit`, the plugin triggers incremental CPG re-parsing via GoCPG and syncs the ChromaDB vector store. If durable review-trace artifacts exist for the new `HEAD`, the plugin also appends a short review-trace summary with findings, recommendations, and a single next action to take.
|
|
47
49
|
|
|
48
50
|
### Custom Tools
|
|
49
51
|
|
|
@@ -62,10 +64,12 @@ Place in `.opencode/commands/`:
|
|
|
62
64
|
|
|
63
65
|
| Command | Description |
|
|
64
66
|
|---------|-------------|
|
|
65
|
-
| `/review` | CPG-powered code review |
|
|
66
|
-
| `/audit` | Full codebase audit (12 dimensions) |
|
|
67
|
-
| `/explain` | Function analysis with call graph |
|
|
68
|
-
| `/onboard` | Codebase understanding |
|
|
67
|
+
| `/review` | CPG-powered code review |
|
|
68
|
+
| `/audit` | Full codebase audit (12 dimensions) |
|
|
69
|
+
| `/explain` | Function analysis with call graph |
|
|
70
|
+
| `/onboard` | Codebase understanding |
|
|
71
|
+
| `/update` | Freshness check and incremental CPG update |
|
|
72
|
+
| `/status` | Unified freshness + latest review-trace status |
|
|
69
73
|
|
|
70
74
|
## Custom Agent
|
|
71
75
|
|
|
@@ -80,12 +84,13 @@ Place in `.opencode/commands/`:
|
|
|
80
84
|
|
|
81
85
|
## Hooks
|
|
82
86
|
|
|
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
|
-
| `
|
|
88
|
-
| `
|
|
87
|
+
| Hook | Purpose |
|
|
88
|
+
|------|---------|
|
|
89
|
+
| `experimental.chat.system.transform` | Inject project summary into system prompt |
|
|
90
|
+
| `chat.message` | Add CPG context for mentioned files |
|
|
91
|
+
| `chat.message` (edit intent) | Add pre-edit warnings for files likely to be modified |
|
|
92
|
+
| `tool.execute.after` | Trigger CPG update after git commit and append review-trace summary with next action |
|
|
93
|
+
| `permission.ask` | Auto-allow `codegraph_*` tools |
|
|
89
94
|
|
|
90
95
|
## License
|
|
91
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-codegraph",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "OpenCode plugin for CodeGraph CPG-powered code analysis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
".": "./src/index.ts"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"src"
|
|
11
|
+
"src",
|
|
12
|
+
"CHANGELOG.md"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
15
|
"typecheck": "tsc --noEmit",
|
package/src/api.ts
CHANGED
|
@@ -72,7 +72,7 @@ export class CodeGraphAPI {
|
|
|
72
72
|
* Get CPG context for a specific file.
|
|
73
73
|
* Returns markdown with methods, callers, complexity, and security findings.
|
|
74
74
|
*/
|
|
75
|
-
async getFileCPGContext(projectId: string, filePath: string): Promise<string | null> {
|
|
75
|
+
async getFileCPGContext(projectId: string, filePath: string): Promise<string | null> {
|
|
76
76
|
const params = new URLSearchParams()
|
|
77
77
|
if (projectId) params.set("project_id", projectId)
|
|
78
78
|
params.set("filename", filePath)
|
|
@@ -117,8 +117,80 @@ export class CodeGraphAPI {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
return lines.join("\n")
|
|
121
|
-
}
|
|
120
|
+
return lines.join("\n")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get pre-edit guidance for a specific file when the user is likely to modify it.
|
|
125
|
+
*/
|
|
126
|
+
async getPreEditGuidance(projectId: string, filePath: string): Promise<string | null> {
|
|
127
|
+
const params = new URLSearchParams()
|
|
128
|
+
if (projectId) params.set("project_id", projectId)
|
|
129
|
+
params.set("filename", filePath)
|
|
130
|
+
const findingsParams = new URLSearchParams(params)
|
|
131
|
+
findingsParams.set("category", "security")
|
|
132
|
+
|
|
133
|
+
const [methodsRes, findingsRes] = await Promise.allSettled([
|
|
134
|
+
this.fetch(`/api/v1/cpg/methods?${params}`),
|
|
135
|
+
this.fetch(`/api/v1/cpg/pattern-results?${findingsParams.toString()}`),
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
const methods = methodsRes.status === "fulfilled" ? methodsRes.value?.results || [] : []
|
|
139
|
+
const findings = findingsRes.status === "fulfilled" ? findingsRes.value?.results || [] : []
|
|
140
|
+
|
|
141
|
+
const highComplexity = methods.filter((m: any) => (m.cyclomatic_complexity ?? 0) >= 15)
|
|
142
|
+
const highFanOut = methods.filter((m: any) => (m.fan_out ?? 0) >= 30)
|
|
143
|
+
const deadMethods = methods.filter(
|
|
144
|
+
(m: any) => (m.fan_in ?? 0) === 0 && !m.is_entry_point && !m.is_external && !m.is_test,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (!highComplexity.length && !highFanOut.length && !deadMethods.length && !findings.length) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const lines: string[] = [`### CodeGraph Pre-Edit Warnings: \`${filePath}\``, ""]
|
|
152
|
+
|
|
153
|
+
if (highComplexity.length) {
|
|
154
|
+
lines.push("**High complexity methods:**")
|
|
155
|
+
for (const method of highComplexity.slice(0, 5)) {
|
|
156
|
+
lines.push(
|
|
157
|
+
`- \`${method.name}\` CC=${method.cyclomatic_complexity} fan_in=${method.fan_in} fan_out=${method.fan_out}`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
lines.push("")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (highFanOut.length) {
|
|
164
|
+
lines.push("**High fan-out methods:**")
|
|
165
|
+
for (const method of highFanOut.slice(0, 5)) {
|
|
166
|
+
lines.push(`- \`${method.name}\` fan_out=${method.fan_out}`)
|
|
167
|
+
}
|
|
168
|
+
lines.push("")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (deadMethods.length) {
|
|
172
|
+
lines.push("**Potential dead methods in file:**")
|
|
173
|
+
for (const method of deadMethods.slice(0, 5)) {
|
|
174
|
+
lines.push(`- \`${method.name}\` has fan_in=0`)
|
|
175
|
+
}
|
|
176
|
+
lines.push("")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (findings.length) {
|
|
180
|
+
lines.push(`**Security findings present:** ${findings.length}`)
|
|
181
|
+
for (const finding of findings.slice(0, 5)) {
|
|
182
|
+
lines.push(`- **${finding.rule_id}** L${finding.line}: ${finding.message}`)
|
|
183
|
+
}
|
|
184
|
+
lines.push("")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
lines.push("**Before editing:**")
|
|
188
|
+
lines.push("- Re-check callers before refactoring methods with high fan-in or high complexity.")
|
|
189
|
+
lines.push("- Update or add tests for the risky paths you touch in this file.")
|
|
190
|
+
lines.push("- If this file defines handlers/services, verify interface registration after the change.")
|
|
191
|
+
|
|
192
|
+
return lines.join("\n")
|
|
193
|
+
}
|
|
122
194
|
|
|
123
195
|
/**
|
|
124
196
|
* Trigger incremental CPG update after a git commit.
|
package/src/index.ts
CHANGED
|
@@ -16,9 +16,12 @@ import { tool } from "@opencode-ai/plugin/tool"
|
|
|
16
16
|
import { CodeGraphAPI } from "./api"
|
|
17
17
|
import {
|
|
18
18
|
extractFileRefs,
|
|
19
|
+
fileNeedsRegistrationCheck,
|
|
19
20
|
formatReviewTraceSummary,
|
|
20
21
|
getHeadCommit,
|
|
21
22
|
isGitCommit,
|
|
23
|
+
messageSuggestsEditing,
|
|
24
|
+
recommendedNextActionFromReviewTrace,
|
|
22
25
|
readReviewTraceSnapshot,
|
|
23
26
|
} from "./util"
|
|
24
27
|
|
|
@@ -50,10 +53,11 @@ const codegraphPlugin: Plugin = async (input) => {
|
|
|
50
53
|
// -----------------------------------------------------------------
|
|
51
54
|
// 2. Chat message: auto-enrich with CPG context for mentioned files
|
|
52
55
|
// -----------------------------------------------------------------
|
|
53
|
-
"chat.message": async (_inp, output) => {
|
|
54
|
-
const files = extractFileRefs(output.parts)
|
|
55
|
-
if (files.length === 0) return
|
|
56
|
-
|
|
56
|
+
"chat.message": async (_inp, output) => {
|
|
57
|
+
const files = extractFileRefs(output.parts)
|
|
58
|
+
if (files.length === 0) return
|
|
59
|
+
const editIntent = messageSuggestsEditing(output.parts)
|
|
60
|
+
|
|
57
61
|
try {
|
|
58
62
|
for (const file of files.slice(0, 5)) {
|
|
59
63
|
const context = await api.getFileCPGContext(projectId, file)
|
|
@@ -63,6 +67,23 @@ const codegraphPlugin: Plugin = async (input) => {
|
|
|
63
67
|
text: context,
|
|
64
68
|
} as any)
|
|
65
69
|
}
|
|
70
|
+
|
|
71
|
+
if (editIntent) {
|
|
72
|
+
const guidance = await api.getPreEditGuidance(projectId, file)
|
|
73
|
+
if (guidance) {
|
|
74
|
+
output.parts.push({
|
|
75
|
+
type: "text",
|
|
76
|
+
text: guidance,
|
|
77
|
+
} as any)
|
|
78
|
+
} else if (fileNeedsRegistrationCheck(file)) {
|
|
79
|
+
output.parts.push({
|
|
80
|
+
type: "text",
|
|
81
|
+
text:
|
|
82
|
+
`### CodeGraph Pre-Edit Reminder: \`${file}\`\n\n` +
|
|
83
|
+
"- This looks like a handler/service file. After editing, verify that CLI/API/MCP/ACP registration still matches the intended entry points.",
|
|
84
|
+
} as any)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
66
87
|
}
|
|
67
88
|
} catch {
|
|
68
89
|
// CodeGraph API not available — skip silently
|
|
@@ -92,6 +113,7 @@ const codegraphPlugin: Plugin = async (input) => {
|
|
|
92
113
|
codegraph_review_trace_findings: traceSnapshot.review_findings_count ?? null,
|
|
93
114
|
codegraph_review_trace_recommendations:
|
|
94
115
|
traceSnapshot.review_recommendations?.slice(0, 3) || [],
|
|
116
|
+
codegraph_review_trace_next_action: recommendedNextActionFromReviewTrace(traceSnapshot),
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
}
|
package/src/util.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type ReviewTraceSnapshot = {
|
|
|
20
20
|
* Extract file references from message parts.
|
|
21
21
|
* Looks for file paths in text parts (e.g., `src/api/main.py`, @filename patterns).
|
|
22
22
|
*/
|
|
23
|
-
export function extractFileRefs(parts: Array<{ type: string; text?: string }>): string[] {
|
|
23
|
+
export function extractFileRefs(parts: Array<{ type: string; text?: string }>): string[] {
|
|
24
24
|
const files: string[] = []
|
|
25
25
|
const seen = new Set<string>()
|
|
26
26
|
|
|
@@ -68,6 +68,37 @@ export function isGitCommit(output: string): boolean {
|
|
|
68
68
|
)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Heuristic: detect whether the user is likely asking to modify code.
|
|
73
|
+
*/
|
|
74
|
+
export function messageSuggestsEditing(parts: Array<{ type: string; text?: string }>): boolean {
|
|
75
|
+
const patterns = [
|
|
76
|
+
/\b(edit|change|modify|update|refactor|rewrite|implement|fix|patch|remove)\b/i,
|
|
77
|
+
/\b(add|improve|cleanup|clean up|rename|split|extract)\b/i,
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (part.type !== "text" || !part.text) continue
|
|
82
|
+
const text = part.text
|
|
83
|
+
if (patterns.some((pattern) => pattern.test(text))) {
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function fileNeedsRegistrationCheck(filePath: string): boolean {
|
|
92
|
+
const normalized = filePath.replace(/\\/g, "/")
|
|
93
|
+
const isHandlerLike = ["scenarios/", "services/", "analysis/", "security/"].some((segment) =>
|
|
94
|
+
normalized.includes(segment),
|
|
95
|
+
)
|
|
96
|
+
const isInterfaceLayer = ["src/cli/", "src/api/routers/", "src/tui/commands/", "src/mcp/", "src/acp/"].some(
|
|
97
|
+
(segment) => normalized.includes(segment),
|
|
98
|
+
)
|
|
99
|
+
return isHandlerLike && !isInterfaceLayer
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
export async function getHeadCommit($: any): Promise<string | null> {
|
|
72
103
|
try {
|
|
73
104
|
const sha = (await $`git rev-parse HEAD`.quiet().text()).trim()
|
|
@@ -132,9 +163,39 @@ export function formatReviewTraceSummary(snapshot: ReviewTraceSnapshot): string
|
|
|
132
163
|
}
|
|
133
164
|
}
|
|
134
165
|
|
|
166
|
+
const nextAction = recommendedNextActionFromReviewTrace(snapshot)
|
|
167
|
+
if (nextAction) {
|
|
168
|
+
lines.push("", `**Next action:** ${nextAction}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
135
171
|
return lines.length > 2 ? lines.join("\n") : null
|
|
136
172
|
}
|
|
137
173
|
|
|
174
|
+
export function recommendedNextActionFromReviewTrace(snapshot: ReviewTraceSnapshot): string {
|
|
175
|
+
const status = (snapshot.status || "").toLowerCase()
|
|
176
|
+
const findingsCount = snapshot.review_findings_count
|
|
177
|
+
const recommendations = Array.isArray(snapshot.review_recommendations)
|
|
178
|
+
? snapshot.review_recommendations.filter(Boolean)
|
|
179
|
+
: []
|
|
180
|
+
|
|
181
|
+
if (snapshot.error) {
|
|
182
|
+
return "Investigate the review-trace error, then run /review once the pipeline is healthy."
|
|
183
|
+
}
|
|
184
|
+
if (status === "running") {
|
|
185
|
+
return "Wait for review trace completion, then inspect findings and recommendations."
|
|
186
|
+
}
|
|
187
|
+
if (status && status !== "completed") {
|
|
188
|
+
return "Check the latest review trace status again and rerun /review if the result is incomplete."
|
|
189
|
+
}
|
|
190
|
+
if (typeof findingsCount === "number" && findingsCount > 0) {
|
|
191
|
+
if (recommendations.length) {
|
|
192
|
+
return "Apply the top CodeGraph recommendations, then run /review to confirm the fixes."
|
|
193
|
+
}
|
|
194
|
+
return "Inspect the reported findings, fix the highest-risk issues, then run /review again."
|
|
195
|
+
}
|
|
196
|
+
return "No review findings recorded; continue with /review or push once the rest of your checks are green."
|
|
197
|
+
}
|
|
198
|
+
|
|
138
199
|
// File extensions recognized as source code
|
|
139
200
|
const SOURCE_EXTENSIONS = new Set([
|
|
140
201
|
"py",
|