@soffits/opencode-rummage 0.1.0 → 0.2.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/index.js +4 -3
- package/package.json +35 -6
- package/tui.js +108 -0
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync } from "node:fs"
|
|
|
10
10
|
|
|
11
11
|
const KEY = "rummage.recall"
|
|
12
12
|
|
|
13
|
-
function recall(command, args, prompt, sessionID, timeout) {
|
|
13
|
+
function recall(command, args, prompt, sessionID, cwd, timeout) {
|
|
14
14
|
return new Promise((resolve) => {
|
|
15
15
|
const child = execFile(
|
|
16
16
|
command,
|
|
@@ -26,7 +26,7 @@ function recall(command, args, prompt, sessionID, timeout) {
|
|
|
26
26
|
},
|
|
27
27
|
)
|
|
28
28
|
child.stdin.on("error", () => {})
|
|
29
|
-
child.stdin.end(JSON.stringify({ prompt, session_id: sessionID }))
|
|
29
|
+
child.stdin.end(JSON.stringify({ prompt, session_id: sessionID, cwd, harness: "opencode" }))
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -53,7 +53,8 @@ export default {
|
|
|
53
53
|
await ctx.session.hook("prompt", async (event) => {
|
|
54
54
|
const text = event.prompt.text ?? ""
|
|
55
55
|
if (!text.trim()) return
|
|
56
|
-
const
|
|
56
|
+
const cwd = ctx.location?.directory ?? process.cwd()
|
|
57
|
+
const block = await recall(command, args, text, String(event.sessionID), cwd, timeout)
|
|
57
58
|
if (block) event.metadata = { ...event.metadata, [KEY]: block }
|
|
58
59
|
})
|
|
59
60
|
|
package/package.json
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soffits/opencode-rummage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OpenCode plugin for rummage: recall from your own files on each prompt,
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "OpenCode plugin for rummage: recall from your own files on each prompt, a skill for searching them, and indexing progress in the footer.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Sakina",
|
|
7
7
|
"homepage": "https://github.com/soffits/rummage/blob/main/docs/integrations.md#opencode",
|
|
8
|
-
"repository": {
|
|
9
|
-
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/soffits/rummage.git",
|
|
11
|
+
"directory": "integrations/opencode"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"opencode",
|
|
15
|
+
"opencode-plugin",
|
|
16
|
+
"memory",
|
|
17
|
+
"recall",
|
|
18
|
+
"search"
|
|
19
|
+
],
|
|
10
20
|
"type": "module",
|
|
11
|
-
"exports": {
|
|
12
|
-
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./index.js",
|
|
23
|
+
"./tui": "./tui.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"index.js",
|
|
27
|
+
"tui.js",
|
|
28
|
+
"skills"
|
|
29
|
+
],
|
|
13
30
|
"scripts": {
|
|
14
31
|
"prepack": "node -e \"require('fs').cpSync('../../skills/rummage', 'skills/rummage', { recursive: true })\""
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@opentui/solid": ">=0.6.6",
|
|
35
|
+
"solid-js": ">=1.9.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@opentui/solid": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"solid-js": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
15
44
|
}
|
|
16
45
|
}
|
package/tui.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// OpenCode TUI part of the rummage plugin: shows rummage's index state in the footer while it is building, paused
|
|
2
|
+
// or broken, and a toast when indexing starts, finishes or fails.
|
|
3
|
+
//
|
|
4
|
+
// All state comes from `rummage status --line --json`, which reads one small progress file. Between runs this part
|
|
5
|
+
// only checks that file's modification time, so an idle index costs a stat every few seconds. Written as plain JS
|
|
6
|
+
// (no JSX): OpenCode does not transpile JSX in packages installed under node_modules.
|
|
7
|
+
|
|
8
|
+
import { execFile } from "node:child_process"
|
|
9
|
+
import { stat } from "node:fs/promises"
|
|
10
|
+
import { createElement, effect, insert, setProp } from "@opentui/solid"
|
|
11
|
+
import { createSignal } from "solid-js"
|
|
12
|
+
|
|
13
|
+
const CHECK_MS = 2000
|
|
14
|
+
const RECHECK_BUSY_MS = 30000
|
|
15
|
+
const FOOTER_CHARS = 60
|
|
16
|
+
const KEY = Symbol.for("rummage.opencode.tui")
|
|
17
|
+
|
|
18
|
+
function run(command, args, timeout) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
execFile(command, args, { timeout, maxBuffer: 1 << 20, windowsHide: true }, (error, stdout, stderr) =>
|
|
21
|
+
resolve({ ok: !error, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }),
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
id: "rummage.tui",
|
|
28
|
+
setup(context) {
|
|
29
|
+
// The TUI may set a plugin up again while an older instance lives; only the newest one polls and renders.
|
|
30
|
+
globalThis[KEY]?.()
|
|
31
|
+
const options = context.options ?? {}
|
|
32
|
+
const command = options.command ?? "rummage"
|
|
33
|
+
const [info, setInfo] = createSignal({ state: "complete", line: "" })
|
|
34
|
+
const started = Date.now()
|
|
35
|
+
let file, mtime = 0, checked = 0, timer, stopped = false
|
|
36
|
+
|
|
37
|
+
async function refresh() {
|
|
38
|
+
const result = await run(command, ["status", "--line", "--json"], 10000)
|
|
39
|
+
checked = Date.now()
|
|
40
|
+
if (!result.ok) return
|
|
41
|
+
let next
|
|
42
|
+
try {
|
|
43
|
+
next = JSON.parse(result.stdout)
|
|
44
|
+
} catch {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const before = info()
|
|
48
|
+
file = next.file ?? file
|
|
49
|
+
setInfo(next)
|
|
50
|
+
if (before.state !== next.state && checked > started + 1000) {
|
|
51
|
+
if (next.state === "complete" && before.state === "building")
|
|
52
|
+
context.ui.toast.show({ title: "rummage", message: "index complete", variant: "success" })
|
|
53
|
+
else if (next.state === "error" || next.state === "stopped")
|
|
54
|
+
context.ui.toast.show({ title: "rummage", message: next.note || next.line, variant: "warning" })
|
|
55
|
+
else if (next.state === "building" && before.state === "complete")
|
|
56
|
+
context.ui.toast.show({ title: "rummage", message: next.line, variant: "info" })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function tick() {
|
|
61
|
+
if (stopped) return
|
|
62
|
+
try {
|
|
63
|
+
const now = Date.now()
|
|
64
|
+
let changed = false
|
|
65
|
+
if (file) {
|
|
66
|
+
const seen = await stat(file).then((s) => s.mtimeMs, () => 0)
|
|
67
|
+
changed = seen !== mtime
|
|
68
|
+
mtime = seen
|
|
69
|
+
}
|
|
70
|
+
const busy = info().state !== "complete" || info().line
|
|
71
|
+
if (!file || changed || (busy && now - checked > RECHECK_BUSY_MS)) await refresh()
|
|
72
|
+
} finally {
|
|
73
|
+
if (!stopped) timer = setTimeout(tick, CHECK_MS)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void tick()
|
|
78
|
+
|
|
79
|
+
function label() {
|
|
80
|
+
const el = createElement("text")
|
|
81
|
+
insert(el, () => {
|
|
82
|
+
const line = info().line || ""
|
|
83
|
+
return line.length > FOOTER_CHARS ? line.slice(0, FOOTER_CHARS - 3) + "..." : line
|
|
84
|
+
})
|
|
85
|
+
effect((prev) => {
|
|
86
|
+
const bad = ["error", "stopped", "paused"].includes(info().state)
|
|
87
|
+
const theme = context.theme.text
|
|
88
|
+
return setProp(el, "fg", bad ? (theme.feedback?.warning?.base ?? theme.base) : theme.muted, prev)
|
|
89
|
+
})
|
|
90
|
+
return el
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const unslot = [
|
|
94
|
+
context.ui.slot({ append: "prompt.footer.status", render: label }),
|
|
95
|
+
context.ui.slot({ append: "home.footer.status", render: label }),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
const dispose = () => {
|
|
99
|
+
if (stopped) return
|
|
100
|
+
stopped = true
|
|
101
|
+
clearTimeout(timer)
|
|
102
|
+
for (const off of unslot) off?.()
|
|
103
|
+
if (globalThis[KEY] === dispose) delete globalThis[KEY]
|
|
104
|
+
}
|
|
105
|
+
globalThis[KEY] = dispose
|
|
106
|
+
return dispose
|
|
107
|
+
},
|
|
108
|
+
}
|