@soffits/opencode-rummage 0.0.0-stage → 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/index.js +77 -0
- package/package.json +14 -4
- package/skills/rummage/SKILL.md +47 -0
- package/README.md +0 -3
package/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// OpenCode plugin for rummage: recall from the user's files on each prompt, plus the rummage skill.
|
|
2
|
+
//
|
|
3
|
+
// The prompt hook runs `rummage recall` once per prompt and keeps the block in the message's metadata, so it is
|
|
4
|
+
// stored with the message and never re-run. The context hook adds that block to its user message in every model
|
|
5
|
+
// request, so the system prompt stays the same and the request prefix stays cacheable. A recall that fails or
|
|
6
|
+
// takes longer than `timeout` adds nothing.
|
|
7
|
+
|
|
8
|
+
import { execFile } from "node:child_process"
|
|
9
|
+
import { readFileSync } from "node:fs"
|
|
10
|
+
|
|
11
|
+
const KEY = "rummage.recall"
|
|
12
|
+
|
|
13
|
+
function recall(command, args, prompt, sessionID, timeout) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const child = execFile(
|
|
16
|
+
command,
|
|
17
|
+
["recall", "--hook", ...args],
|
|
18
|
+
{ timeout, maxBuffer: 1 << 20, windowsHide: true },
|
|
19
|
+
(error, stdout) => {
|
|
20
|
+
if (error || !stdout.trim()) return resolve("")
|
|
21
|
+
try {
|
|
22
|
+
resolve(JSON.parse(stdout).hookSpecificOutput?.additionalContext ?? "")
|
|
23
|
+
} catch {
|
|
24
|
+
resolve("")
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
child.stdin.on("error", () => {})
|
|
29
|
+
child.stdin.end(JSON.stringify({ prompt, session_id: sessionID }))
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bundledSkill() {
|
|
34
|
+
try {
|
|
35
|
+
const path = new URL("./skills/rummage/SKILL.md", import.meta.url)
|
|
36
|
+
const text = readFileSync(path, "utf8")
|
|
37
|
+
const [, front = "", body = text] = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/) ?? []
|
|
38
|
+
const field = (name) => front.match(new RegExp(`^${name}:\\s*(.*)$`, "m"))?.[1].trim()
|
|
39
|
+
return { id: field("name") ?? "rummage", description: field("description"), path: path.pathname, content: body }
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default {
|
|
46
|
+
id: "rummage",
|
|
47
|
+
async setup(ctx) {
|
|
48
|
+
const options = ctx.options ?? {}
|
|
49
|
+
const command = options.command ?? "rummage"
|
|
50
|
+
const args = options.args ?? []
|
|
51
|
+
const timeout = options.timeout ?? 5000
|
|
52
|
+
|
|
53
|
+
await ctx.session.hook("prompt", async (event) => {
|
|
54
|
+
const text = event.prompt.text ?? ""
|
|
55
|
+
if (!text.trim()) return
|
|
56
|
+
const block = await recall(command, args, text, String(event.sessionID), timeout)
|
|
57
|
+
if (block) event.metadata = { ...event.metadata, [KEY]: block }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
await ctx.session.hook("context", (event) => {
|
|
61
|
+
event.messages.forEach((message, index) => {
|
|
62
|
+
const block = message.role === "user" && message.metadata?.[KEY]
|
|
63
|
+
if (typeof block !== "string") return
|
|
64
|
+
event.messages[index] = Object.assign(Object.create(Object.getPrototypeOf(message)), message, {
|
|
65
|
+
content: [...message.content, { type: "text", text: block }],
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const skill = options.skill === false ? undefined : bundledSkill()
|
|
71
|
+
if (skill) {
|
|
72
|
+
await ctx.skill.transform((editor) => {
|
|
73
|
+
if (!editor.get(skill.id)) editor.add({ ...skill, name: skill.id })
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soffits/opencode-rummage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin for rummage: recall from your own files on each prompt, and a skill for searching them.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"author": "Sakina",
|
|
7
|
+
"homepage": "https://github.com/soffits/rummage/blob/main/docs/integrations.md#opencode",
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/soffits/rummage.git", "directory": "integrations/opencode" },
|
|
9
|
+
"keywords": ["opencode", "opencode-plugin", "memory", "recall", "search"],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": { ".": "./index.js" },
|
|
12
|
+
"files": ["index.js", "skills"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prepack": "node -e \"require('fs').cpSync('../../skills/rummage', 'skills/rummage', { recursive: true })\""
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rummage
|
|
3
|
+
description: Search the user's own local files (notes, documents, PDFs, pictures, chats) by meaning or exact words with the rummage CLI, then open the passages it finds. Use when the answer may be in the user's files and you don't know where, or the exact words.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rummage
|
|
7
|
+
|
|
8
|
+
`rummage` keeps a local index of the user's files and finds things by what they mean and by the exact words in
|
|
9
|
+
them. It runs on the user's machine; nothing is sent anywhere.
|
|
10
|
+
|
|
11
|
+
## Search
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
rummage search "what the user is looking for, in their own words" --json -n 10
|
|
15
|
+
rummage search "INV-20931" --words-only --json # names, IDs, numbers: exact words, no model, instant
|
|
16
|
+
rummage search "..." --json --path notes/work --glob '*.md' -s notes # narrow by folder, pattern, source
|
|
17
|
+
rummage search "..." --json --since 2026-10-01 --until 2026-10-31 # a time window (dates you work out)
|
|
18
|
+
rummage search "Ana Souza" --json --sort newest -n 30 # the newest matches first
|
|
19
|
+
rummage search "..." --json --budget-tokens 1500 # shorter output
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The JSON has `status`:
|
|
23
|
+
|
|
24
|
+
- `ok`: `results` are ranked. `summary.strong` says how many score near the top; the rest are weak.
|
|
25
|
+
- `no_results`: nothing matched. Rephrase by meaning, try exact words with `--words-only`, or drop a scope
|
|
26
|
+
filter.
|
|
27
|
+
- `empty_index` or an error with status `index_missing`: nothing is indexed yet (`rummage index`).
|
|
28
|
+
|
|
29
|
+
Each result has `id`, `source`, `title`, `ref` (where it is), `lines` for text files, `score`, `snippet`.
|
|
30
|
+
`next_actions` holds the commands that show what the output left out (masked credentials, the rest of a partially
|
|
31
|
+
indexed file); the next query is yours to choose.
|
|
32
|
+
|
|
33
|
+
## Open what you found
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
rummage get '#3f2a9c01be' # the whole item's text
|
|
37
|
+
rummage get '#3f2a9c01be' --loc 1800 # only the matched passage (a result's `loc`)
|
|
38
|
+
rummage get 'notes/lease.md' --head 40
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For a chat result, `rummage get '#id' --around 3` shows the turns before and after it; for a file passage it shows
|
|
42
|
+
the lines around it. `rummage recent --minutes 30 --json` lists what was said or changed lately, newest first.
|
|
43
|
+
|
|
44
|
+
Credentials in results print as `[secret]`. Add `--show-secrets` only when the user asks for a value.
|
|
45
|
+
|
|
46
|
+
Quote the file and lines you relied on. A miss doesn't prove the thing isn't there: try other words, exact
|
|
47
|
+
names, or the folder directly before saying so.
|
package/README.md
DELETED