scroll-plugin 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/README.md +36 -0
- package/biome.json +9 -0
- package/bun.lock +31 -0
- package/index.js +151 -0
- package/index.test.js +32 -0
- package/package.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Scroll Plugin (OpenCode)
|
|
2
|
+
|
|
3
|
+
Unified OpenCode plugin for Scroll that handles both prompt capture (for RLM feedback) and inline skill/prompt suggestions.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
- Bun runtime (tested with 1.3+)
|
|
7
|
+
- `scroll` CLI available in PATH (or set `SCROLL_BIN`)
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
1. Copy the plugin into OpenCode plugins:
|
|
11
|
+
```bash
|
|
12
|
+
mkdir -p ~/.config/opencode/plugin
|
|
13
|
+
cp -r scroll-plugin ~/.config/opencode/plugin/
|
|
14
|
+
```
|
|
15
|
+
2. Ensure `opencode.json` includes the plugin:
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"plugin": ["scroll-plugin"],
|
|
19
|
+
"mcp": { "scroll": { "type": "local", "command": ["scroll", "mcp", "start"], "enabled": true } }
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
(running `scroll init --opencode` will add this automatically.)
|
|
23
|
+
|
|
24
|
+
## Environment
|
|
25
|
+
- `SCROLL_BIN`: override scroll binary path
|
|
26
|
+
- `SCROLL_SUGGEST_LIMIT`: suggestions per request (default 3)
|
|
27
|
+
- `SCROLL_SUGGEST_DEBOUNCE_MS`: debounce for TUI suggestions (default 800ms)
|
|
28
|
+
- `SCROLL_SUGGEST_PROVISION`: set `0` to skip MCP provisioning
|
|
29
|
+
- `SCROLL_PLUGIN_DRY_RUN`: set `1` to disable spawning scroll (useful for tests)
|
|
30
|
+
|
|
31
|
+
## Dev
|
|
32
|
+
```bash
|
|
33
|
+
bun install
|
|
34
|
+
bun run lint
|
|
35
|
+
bun test
|
|
36
|
+
```
|
package/biome.json
ADDED
package/bun.lock
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "scroll-plugin",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@biomejs/biome": "^1.7.3",
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
"packages": {
|
|
13
|
+
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
|
|
14
|
+
|
|
15
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
|
|
16
|
+
|
|
17
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
|
|
18
|
+
|
|
19
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
|
|
20
|
+
|
|
21
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
|
|
22
|
+
|
|
23
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
|
|
24
|
+
|
|
25
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
|
|
26
|
+
|
|
27
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
|
|
28
|
+
|
|
29
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
|
|
30
|
+
}
|
|
31
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Scroll OpenCode plugin: capture + suggest combined
|
|
2
|
+
|
|
3
|
+
export const ScrollPlugin = async ({ client, directory }) => {
|
|
4
|
+
const CONFIG = {
|
|
5
|
+
scrollBin: process.env.SCROLL_BIN || "scroll",
|
|
6
|
+
suggestLimit: Number.parseInt(process.env.SCROLL_SUGGEST_LIMIT || "3", 10),
|
|
7
|
+
debounceMs: Number.parseInt(
|
|
8
|
+
process.env.SCROLL_SUGGEST_DEBOUNCE_MS || "800",
|
|
9
|
+
10,
|
|
10
|
+
),
|
|
11
|
+
provision: (process.env.SCROLL_SUGGEST_PROVISION || "1") === "1",
|
|
12
|
+
dryRun: (process.env.SCROLL_PLUGIN_DRY_RUN || "0") === "1",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let promptCaptured = false;
|
|
16
|
+
let lastInput = "";
|
|
17
|
+
let lastInjected = "";
|
|
18
|
+
let timer = null;
|
|
19
|
+
let runId = 0;
|
|
20
|
+
|
|
21
|
+
const log = async (level, message, extra = {}) => {
|
|
22
|
+
try {
|
|
23
|
+
if (client?.app?.log) {
|
|
24
|
+
await client.app.log({
|
|
25
|
+
service: "scroll-plugin",
|
|
26
|
+
level,
|
|
27
|
+
message,
|
|
28
|
+
extra,
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
console.log(`[scroll-plugin:${level}]`, message, extra);
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
/* logging should never break the plugin */
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const execScroll = async (args, env = {}) => {
|
|
39
|
+
if (CONFIG.dryRun) return "";
|
|
40
|
+
|
|
41
|
+
const scrollEnv = { ...process.env, ...env, SCROLL_SOURCE: "opencode" };
|
|
42
|
+
|
|
43
|
+
const proc = Bun.spawn([CONFIG.scrollBin, ...args], {
|
|
44
|
+
env: scrollEnv,
|
|
45
|
+
stdout: "pipe",
|
|
46
|
+
stderr: "pipe",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const stdout = await new Response(proc.stdout).text();
|
|
50
|
+
const stderr = await new Response(proc.stderr).text();
|
|
51
|
+
|
|
52
|
+
if (proc.exitCode !== 0) {
|
|
53
|
+
throw new Error(stderr || `scroll exited with code ${proc.exitCode}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return stdout.trim();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const scheduleSuggest = (text) => {
|
|
60
|
+
lastInput = text;
|
|
61
|
+
runId += 1;
|
|
62
|
+
const current = runId;
|
|
63
|
+
|
|
64
|
+
if (timer) clearTimeout(timer);
|
|
65
|
+
|
|
66
|
+
timer = setTimeout(() => {
|
|
67
|
+
void (async () => {
|
|
68
|
+
const prompt = lastInput.trim();
|
|
69
|
+
if (!prompt) return;
|
|
70
|
+
if (current !== runId) return;
|
|
71
|
+
if (prompt === lastInjected) return;
|
|
72
|
+
|
|
73
|
+
await log("debug", "running scroll suggest", {
|
|
74
|
+
cwd: directory,
|
|
75
|
+
prompt_len: prompt.length,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const args = [
|
|
80
|
+
"suggest",
|
|
81
|
+
"skill",
|
|
82
|
+
prompt,
|
|
83
|
+
"--limit",
|
|
84
|
+
CONFIG.suggestLimit.toString(),
|
|
85
|
+
];
|
|
86
|
+
if (CONFIG.provision) args.push("--provision");
|
|
87
|
+
|
|
88
|
+
const suggestions = await execScroll(args);
|
|
89
|
+
if (!suggestions) return;
|
|
90
|
+
if (current !== runId) return;
|
|
91
|
+
|
|
92
|
+
lastInjected = prompt;
|
|
93
|
+
await client.tui.appendPrompt({
|
|
94
|
+
body: { text: `\n\n${suggestions}\n` },
|
|
95
|
+
});
|
|
96
|
+
} catch (e) {
|
|
97
|
+
await log("warn", "scroll suggest failed", { error: String(e) });
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
}, CONFIG.debounceMs);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"message.part.updated": async (input) => {
|
|
105
|
+
const role = input?.role;
|
|
106
|
+
const content = input?.content ?? "";
|
|
107
|
+
|
|
108
|
+
if (role === "user" && content?.trim() && !promptCaptured) {
|
|
109
|
+
promptCaptured = true;
|
|
110
|
+
try {
|
|
111
|
+
await execScroll(["capture", "hook", "prompt"], { PROMPT: content });
|
|
112
|
+
} catch (e) {
|
|
113
|
+
await log("warn", "capture prompt failed", { error: String(e) });
|
|
114
|
+
promptCaptured = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
"message.updated": async (input) => {
|
|
120
|
+
const role = input?.role;
|
|
121
|
+
const content = input?.content ?? "";
|
|
122
|
+
|
|
123
|
+
if (role === "assistant" && content?.trim() && promptCaptured) {
|
|
124
|
+
try {
|
|
125
|
+
await execScroll(["capture", "hook", "response"], {
|
|
126
|
+
CLAUDE_RESPONSE: content,
|
|
127
|
+
});
|
|
128
|
+
} catch (e) {
|
|
129
|
+
await log("warn", "capture response failed", { error: String(e) });
|
|
130
|
+
}
|
|
131
|
+
promptCaptured = false;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
"session.created": async (input) => {
|
|
136
|
+
await log("info", "session created", {
|
|
137
|
+
sessionId: input?.session?.id ?? "unknown",
|
|
138
|
+
isSubagent: input?.session?.parentId != null,
|
|
139
|
+
cwd: directory,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
"tui.prompt.append": async (input) => {
|
|
144
|
+
const text = input?.text ?? "";
|
|
145
|
+
if (!text.trim()) return;
|
|
146
|
+
scheduleSuggest(text);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default ScrollPlugin;
|
package/index.test.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import ScrollPlugin from "./index.js";
|
|
3
|
+
|
|
4
|
+
describe("ScrollPlugin", () => {
|
|
5
|
+
it("exposes handlers without executing scroll in dry-run", async () => {
|
|
6
|
+
process.env.SCROLL_PLUGIN_DRY_RUN = "1";
|
|
7
|
+
|
|
8
|
+
const appLogCalls = [];
|
|
9
|
+
const promptAppends = [];
|
|
10
|
+
|
|
11
|
+
const client = {
|
|
12
|
+
app: {
|
|
13
|
+
log: ({ level, message, extra }) =>
|
|
14
|
+
appLogCalls.push({ level, message, extra }),
|
|
15
|
+
},
|
|
16
|
+
tui: { appendPrompt: ({ body }) => promptAppends.push(body?.text ?? "") },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const plugin = await ScrollPlugin({ client, directory: "/tmp" });
|
|
20
|
+
|
|
21
|
+
expect(typeof plugin["message.part.updated"]).toBe("function");
|
|
22
|
+
expect(typeof plugin["message.updated"]).toBe("function");
|
|
23
|
+
expect(typeof plugin["tui.prompt.append"]).toBe("function");
|
|
24
|
+
|
|
25
|
+
await plugin["message.part.updated"]({ role: "user", content: "hello" });
|
|
26
|
+
await plugin["message.updated"]({ role: "assistant", content: "hi" });
|
|
27
|
+
await plugin["tui.prompt.append"]({ text: "prompt text" });
|
|
28
|
+
|
|
29
|
+
expect(promptAppends.length).toBeGreaterThanOrEqual(0);
|
|
30
|
+
expect(appLogCalls.length).toBeGreaterThanOrEqual(0);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scroll-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin for Scroll: capture + suggest in one module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"lint": "bunx biome check .",
|
|
10
|
+
"test": "bun test --no-bail"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@biomejs/biome": "^1.7.3"
|
|
14
|
+
}
|
|
15
|
+
}
|