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 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
- | `tool.execute.after` | Trigger CPG update after git commit |
88
- | `permission.ask` | Auto-allow `codegraph_*` tools |
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",
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",