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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-memsearch",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Persistent cross-session memory for OpenCode, powered by memsearch",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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 Backfill memory from existing OpenCode sessions
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
+ }
@@ -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, basename, resolve } from "path"
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