open-plan-annotator 0.2.5 → 0.2.6
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -22
- package/opencode/bridge.js +166 -0
- package/opencode/config.js +79 -0
- package/opencode/config.test.ts +110 -0
- package/opencode/index.js +184 -0
- package/package.json +6 -8
- package/bin/open-plan-annotator-install-opencode.cjs +0 -3
- package/opencode-plugin/index.ts +0 -174
- package/opencode-plugin/package.json +0 -23
- package/scripts/install-opencode-plugin.cjs +0 -91
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"name": "open-plan-annotator",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
|
|
15
|
-
"version": "0.2.
|
|
15
|
+
"version": "0.2.6",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "ndom91"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-plan-annotator",
|
|
3
3
|
"description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ndom91"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Select text to <code style="color: purple">strikethrough</code>, <code style="co
|
|
|
18
18
|
2. An ephemeral HTTP server starts and opens a React UI in your browser
|
|
19
19
|
3. You review and annotate the plan
|
|
20
20
|
4. **Approve** or **Request Changes**
|
|
21
|
-
5. The tool returns
|
|
21
|
+
5. The tool returns structured JSON output back to the host
|
|
22
22
|
|
|
23
23
|
The server shuts down after you decide. Everything runs locally, nothing leaves your machine.
|
|
24
24
|
|
|
@@ -57,34 +57,35 @@ This registers the `ExitPlanMode` hook that launches the annotation UI.
|
|
|
57
57
|
|
|
58
58
|
### OpenCode
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Add `open-plan-annotator` to the `plugin` array in your OpenCode config (`opencode.json` or `.opencode/config.json`):
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cd /path/to/your/project
|
|
67
|
-
open-plan-annotator-install-opencode
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"plugin": ["open-plan-annotator"]
|
|
65
|
+
}
|
|
68
66
|
```
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
**Option B: From source**
|
|
73
|
-
|
|
74
|
-
```sh
|
|
75
|
-
git clone https://github.com/ndom91/open-plan-annotator.git
|
|
76
|
-
cd open-plan-annotator
|
|
77
|
-
bun install
|
|
78
|
-
bun run install:opencode-plugin # installs to .opencode/plugins/ in CWD
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The install script creates the auto-discovery shim, so no config changes needed.
|
|
82
|
-
|
|
83
|
-
The plugin automatically:
|
|
68
|
+
OpenCode will install the package and load it automatically. The plugin:
|
|
84
69
|
- Injects plan-mode instructions into the agent's system prompt
|
|
85
70
|
- Registers a `submit_plan` tool that the agent calls after creating a plan
|
|
86
71
|
- Spawns the annotation UI in your browser for review
|
|
87
72
|
- Returns structured feedback to the agent on approval or rejection
|
|
73
|
+
- Optionally hands off to an implementation agent after approval
|
|
74
|
+
|
|
75
|
+
#### Implementation handoff
|
|
76
|
+
|
|
77
|
+
By default, after a plan is approved the plugin sends "Proceed with implementation." to a `build` agent. To customize or disable this, create `open-plan-annotator.json` in your project's `.opencode/` directory or globally in `~/.config/opencode/`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"implementationHandoff": {
|
|
82
|
+
"enabled": true,
|
|
83
|
+
"agent": "build"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Set `enabled` to `false` to disable auto-handoff. Project config overrides global config.
|
|
88
89
|
|
|
89
90
|
### From source (Claude Code)
|
|
90
91
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const WRAPPER_PATH = fileURLToPath(new URL("../bin/open-plan-annotator.cjs", import.meta.url));
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* hookSpecificOutput: {
|
|
10
|
+
* hookEventName: "PermissionRequest",
|
|
11
|
+
* decision: { behavior: "allow" } | { behavior: "deny", message: string }
|
|
12
|
+
* }
|
|
13
|
+
* }} HookOutput
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ plan: string, sessionId?: string, cwd?: string }} options
|
|
18
|
+
*/
|
|
19
|
+
function buildHookPayload(options) {
|
|
20
|
+
return {
|
|
21
|
+
session_id: options.sessionId ?? randomUUID(),
|
|
22
|
+
transcript_path: "",
|
|
23
|
+
cwd: options.cwd ?? process.cwd(),
|
|
24
|
+
permission_mode: "default",
|
|
25
|
+
hook_event_name: "PermissionRequest",
|
|
26
|
+
tool_name: "ExitPlanMode",
|
|
27
|
+
tool_use_id: randomUUID(),
|
|
28
|
+
tool_input: {
|
|
29
|
+
plan: options.plan,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} stdoutText
|
|
36
|
+
* @param {string} stderrText
|
|
37
|
+
* @returns {HookOutput}
|
|
38
|
+
*/
|
|
39
|
+
function parseHookOutput(stdoutText, stderrText) {
|
|
40
|
+
const trimmed = stdoutText.trim();
|
|
41
|
+
if (!trimmed) {
|
|
42
|
+
const stderr = stderrText.trim();
|
|
43
|
+
throw new Error(
|
|
44
|
+
stderr
|
|
45
|
+
? `open-plan-annotator returned empty stdout; stderr: ${stderr}`
|
|
46
|
+
: "open-plan-annotator returned empty stdout",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return validateHookOutput(JSON.parse(trimmed));
|
|
52
|
+
} catch {
|
|
53
|
+
const lines = trimmed
|
|
54
|
+
.split(/\r?\n/)
|
|
55
|
+
.map((line) => line.trim())
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.reverse();
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
try {
|
|
61
|
+
return validateHookOutput(JSON.parse(line));
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore and keep searching
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error("open-plan-annotator returned invalid hook JSON");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {unknown} value
|
|
73
|
+
* @returns {HookOutput}
|
|
74
|
+
*/
|
|
75
|
+
function validateHookOutput(value) {
|
|
76
|
+
if (!value || typeof value !== "object") {
|
|
77
|
+
throw new Error("invalid hook output shape");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const output = /** @type {HookOutput} */ (value);
|
|
81
|
+
const decision = output?.hookSpecificOutput?.decision;
|
|
82
|
+
|
|
83
|
+
if (!decision || typeof decision !== "object" || typeof decision.behavior !== "string") {
|
|
84
|
+
throw new Error("missing decision in hook output");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (decision.behavior === "allow") {
|
|
88
|
+
return output;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (decision.behavior === "deny" && typeof decision.message === "string") {
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error("unsupported decision payload");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {{ plan: string, sessionId?: string, cwd?: string }} options
|
|
100
|
+
*/
|
|
101
|
+
export async function runPlanReview(options) {
|
|
102
|
+
const payload = buildHookPayload(options);
|
|
103
|
+
|
|
104
|
+
const result = await new Promise((resolve, reject) => {
|
|
105
|
+
const child = spawn(WRAPPER_PATH, [], {
|
|
106
|
+
cwd: options.cwd ?? process.cwd(),
|
|
107
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
108
|
+
env: process.env,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let stdout = "";
|
|
112
|
+
let stderr = "";
|
|
113
|
+
|
|
114
|
+
child.stdout.on("data", (chunk) => {
|
|
115
|
+
stdout += String(chunk);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.stderr.on("data", (chunk) => {
|
|
119
|
+
stderr += String(chunk);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
child.on("error", (error) => {
|
|
123
|
+
reject(error);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.on("close", (code, signal) => {
|
|
127
|
+
resolve({ code, signal, stdout, stderr });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
131
|
+
child.stdin.end();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const settled =
|
|
135
|
+
/** @type {{ code: number | null, signal: NodeJS.Signals | null, stdout: string, stderr: string }} */ (result);
|
|
136
|
+
|
|
137
|
+
if (settled.signal) {
|
|
138
|
+
const errorText = settled.stderr.trim();
|
|
139
|
+
throw new Error(
|
|
140
|
+
errorText
|
|
141
|
+
? `open-plan-annotator was terminated by signal ${settled.signal}: ${errorText}`
|
|
142
|
+
: `open-plan-annotator was terminated by signal ${settled.signal}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (settled.code !== 0) {
|
|
147
|
+
const errorText = settled.stderr.trim();
|
|
148
|
+
throw new Error(
|
|
149
|
+
errorText
|
|
150
|
+
? `open-plan-annotator exited with code ${settled.code}: ${errorText}`
|
|
151
|
+
: `open-plan-annotator exited with code ${settled.code}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const output = parseHookOutput(settled.stdout, settled.stderr);
|
|
156
|
+
const decision = output.hookSpecificOutput.decision;
|
|
157
|
+
|
|
158
|
+
if (decision.behavior === "allow") {
|
|
159
|
+
return { approved: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
approved: false,
|
|
164
|
+
feedback: decision.message,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE_NAME = "open-plan-annotator.json";
|
|
6
|
+
const DEFAULT_HANDOFF_ENABLED = true;
|
|
7
|
+
const DEFAULT_IMPLEMENTATION_AGENT = "build";
|
|
8
|
+
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readConfigFile(path) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(path, "utf8");
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveConfigBaseDir() {
|
|
24
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
|
|
25
|
+
if (xdgConfigHome) {
|
|
26
|
+
return xdgConfigHome;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const homeDir = homedir()?.trim();
|
|
30
|
+
if (homeDir) {
|
|
31
|
+
return join(homeDir, ".config");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseImplementationHandoff(config) {
|
|
38
|
+
if (!isRecord(config)) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const raw = config.implementationHandoff;
|
|
43
|
+
if (!isRecord(raw)) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const enabled = typeof raw.enabled === "boolean" ? raw.enabled : undefined;
|
|
48
|
+
|
|
49
|
+
let agent;
|
|
50
|
+
if (typeof raw.agent === "string") {
|
|
51
|
+
const trimmed = raw.agent.trim();
|
|
52
|
+
if (trimmed) {
|
|
53
|
+
agent = trimmed;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { enabled, agent };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function resolveImplementationHandoff(directory) {
|
|
61
|
+
const globalConfigDir = resolveConfigBaseDir();
|
|
62
|
+
const globalConfigPath = globalConfigDir ? join(globalConfigDir, "opencode", CONFIG_FILE_NAME) : undefined;
|
|
63
|
+
const projectConfigPath = directory ? join(directory, ".opencode", CONFIG_FILE_NAME) : undefined;
|
|
64
|
+
|
|
65
|
+
const globalConfig = globalConfigPath ? parseImplementationHandoff(await readConfigFile(globalConfigPath)) : {};
|
|
66
|
+
const projectConfig = projectConfigPath ? parseImplementationHandoff(await readConfigFile(projectConfigPath)) : {};
|
|
67
|
+
|
|
68
|
+
const enabled = projectConfig.enabled ?? globalConfig.enabled ?? DEFAULT_HANDOFF_ENABLED;
|
|
69
|
+
const agent = projectConfig.agent ?? globalConfig.agent ?? DEFAULT_IMPLEMENTATION_AGENT;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
enabled,
|
|
73
|
+
agent,
|
|
74
|
+
paths: {
|
|
75
|
+
global: globalConfigPath,
|
|
76
|
+
project: projectConfigPath,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { resolveImplementationHandoff } from "./config.js";
|
|
6
|
+
|
|
7
|
+
function withEnv(key: string, value: string | undefined, fn: () => Promise<void>) {
|
|
8
|
+
const original = process.env[key];
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
delete process.env[key];
|
|
11
|
+
} else {
|
|
12
|
+
process.env[key] = value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return fn().finally(() => {
|
|
16
|
+
if (original === undefined) {
|
|
17
|
+
delete process.env[key];
|
|
18
|
+
} else {
|
|
19
|
+
process.env[key] = original;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("resolveImplementationHandoff", () => {
|
|
25
|
+
test("returns defaults when no config files exist", async () => {
|
|
26
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await withEnv("XDG_CONFIG_HOME", join(tempRoot, "xdg"), async () => {
|
|
30
|
+
const result = await resolveImplementationHandoff(join(tempRoot, "project"));
|
|
31
|
+
expect(result.enabled).toBe(true);
|
|
32
|
+
expect(result.agent).toBe("build");
|
|
33
|
+
});
|
|
34
|
+
} finally {
|
|
35
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("uses global config when project config is missing", async () => {
|
|
40
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
|
|
41
|
+
const xdgHome = join(tempRoot, "xdg");
|
|
42
|
+
const globalConfigDir = join(xdgHome, "opencode");
|
|
43
|
+
mkdirSync(globalConfigDir, { recursive: true });
|
|
44
|
+
writeFileSync(
|
|
45
|
+
join(globalConfigDir, "open-plan-annotator.json"),
|
|
46
|
+
JSON.stringify({ implementationHandoff: { enabled: false, agent: "explore" } }),
|
|
47
|
+
"utf8",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await withEnv("XDG_CONFIG_HOME", xdgHome, async () => {
|
|
52
|
+
const result = await resolveImplementationHandoff(join(tempRoot, "project"));
|
|
53
|
+
expect(result.enabled).toBe(false);
|
|
54
|
+
expect(result.agent).toBe("explore");
|
|
55
|
+
});
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("project config overrides global config", async () => {
|
|
62
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
|
|
63
|
+
const xdgHome = join(tempRoot, "xdg");
|
|
64
|
+
const globalConfigDir = join(xdgHome, "opencode");
|
|
65
|
+
const projectDir = join(tempRoot, "project");
|
|
66
|
+
const projectConfigDir = join(projectDir, ".opencode");
|
|
67
|
+
|
|
68
|
+
mkdirSync(globalConfigDir, { recursive: true });
|
|
69
|
+
mkdirSync(projectConfigDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(globalConfigDir, "open-plan-annotator.json"),
|
|
73
|
+
JSON.stringify({ implementationHandoff: { enabled: true, agent: "explore" } }),
|
|
74
|
+
"utf8",
|
|
75
|
+
);
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(projectConfigDir, "open-plan-annotator.json"),
|
|
78
|
+
JSON.stringify({ implementationHandoff: { enabled: false, agent: "build" } }),
|
|
79
|
+
"utf8",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await withEnv("XDG_CONFIG_HOME", xdgHome, async () => {
|
|
84
|
+
const result = await resolveImplementationHandoff(projectDir);
|
|
85
|
+
expect(result.enabled).toBe(false);
|
|
86
|
+
expect(result.agent).toBe("build");
|
|
87
|
+
});
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("ignores malformed config and falls back", async () => {
|
|
94
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
|
|
95
|
+
const xdgHome = join(tempRoot, "xdg");
|
|
96
|
+
const globalConfigDir = join(xdgHome, "opencode");
|
|
97
|
+
mkdirSync(globalConfigDir, { recursive: true });
|
|
98
|
+
writeFileSync(join(globalConfigDir, "open-plan-annotator.json"), "not-json", "utf8");
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await withEnv("XDG_CONFIG_HOME", xdgHome, async () => {
|
|
102
|
+
const result = await resolveImplementationHandoff(join(tempRoot, "project"));
|
|
103
|
+
expect(result.enabled).toBe(true);
|
|
104
|
+
expect(result.agent).toBe("build");
|
|
105
|
+
});
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { runPlanReview } from "./bridge.js";
|
|
3
|
+
import { resolveImplementationHandoff } from "./config.js";
|
|
4
|
+
|
|
5
|
+
const PLAN_REVIEW_INSTRUCTIONS = `## Plan Review Workflow
|
|
6
|
+
|
|
7
|
+
For non-trivial implementation work, create a plan first and call the \`submit_plan\` tool.
|
|
8
|
+
The user will review the plan in a browser and either approve it or request changes.
|
|
9
|
+
|
|
10
|
+
- If approved, proceed with implementation.
|
|
11
|
+
- If changes are requested, revise the plan and call \`submit_plan\` again.
|
|
12
|
+
|
|
13
|
+
Do not begin implementation until the plan is approved.`;
|
|
14
|
+
|
|
15
|
+
const IMPLEMENTATION_PROMPT = "Proceed with implementation.";
|
|
16
|
+
|
|
17
|
+
function getErrorMessage(error) {
|
|
18
|
+
if (error instanceof Error && error.message) {
|
|
19
|
+
return error.message;
|
|
20
|
+
}
|
|
21
|
+
return "unknown error";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @type {import("@opencode-ai/plugin").Plugin} */
|
|
25
|
+
export const OpenPlanAnnotatorPlugin = async (ctx) => {
|
|
26
|
+
const implementationHandoff = await resolveImplementationHandoff(ctx.directory);
|
|
27
|
+
const implementationAgent = implementationHandoff.enabled ? implementationHandoff.agent : undefined;
|
|
28
|
+
|
|
29
|
+
async function getCurrentUserAgent(sessionID) {
|
|
30
|
+
if (!sessionID) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const response = await ctx.client.session.messages({
|
|
35
|
+
path: { id: sessionID },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const messages = response?.data;
|
|
39
|
+
if (!Array.isArray(messages)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
44
|
+
const message = messages[i];
|
|
45
|
+
if (message?.info?.role === "user") {
|
|
46
|
+
return message?.info?.agent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function shouldInjectPlanReviewInstructions(sessionID) {
|
|
54
|
+
if (!sessionID) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const currentAgent = await getCurrentUserAgent(sessionID);
|
|
60
|
+
if (!currentAgent) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (implementationAgent && currentAgent === implementationAgent) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = await ctx.client.app.agents({
|
|
69
|
+
query: { directory: ctx.directory },
|
|
70
|
+
});
|
|
71
|
+
const agents = response?.data;
|
|
72
|
+
if (!Array.isArray(agents)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const agent = agents.find((candidate) => candidate.name === currentAgent);
|
|
77
|
+
if (agent?.mode === "subagent") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handoffToImplementationAgent(sessionID) {
|
|
88
|
+
if (!implementationAgent) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!sessionID) {
|
|
93
|
+
return {
|
|
94
|
+
agent: implementationAgent,
|
|
95
|
+
warning: "Could not auto-switch because the current session ID was unavailable.",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await ctx.client.session.prompt({
|
|
101
|
+
path: { id: sessionID },
|
|
102
|
+
body: {
|
|
103
|
+
agent: implementationAgent,
|
|
104
|
+
noReply: true,
|
|
105
|
+
parts: [{ type: "text", text: IMPLEMENTATION_PROMPT }],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return { agent: implementationAgent };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
agent: implementationAgent,
|
|
113
|
+
warning: `Could not auto-switch to \`${implementationAgent}\`: ${getErrorMessage(error)}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
120
|
+
const currentSystem = output.system.join("\n").toLowerCase();
|
|
121
|
+
if (currentSystem.includes("title generator") || currentSystem.includes("generate a concise title")) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!currentSystem.includes("submit_plan")) {
|
|
126
|
+
const shouldInject = await shouldInjectPlanReviewInstructions(input?.sessionID);
|
|
127
|
+
if (shouldInject) {
|
|
128
|
+
output.system.push(PLAN_REVIEW_INSTRUCTIONS);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
tool: {
|
|
134
|
+
submit_plan: tool({
|
|
135
|
+
description:
|
|
136
|
+
"Submit a markdown plan for interactive user review. Returns approval status or structured revision feedback.",
|
|
137
|
+
|
|
138
|
+
args: {
|
|
139
|
+
plan: tool.schema.string().describe("The complete implementation plan in markdown format"),
|
|
140
|
+
summary: tool.schema.string().optional().describe("Optional one-line plan summary"),
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async execute(args, context) {
|
|
144
|
+
const result = await runPlanReview({
|
|
145
|
+
plan: args.plan,
|
|
146
|
+
sessionId: context.sessionID,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (result.approved) {
|
|
150
|
+
const lines = ["Plan approved by the user."];
|
|
151
|
+
|
|
152
|
+
if (args.summary) {
|
|
153
|
+
lines.push(`Summary: ${args.summary}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const handoffResult = await handoffToImplementationAgent(context.sessionID);
|
|
157
|
+
if (handoffResult) {
|
|
158
|
+
if (handoffResult.warning) {
|
|
159
|
+
lines.push(`Auto-switch warning: ${handoffResult.warning}`);
|
|
160
|
+
} else {
|
|
161
|
+
lines.push(`Auto-switched to the \`${handoffResult.agent}\` agent for implementation.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lines.push("Proceed with implementation.");
|
|
166
|
+
return lines.join("\n\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
"Plan needs revision.",
|
|
171
|
+
"",
|
|
172
|
+
"## User feedback",
|
|
173
|
+
"",
|
|
174
|
+
result.feedback ?? "Plan changes requested.",
|
|
175
|
+
"",
|
|
176
|
+
"Revise the plan using this feedback, then call `submit_plan` again.",
|
|
177
|
+
].join("\n");
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export default OpenPlanAnnotatorPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-plan-annotator",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
|
|
6
6
|
"author": "ndom91",
|
|
@@ -17,20 +17,18 @@
|
|
|
17
17
|
"annotation",
|
|
18
18
|
"code-review"
|
|
19
19
|
],
|
|
20
|
+
"main": "opencode/index.js",
|
|
20
21
|
"bin": {
|
|
21
|
-
"open-plan-annotator": "bin/open-plan-annotator.cjs"
|
|
22
|
-
"open-plan-annotator-install-opencode": "bin/open-plan-annotator-install-opencode.cjs"
|
|
22
|
+
"open-plan-annotator": "bin/open-plan-annotator.cjs"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"bin/open-plan-annotator.cjs",
|
|
26
|
-
"bin/open-plan-annotator-install-opencode.cjs",
|
|
27
26
|
"install.cjs",
|
|
28
27
|
".claude-plugin/",
|
|
29
|
-
"opencode
|
|
28
|
+
"opencode/",
|
|
30
29
|
"hooks/",
|
|
31
30
|
"CLAUDE.md",
|
|
32
|
-
"README.md"
|
|
33
|
-
"scripts/install-opencode-plugin.cjs"
|
|
31
|
+
"README.md"
|
|
34
32
|
],
|
|
35
33
|
"scripts": {
|
|
36
34
|
"postinstall": "node install.cjs",
|
|
@@ -46,7 +44,6 @@
|
|
|
46
44
|
"lint": "biome check .",
|
|
47
45
|
"lint:fix": "biome check --write .",
|
|
48
46
|
"format": "biome format --write .",
|
|
49
|
-
"install:opencode-plugin": "node scripts/install-opencode-plugin.cjs",
|
|
50
47
|
"do-release": "./scripts/release.sh",
|
|
51
48
|
"prepack": "mv CLAUDE.md CLAUDE.dev.md.bak && cp CLAUDE.plugin.md CLAUDE.md",
|
|
52
49
|
"postpack": "mv CLAUDE.dev.md.bak CLAUDE.md"
|
|
@@ -56,6 +53,7 @@
|
|
|
56
53
|
"@types/bun": "^1.3.9"
|
|
57
54
|
},
|
|
58
55
|
"dependencies": {
|
|
56
|
+
"@opencode-ai/plugin": "^0.0.3",
|
|
59
57
|
"@types/node": "^25.3.0"
|
|
60
58
|
}
|
|
61
59
|
}
|
package/opencode-plugin/index.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { type Plugin, tool } from "@opencode-ai/plugin";
|
|
5
|
-
|
|
6
|
-
function resolveRunner(): { command: string; args: string[] } {
|
|
7
|
-
const moduleDir = dirname(new URL(import.meta.url).pathname);
|
|
8
|
-
const repoRoot = join(moduleDir, "..");
|
|
9
|
-
const sourceServer = join(repoRoot, "server", "index.ts");
|
|
10
|
-
const packageBin = join(repoRoot, "bin", "open-plan-annotator.cjs");
|
|
11
|
-
const projectLocal = join(process.cwd(), "node_modules", ".bin", "open-plan-annotator");
|
|
12
|
-
|
|
13
|
-
if (existsSync(sourceServer) && process.env.OPEN_PLAN_ANNOTATOR_FORCE_BINARY !== "1") {
|
|
14
|
-
return { command: "bun", args: ["run", sourceServer] };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (existsSync(packageBin)) {
|
|
18
|
-
return { command: process.execPath, args: [packageBin] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (existsSync(projectLocal)) {
|
|
22
|
-
return { command: projectLocal, args: [] };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return { command: "open-plan-annotator", args: [] };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function execAsync(
|
|
29
|
-
command: string,
|
|
30
|
-
args: string[],
|
|
31
|
-
stdinData: string,
|
|
32
|
-
): Promise<{ stdout: string; stderr: string; status: number | null }> {
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
const child = execFile(
|
|
35
|
-
command,
|
|
36
|
-
args,
|
|
37
|
-
{ encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
|
|
38
|
-
(error, stdout, stderr) => {
|
|
39
|
-
resolve({
|
|
40
|
-
stdout: stdout ?? "",
|
|
41
|
-
stderr: stderr ?? "",
|
|
42
|
-
status: error ? 1 : 0,
|
|
43
|
-
});
|
|
44
|
-
},
|
|
45
|
-
);
|
|
46
|
-
if (child.stdin) {
|
|
47
|
-
child.stdin.write(stdinData);
|
|
48
|
-
child.stdin.end();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function normalizeOutput(
|
|
54
|
-
stdout: string,
|
|
55
|
-
stderr: string,
|
|
56
|
-
_status: number | null,
|
|
57
|
-
): { ok: boolean; decision: string; feedback?: string; message?: string } {
|
|
58
|
-
const text = (stdout ?? "").trim();
|
|
59
|
-
if (!text) {
|
|
60
|
-
return {
|
|
61
|
-
ok: false,
|
|
62
|
-
decision: "deny",
|
|
63
|
-
feedback: stderr || "open-plan-annotator returned empty output",
|
|
64
|
-
message: "Plan changes requested.",
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(text);
|
|
70
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
71
|
-
} catch {
|
|
72
|
-
// fall through
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
ok: false,
|
|
77
|
-
decision: "deny",
|
|
78
|
-
feedback: `Unexpected response from open-plan-annotator: ${text}`,
|
|
79
|
-
message: "Plan changes requested.",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const SYSTEM_INSTRUCTIONS = `
|
|
84
|
-
## Plan Submission
|
|
85
|
-
|
|
86
|
-
When you have completed your plan, you MUST call the \`submit_plan\` tool to submit it for user review.
|
|
87
|
-
The user will be able to:
|
|
88
|
-
- Review your plan visually in a dedicated UI
|
|
89
|
-
- Annotate specific sections with feedback
|
|
90
|
-
- Approve the plan to proceed with implementation
|
|
91
|
-
- Request changes with detailed feedback
|
|
92
|
-
|
|
93
|
-
If your plan is rejected, you will receive the user's annotated feedback. Revise your plan
|
|
94
|
-
based on their feedback and call submit_plan again.
|
|
95
|
-
|
|
96
|
-
Do NOT proceed with implementation until your plan is approved.
|
|
97
|
-
|
|
98
|
-
### When to Use Plan Mode
|
|
99
|
-
|
|
100
|
-
Use plan mode (and submit your plan for review) for any task involving:
|
|
101
|
-
- Creating or modifying more than 2 files
|
|
102
|
-
- Architectural or structural changes
|
|
103
|
-
- Anything the user hasn't explicitly described step-by-step
|
|
104
|
-
- Refactoring, migration, or feature additions
|
|
105
|
-
- Bug fixes that require investigation
|
|
106
|
-
|
|
107
|
-
For truly trivial tasks (fix a typo, rename a single variable, answer a factual question), you may skip plan submission.
|
|
108
|
-
|
|
109
|
-
### Plan Quality Standards
|
|
110
|
-
|
|
111
|
-
When writing a plan, include:
|
|
112
|
-
- A brief summary of what you understood the task to require
|
|
113
|
-
- The specific files you intend to create or modify and why
|
|
114
|
-
- Any assumptions you are making
|
|
115
|
-
- An explicit question if anything is ambiguous
|
|
116
|
-
`;
|
|
117
|
-
|
|
118
|
-
export const OpenPlanAnnotatorPlugin: Plugin = async (_ctx) => {
|
|
119
|
-
return {
|
|
120
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
121
|
-
output.system.push(SYSTEM_INSTRUCTIONS);
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
tool: {
|
|
125
|
-
submit_plan: tool({
|
|
126
|
-
description:
|
|
127
|
-
"Submit your completed plan for interactive user review. The user can annotate, approve, or request changes. Call this when you have finished creating your implementation plan.",
|
|
128
|
-
args: {
|
|
129
|
-
plan: tool.schema.string().describe("The complete implementation plan in markdown format"),
|
|
130
|
-
summary: tool.schema
|
|
131
|
-
.string()
|
|
132
|
-
.optional()
|
|
133
|
-
.describe("A brief 1-2 sentence summary of what the plan accomplishes"),
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
async execute(args) {
|
|
137
|
-
const runner = resolveRunner();
|
|
138
|
-
const payload = JSON.stringify({
|
|
139
|
-
host: "opencode",
|
|
140
|
-
command: "submit_plan",
|
|
141
|
-
plan: args.plan,
|
|
142
|
-
cwd: process.cwd(),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const result = await execAsync(runner.command, runner.args, payload);
|
|
146
|
-
const output = normalizeOutput(result.stdout, result.stderr, result.status);
|
|
147
|
-
|
|
148
|
-
if (output.ok && output.decision === "approve") {
|
|
149
|
-
return [
|
|
150
|
-
"Plan approved by the user. You may now proceed with implementation.",
|
|
151
|
-
args.summary ? `\nPlan Summary: ${args.summary}` : "",
|
|
152
|
-
].join("");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return [
|
|
156
|
-
"Plan needs revision.",
|
|
157
|
-
"",
|
|
158
|
-
"The user has requested changes to your plan. Please review their feedback below and revise your plan accordingly.",
|
|
159
|
-
"",
|
|
160
|
-
"## User Feedback",
|
|
161
|
-
"",
|
|
162
|
-
output.feedback ?? "Plan changes requested.",
|
|
163
|
-
"",
|
|
164
|
-
"---",
|
|
165
|
-
"",
|
|
166
|
-
"Please revise your plan based on this feedback and call `submit_plan` again when ready.",
|
|
167
|
-
].join("\n");
|
|
168
|
-
},
|
|
169
|
-
}),
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
export default OpenPlanAnnotatorPlugin;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "open-plan-annotator-opencode",
|
|
3
|
-
"version": "0.2.3",
|
|
4
|
-
"description": "Open Plan Annotator plugin for OpenCode - interactive plan review with visual annotation",
|
|
5
|
-
"author": "ndom91",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/ndom91/open-plan-annotator.git",
|
|
10
|
-
"directory": "opencode-plugin"
|
|
11
|
-
},
|
|
12
|
-
"main": "index.ts",
|
|
13
|
-
"files": [
|
|
14
|
-
"index.ts",
|
|
15
|
-
"package.json"
|
|
16
|
-
],
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@opencode-ai/plugin": "^1.0.218"
|
|
19
|
-
},
|
|
20
|
-
"peerDependencies": {
|
|
21
|
-
"bun": ">=1.0.0"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
|
|
6
|
-
function resolveTargetRoot() {
|
|
7
|
-
const fromArg = process.argv[2];
|
|
8
|
-
if (fromArg && fromArg.trim().length > 0) {
|
|
9
|
-
return path.resolve(process.cwd(), fromArg);
|
|
10
|
-
}
|
|
11
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(require("os").homedir(), ".config");
|
|
12
|
-
return path.join(configHome, "opencode", "plugins");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function ensureParentDir(dir) {
|
|
16
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function installPlugin(sourceDir, destinationDir) {
|
|
20
|
-
if (fs.existsSync(destinationDir)) {
|
|
21
|
-
fs.rmSync(destinationDir, { recursive: true, force: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
fs.symlinkSync(sourceDir, destinationDir, "dir");
|
|
26
|
-
return "symlink";
|
|
27
|
-
} catch {
|
|
28
|
-
fs.cpSync(sourceDir, destinationDir, { recursive: true });
|
|
29
|
-
return "copy";
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function main() {
|
|
34
|
-
const projectRoot = path.resolve(__dirname, "..");
|
|
35
|
-
const sourceDir = path.join(projectRoot, "opencode-plugin");
|
|
36
|
-
const targetRoot = resolveTargetRoot();
|
|
37
|
-
const destinationDir = path.join(targetRoot, "open-plan-annotator");
|
|
38
|
-
|
|
39
|
-
if (!fs.existsSync(sourceDir)) {
|
|
40
|
-
console.error(`open-plan-annotator: missing opencode-plugin at ${sourceDir}`);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
ensureParentDir(targetRoot);
|
|
45
|
-
const mode = installPlugin(sourceDir, destinationDir);
|
|
46
|
-
|
|
47
|
-
console.log(
|
|
48
|
-
`open-plan-annotator: installed OpenCode plugin (${mode}) at ${destinationDir}`,
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
// Install plugin dependencies using the first available package manager
|
|
52
|
-
const { execFileSync } = require("child_process");
|
|
53
|
-
const packageManagers = [
|
|
54
|
-
{ cmd: "bun", args: ["install"] },
|
|
55
|
-
{ cmd: "pnpm", args: ["install"] },
|
|
56
|
-
{ cmd: "npm", args: ["install"] },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
let installed = false;
|
|
60
|
-
for (const pm of packageManagers) {
|
|
61
|
-
try {
|
|
62
|
-
execFileSync(pm.cmd, pm.args, { cwd: destinationDir, stdio: "inherit" });
|
|
63
|
-
installed = true;
|
|
64
|
-
break;
|
|
65
|
-
} catch {
|
|
66
|
-
// Try the next package manager
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!installed) {
|
|
71
|
-
console.log(
|
|
72
|
-
"open-plan-annotator: could not install dependencies automatically. Run `npm install` in:",
|
|
73
|
-
destinationDir,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Create wrapper shim at plugins/open-plan-annotator.ts for OpenCode auto-discovery
|
|
78
|
-
// OpenCode scans plugins/*.{ts,js} but not subdirectories
|
|
79
|
-
const shimPath = path.join(targetRoot, "open-plan-annotator.ts");
|
|
80
|
-
fs.writeFileSync(
|
|
81
|
-
shimPath,
|
|
82
|
-
`export { default } from "./open-plan-annotator/index.ts";\n`,
|
|
83
|
-
);
|
|
84
|
-
console.log(`open-plan-annotator: created loader shim at ${shimPath}`);
|
|
85
|
-
|
|
86
|
-
console.log(
|
|
87
|
-
"\nThe plugin will be auto-discovered by OpenCode — no config changes needed.",
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
main();
|