opencode-memsearch 0.4.0 → 0.5.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/package.json +1 -1
- package/scripts/cli.ts +34 -1
- package/scripts/lib.ts +75 -0
- package/scripts/reindex.ts +135 -0
- package/scripts/seed-memories.ts +9 -45
package/package.json
CHANGED
package/scripts/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { seed } from "./seed-memories"
|
|
12
|
+
import { reindex } from "./reindex"
|
|
12
13
|
|
|
13
14
|
const HELP = `opencode-memsearch — CLI utilities for the opencode-memsearch plugin
|
|
14
15
|
|
|
@@ -16,7 +17,8 @@ Usage:
|
|
|
16
17
|
opencode-memsearch <command> [options]
|
|
17
18
|
|
|
18
19
|
Commands:
|
|
19
|
-
seed
|
|
20
|
+
seed Backfill memory from existing OpenCode sessions
|
|
21
|
+
reindex Reset and rebuild vector index from existing memory files
|
|
20
22
|
|
|
21
23
|
Options:
|
|
22
24
|
--help, -h Show this help message
|
|
@@ -36,6 +38,23 @@ Options:
|
|
|
36
38
|
--days <n> Number of days of history to process (default: 14)
|
|
37
39
|
--help, -h Show this help message`
|
|
38
40
|
|
|
41
|
+
const REINDEX_HELP = `Reset and rebuild the vector index from existing memory files.
|
|
42
|
+
|
|
43
|
+
Discovers all project directories from the OpenCode session database,
|
|
44
|
+
resets each memsearch collection, and re-indexes the .memsearch/memory/
|
|
45
|
+
markdown files using the currently configured embedding provider.
|
|
46
|
+
|
|
47
|
+
This is useful after switching embedding providers (e.g. memsearch[local]
|
|
48
|
+
to memsearch[onnx]) — it rebuilds the vector index without re-running the
|
|
49
|
+
expensive LLM summarization from 'seed'.
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
opencode-memsearch reindex [--dry-run]
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--dry-run Preview what would be reset/reindexed without making changes
|
|
56
|
+
--help, -h Show this help message`
|
|
57
|
+
|
|
39
58
|
function parseSeedArgs(args: string[]): { days: number } {
|
|
40
59
|
let days = 14
|
|
41
60
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -50,6 +69,10 @@ function parseSeedArgs(args: string[]): { days: number } {
|
|
|
50
69
|
return { days }
|
|
51
70
|
}
|
|
52
71
|
|
|
72
|
+
function parseReindexArgs(args: string[]): { dryRun: boolean } {
|
|
73
|
+
return { dryRun: args.includes("--dry-run") }
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
async function main() {
|
|
54
77
|
const args = process.argv.slice(2)
|
|
55
78
|
const command = args[0]
|
|
@@ -70,6 +93,16 @@ async function main() {
|
|
|
70
93
|
await seed({ days })
|
|
71
94
|
break
|
|
72
95
|
}
|
|
96
|
+
case "reindex": {
|
|
97
|
+
const subArgs = args.slice(1)
|
|
98
|
+
if (subArgs.includes("--help") || subArgs.includes("-h")) {
|
|
99
|
+
console.log(REINDEX_HELP)
|
|
100
|
+
process.exit(0)
|
|
101
|
+
}
|
|
102
|
+
const { dryRun } = parseReindexArgs(subArgs)
|
|
103
|
+
await reindex({ dryRun })
|
|
104
|
+
break
|
|
105
|
+
}
|
|
73
106
|
default:
|
|
74
107
|
console.error(`Unknown command: ${command}`)
|
|
75
108
|
console.error()
|
package/scripts/lib.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib.ts — Shared utilities for opencode-memsearch CLI scripts.
|
|
3
|
+
*
|
|
4
|
+
* Contains helpers used by both the seed and reindex commands:
|
|
5
|
+
* database access, collection name derivation, memsearch detection, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Database } from "bun:sqlite"
|
|
9
|
+
import { createHash } from "crypto"
|
|
10
|
+
import { basename, join, resolve } from "path"
|
|
11
|
+
import { homedir } from "os"
|
|
12
|
+
import { $ } from "bun"
|
|
13
|
+
|
|
14
|
+
// --- Constants ---
|
|
15
|
+
|
|
16
|
+
export const DB_PATH = join(homedir(), ".local", "share", "opencode", "opencode.db")
|
|
17
|
+
|
|
18
|
+
// --- Database types ---
|
|
19
|
+
|
|
20
|
+
export interface DbSession {
|
|
21
|
+
id: string
|
|
22
|
+
directory: string
|
|
23
|
+
title: string
|
|
24
|
+
parent_id: string | null
|
|
25
|
+
time_created: number
|
|
26
|
+
time_updated: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- Helpers ---
|
|
30
|
+
|
|
31
|
+
export function deriveCollectionName(directory: string): string {
|
|
32
|
+
const abs = resolve(directory)
|
|
33
|
+
const sanitized = basename(abs)
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]/g, "_")
|
|
36
|
+
.replace(/_+/g, "_")
|
|
37
|
+
.replace(/^_|_$/g, "")
|
|
38
|
+
.slice(0, 40)
|
|
39
|
+
const hash = createHash("sha256").update(abs).digest("hex").slice(0, 8)
|
|
40
|
+
return `ms_${sanitized}_${hash}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Database access ---
|
|
44
|
+
|
|
45
|
+
export function listSessionsFromDb(db: Database, cutoffMs: number): DbSession[] {
|
|
46
|
+
return db.query<DbSession, [number]>(`
|
|
47
|
+
SELECT id, directory, title, parent_id, time_created, time_updated
|
|
48
|
+
FROM session
|
|
49
|
+
WHERE time_created >= ?
|
|
50
|
+
AND parent_id IS NULL
|
|
51
|
+
ORDER BY time_created ASC
|
|
52
|
+
`).all(cutoffMs)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function listDistinctDirectories(db: Database): string[] {
|
|
56
|
+
const rows = db.query<{ directory: string }, []>(`
|
|
57
|
+
SELECT DISTINCT directory
|
|
58
|
+
FROM session
|
|
59
|
+
WHERE parent_id IS NULL
|
|
60
|
+
ORDER BY directory ASC
|
|
61
|
+
`).all()
|
|
62
|
+
return rows.map((r) => r.directory)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Memsearch detection ---
|
|
66
|
+
|
|
67
|
+
export async function detectMemsearch(): Promise<string[]> {
|
|
68
|
+
try {
|
|
69
|
+
await $`which memsearch`.quiet()
|
|
70
|
+
return ["memsearch"]
|
|
71
|
+
} catch {}
|
|
72
|
+
throw new Error(
|
|
73
|
+
"memsearch is not installed. Install it by running: uv tool install 'memsearch[onnx]' — or with pip: pip install 'memsearch[onnx]'. See https://github.com/jdormit/opencode-memsearch for details."
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reindex.ts — Re-index all memsearch memory files from scratch.
|
|
3
|
+
*
|
|
4
|
+
* This module exports a `reindex` function used by the CLI (cli.ts).
|
|
5
|
+
*
|
|
6
|
+
* Useful when switching embedding providers (e.g. memsearch[local] -> memsearch[onnx])
|
|
7
|
+
* without needing to re-run the expensive LLM-based seed process. The memory markdown
|
|
8
|
+
* files are the source of truth; this command just rebuilds the vector index from them.
|
|
9
|
+
*
|
|
10
|
+
* What it does:
|
|
11
|
+
* 1. Discovers all project directories from the OpenCode SQLite database
|
|
12
|
+
* 2. For each project with existing .memsearch/memory/ files:
|
|
13
|
+
* a. Resets the memsearch collection (drops the old vector data)
|
|
14
|
+
* b. Re-indexes the memory markdown files with the current embedding provider
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Database } from "bun:sqlite"
|
|
18
|
+
import { readdir } from "fs/promises"
|
|
19
|
+
import { join } from "path"
|
|
20
|
+
import { $ } from "bun"
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
DB_PATH,
|
|
24
|
+
deriveCollectionName,
|
|
25
|
+
detectMemsearch,
|
|
26
|
+
listDistinctDirectories,
|
|
27
|
+
} from "./lib"
|
|
28
|
+
|
|
29
|
+
// --- Helpers ---
|
|
30
|
+
|
|
31
|
+
async function hasMemoryFiles(memoryDir: string): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
const files = await readdir(memoryDir)
|
|
34
|
+
return files.some((f) => f.endsWith(".md"))
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Main ---
|
|
41
|
+
|
|
42
|
+
export async function reindex(opts: { dryRun: boolean }) {
|
|
43
|
+
const { dryRun } = opts
|
|
44
|
+
|
|
45
|
+
if (dryRun) {
|
|
46
|
+
console.log("DRY RUN — no changes will be made.\n")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Setup
|
|
50
|
+
const memsearchCmd = await detectMemsearch()
|
|
51
|
+
console.log(`Using memsearch: ${memsearchCmd.join(" ")}`)
|
|
52
|
+
|
|
53
|
+
// Open database (read-only)
|
|
54
|
+
const db = new Database(DB_PATH, { readonly: true })
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Discover all project directories
|
|
58
|
+
const allDirs = listDistinctDirectories(db)
|
|
59
|
+
console.log(`Found ${allDirs.length} project directories in the OpenCode database.`)
|
|
60
|
+
console.log()
|
|
61
|
+
|
|
62
|
+
// Filter to directories that have memory files
|
|
63
|
+
const targets: { directory: string; memoryDir: string; collectionName: string }[] = []
|
|
64
|
+
|
|
65
|
+
for (const dir of allDirs) {
|
|
66
|
+
const memoryDir = join(dir, ".memsearch", "memory")
|
|
67
|
+
if (await hasMemoryFiles(memoryDir)) {
|
|
68
|
+
targets.push({
|
|
69
|
+
directory: dir,
|
|
70
|
+
memoryDir,
|
|
71
|
+
collectionName: deriveCollectionName(dir),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (targets.length === 0) {
|
|
77
|
+
console.log("No projects with memory files found. Nothing to reindex.")
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`Projects with memory files (${targets.length}):`)
|
|
82
|
+
for (const t of targets) {
|
|
83
|
+
console.log(` ${t.directory}`)
|
|
84
|
+
console.log(` collection: ${t.collectionName}`)
|
|
85
|
+
console.log(` memory dir: ${t.memoryDir}`)
|
|
86
|
+
}
|
|
87
|
+
console.log()
|
|
88
|
+
|
|
89
|
+
if (dryRun) {
|
|
90
|
+
console.log("DRY RUN — would reset and reindex the above collections.")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Reset and reindex each collection
|
|
95
|
+
let succeeded = 0
|
|
96
|
+
let failed = 0
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < targets.length; i++) {
|
|
99
|
+
const t = targets[i]
|
|
100
|
+
const label = `[${i + 1}/${targets.length}]`
|
|
101
|
+
|
|
102
|
+
console.log(`${label} ${t.directory}`)
|
|
103
|
+
|
|
104
|
+
// Reset the collection
|
|
105
|
+
console.log(`${label} Resetting collection ${t.collectionName}...`)
|
|
106
|
+
try {
|
|
107
|
+
const resetArgs = [...memsearchCmd, "reset", "--collection", t.collectionName, "--yes"]
|
|
108
|
+
await $`${resetArgs}`.nothrow().quiet()
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`${label} Failed to reset: ${err}`)
|
|
111
|
+
failed++
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Re-index the memory files
|
|
116
|
+
console.log(`${label} Indexing ${t.memoryDir}...`)
|
|
117
|
+
try {
|
|
118
|
+
const indexArgs = [...memsearchCmd, "index", t.memoryDir, "--collection", t.collectionName, "--force"]
|
|
119
|
+
const output = await $`${indexArgs}`.nothrow().quiet().text()
|
|
120
|
+
if (output.trim()) {
|
|
121
|
+
console.log(`${label} ${output.trim()}`)
|
|
122
|
+
}
|
|
123
|
+
succeeded++
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`${label} Failed to index: ${err}`)
|
|
126
|
+
failed++
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log()
|
|
131
|
+
console.log(`Reindex complete: ${succeeded} succeeded, ${failed} failed.`)
|
|
132
|
+
} finally {
|
|
133
|
+
db.close()
|
|
134
|
+
}
|
|
135
|
+
}
|
package/scripts/seed-memories.ts
CHANGED
|
@@ -12,12 +12,19 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { Database } from "bun:sqlite"
|
|
15
|
-
import { createHash } from "crypto"
|
|
16
15
|
import { appendFile, mkdir, readFile, writeFile, unlink } from "fs/promises"
|
|
17
|
-
import { join
|
|
16
|
+
import { join } from "path"
|
|
18
17
|
import { homedir, tmpdir } from "os"
|
|
19
18
|
import { $ } from "bun"
|
|
20
19
|
|
|
20
|
+
import {
|
|
21
|
+
DB_PATH,
|
|
22
|
+
DbSession,
|
|
23
|
+
deriveCollectionName,
|
|
24
|
+
detectMemsearch,
|
|
25
|
+
listSessionsFromDb,
|
|
26
|
+
} from "./lib"
|
|
27
|
+
|
|
21
28
|
// --- Configuration ---
|
|
22
29
|
|
|
23
30
|
interface PluginConfig {
|
|
@@ -73,23 +80,10 @@ Rules:
|
|
|
73
80
|
- Do NOT ask follow-up questions
|
|
74
81
|
- STOP immediately after the last bullet point`
|
|
75
82
|
|
|
76
|
-
const DB_PATH = join(homedir(), ".local", "share", "opencode", "opencode.db")
|
|
77
83
|
const TEMP_DIR = join(tmpdir(), "memsearch-seed")
|
|
78
84
|
|
|
79
85
|
// --- Helpers ---
|
|
80
86
|
|
|
81
|
-
function deriveCollectionName(directory: string): string {
|
|
82
|
-
const abs = resolve(directory)
|
|
83
|
-
const sanitized = basename(abs)
|
|
84
|
-
.toLowerCase()
|
|
85
|
-
.replace(/[^a-z0-9]/g, "_")
|
|
86
|
-
.replace(/_+/g, "_")
|
|
87
|
-
.replace(/^_|_$/g, "")
|
|
88
|
-
.slice(0, 40)
|
|
89
|
-
const hash = createHash("sha256").update(abs).digest("hex").slice(0, 8)
|
|
90
|
-
return `ms_${sanitized}_${hash}`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
87
|
function formatDate(epochMs: number): string {
|
|
94
88
|
const d = new Date(epochMs)
|
|
95
89
|
const yyyy = d.getFullYear()
|
|
@@ -105,15 +99,6 @@ function formatTime(epochMs: number): string {
|
|
|
105
99
|
|
|
106
100
|
// --- Database types ---
|
|
107
101
|
|
|
108
|
-
interface DbSession {
|
|
109
|
-
id: string
|
|
110
|
-
directory: string
|
|
111
|
-
title: string
|
|
112
|
-
parent_id: string | null
|
|
113
|
-
time_created: number
|
|
114
|
-
time_updated: number
|
|
115
|
-
}
|
|
116
|
-
|
|
117
102
|
interface DbMessage {
|
|
118
103
|
id: string
|
|
119
104
|
session_id: string
|
|
@@ -130,16 +115,6 @@ interface DbPart {
|
|
|
130
115
|
|
|
131
116
|
// --- Database access ---
|
|
132
117
|
|
|
133
|
-
function listSessionsFromDb(db: Database, cutoffMs: number): DbSession[] {
|
|
134
|
-
return db.query<DbSession, [number]>(`
|
|
135
|
-
SELECT id, directory, title, parent_id, time_created, time_updated
|
|
136
|
-
FROM session
|
|
137
|
-
WHERE time_created >= ?
|
|
138
|
-
AND parent_id IS NULL
|
|
139
|
-
ORDER BY time_created ASC
|
|
140
|
-
`).all(cutoffMs)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
118
|
function getSessionMessages(
|
|
144
119
|
db: Database,
|
|
145
120
|
sessionId: string,
|
|
@@ -290,17 +265,6 @@ function getUserText(turn: { info: any; parts: any[] }[]): string {
|
|
|
290
265
|
return ""
|
|
291
266
|
}
|
|
292
267
|
|
|
293
|
-
// Detect memsearch command
|
|
294
|
-
async function detectMemsearch(): Promise<string[]> {
|
|
295
|
-
try {
|
|
296
|
-
await $`which memsearch`.quiet()
|
|
297
|
-
return ["memsearch"]
|
|
298
|
-
} catch {}
|
|
299
|
-
throw new Error(
|
|
300
|
-
"memsearch is not installed. Install it by running: uv tool install 'memsearch[onnx]' — or with pip: pip install 'memsearch[onnx]'. See https://github.com/jdormit/opencode-memsearch for details."
|
|
301
|
-
)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
268
|
// Summarize a transcript via `opencode run`
|
|
305
269
|
async function summarizeWithOpencode(transcript: string, tempFile: string, model: string): Promise<string> {
|
|
306
270
|
// Write transcript to temp file
|