opencode-memsearch 0.3.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/README.md CHANGED
@@ -97,21 +97,27 @@ your-project/
97
97
 
98
98
  You should add `.memsearch/` to your `.gitignore`.
99
99
 
100
- ## Seed script
100
+ ## CLI
101
101
 
102
- The package includes a seed script that can backfill memory from existing OpenCode sessions. This is useful when first installing the plugin on a project you've already been working on.
102
+ The package includes a CLI for utility tasks. It requires [Bun](https://bun.sh/) to run.
103
103
 
104
- The seed script requires [Bun](https://bun.sh/) to run.
104
+ ```bash
105
+ bunx opencode-memsearch --help
106
+ ```
107
+
108
+ ### Seed
109
+
110
+ Backfill memory from existing OpenCode sessions. This is useful when first installing the plugin on a project you've already been working on.
105
111
 
106
112
  ```bash
107
113
  # Seed from the last 14 days of sessions (default)
108
- npx opencode-memsearch-seed
114
+ bunx opencode-memsearch seed
109
115
 
110
116
  # Seed from the last 30 days
111
- npx opencode-memsearch-seed --days 30
117
+ bunx opencode-memsearch seed --days 30
112
118
  ```
113
119
 
114
- Run the script from your project directory. It reads directly from the OpenCode SQLite database, summarizes each conversation turn, and writes the results to `.memsearch/memory/`. The seed script respects the same [configuration](#configuration) as the plugin (config file and environment variables).
120
+ The command reads directly from the OpenCode SQLite database, processes all sessions across all projects, summarizes each conversation turn, and writes the results to each project's `.memsearch/memory/` directory. It can be run from anywhere. The seed command respects the same [configuration](#configuration) as the plugin (config file and environment variables).
115
121
 
116
122
  ## Configuration
117
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-memsearch",
3
- "version": "0.3.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",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  },
14
14
  "bin": {
15
- "opencode-memsearch-seed": "scripts/seed-memories.ts"
15
+ "opencode-memsearch": "scripts/cli.ts"
16
16
  },
17
17
  "files": [
18
18
  "dist",
package/scripts/cli.ts ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * opencode-memsearch CLI — utilities for the opencode-memsearch plugin.
4
+ *
5
+ * Usage:
6
+ * bunx opencode-memsearch <command> [options]
7
+ *
8
+ * Requires Bun (https://bun.sh/) to run.
9
+ */
10
+
11
+ import { seed } from "./seed-memories"
12
+ import { reindex } from "./reindex"
13
+
14
+ const HELP = `opencode-memsearch — CLI utilities for the opencode-memsearch plugin
15
+
16
+ Usage:
17
+ opencode-memsearch <command> [options]
18
+
19
+ Commands:
20
+ seed Backfill memory from existing OpenCode sessions
21
+ reindex Reset and rebuild vector index from existing memory files
22
+
23
+ Options:
24
+ --help, -h Show this help message
25
+
26
+ Run 'opencode-memsearch <command> --help' for command-specific help.`
27
+
28
+ const SEED_HELP = `Seed memsearch memory files from recent OpenCode sessions.
29
+
30
+ Reads all sessions from the OpenCode SQLite database, summarizes each
31
+ conversation turn via an LLM, and writes the results to each project's
32
+ .memsearch/memory/ directory. Processes all projects; can be run from anywhere.
33
+
34
+ Usage:
35
+ opencode-memsearch seed [--days <n>]
36
+
37
+ Options:
38
+ --days <n> Number of days of history to process (default: 14)
39
+ --help, -h Show this help message`
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
+
58
+ function parseSeedArgs(args: string[]): { days: number } {
59
+ let days = 14
60
+ for (let i = 0; i < args.length; i++) {
61
+ if (args[i] === "--days" && args[i + 1]) {
62
+ days = parseInt(args[i + 1], 10)
63
+ if (isNaN(days) || days < 1) {
64
+ console.error("Invalid --days value, using default 14")
65
+ days = 14
66
+ }
67
+ }
68
+ }
69
+ return { days }
70
+ }
71
+
72
+ function parseReindexArgs(args: string[]): { dryRun: boolean } {
73
+ return { dryRun: args.includes("--dry-run") }
74
+ }
75
+
76
+ async function main() {
77
+ const args = process.argv.slice(2)
78
+ const command = args[0]
79
+
80
+ if (!command || command === "--help" || command === "-h") {
81
+ console.log(HELP)
82
+ process.exit(0)
83
+ }
84
+
85
+ switch (command) {
86
+ case "seed": {
87
+ const subArgs = args.slice(1)
88
+ if (subArgs.includes("--help") || subArgs.includes("-h")) {
89
+ console.log(SEED_HELP)
90
+ process.exit(0)
91
+ }
92
+ const { days } = parseSeedArgs(subArgs)
93
+ await seed({ days })
94
+ break
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
+ }
106
+ default:
107
+ console.error(`Unknown command: ${command}`)
108
+ console.error()
109
+ console.log(HELP)
110
+ process.exit(1)
111
+ }
112
+ }
113
+
114
+ main().catch((err) => {
115
+ console.error("Fatal error:", err)
116
+ process.exit(1)
117
+ })
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
+ }
@@ -1,13 +1,9 @@
1
- #!/usr/bin/env bun
2
1
  /**
3
2
  * seed-memories.ts — Seed memsearch memory files from recent OpenCode sessions.
4
3
  *
5
- * Usage:
6
- * npx opencode-memsearch-seed [--days 14]
4
+ * This module exports a `seed` function used by the CLI (cli.ts).
7
5
  *
8
- * Requires Bun (https://bun.sh/) to run.
9
- *
10
- * This script:
6
+ * What it does:
11
7
  * 1. Reads session + message data directly from the OpenCode SQLite database
12
8
  * 2. For each session, formats each conversation turn as a transcript
13
9
  * 3. Summarizes each turn via `opencode run` (model is configurable, see README)
@@ -16,12 +12,19 @@
16
12
  */
17
13
 
18
14
  import { Database } from "bun:sqlite"
19
- import { createHash } from "crypto"
20
15
  import { appendFile, mkdir, readFile, writeFile, unlink } from "fs/promises"
21
- import { join, basename, resolve } from "path"
16
+ import { join } from "path"
22
17
  import { homedir, tmpdir } from "os"
23
18
  import { $ } from "bun"
24
19
 
20
+ import {
21
+ DB_PATH,
22
+ DbSession,
23
+ deriveCollectionName,
24
+ detectMemsearch,
25
+ listSessionsFromDb,
26
+ } from "./lib"
27
+
25
28
  // --- Configuration ---
26
29
 
27
30
  interface PluginConfig {
@@ -77,23 +80,10 @@ Rules:
77
80
  - Do NOT ask follow-up questions
78
81
  - STOP immediately after the last bullet point`
79
82
 
80
- const DB_PATH = join(homedir(), ".local", "share", "opencode", "opencode.db")
81
83
  const TEMP_DIR = join(tmpdir(), "memsearch-seed")
82
84
 
83
85
  // --- Helpers ---
84
86
 
85
- function deriveCollectionName(directory: string): string {
86
- const abs = resolve(directory)
87
- const sanitized = basename(abs)
88
- .toLowerCase()
89
- .replace(/[^a-z0-9]/g, "_")
90
- .replace(/_+/g, "_")
91
- .replace(/^_|_$/g, "")
92
- .slice(0, 40)
93
- const hash = createHash("sha256").update(abs).digest("hex").slice(0, 8)
94
- return `ms_${sanitized}_${hash}`
95
- }
96
-
97
87
  function formatDate(epochMs: number): string {
98
88
  const d = new Date(epochMs)
99
89
  const yyyy = d.getFullYear()
@@ -107,32 +97,8 @@ function formatTime(epochMs: number): string {
107
97
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`
108
98
  }
109
99
 
110
- function parseArgs(): { days: number } {
111
- const args = process.argv.slice(2)
112
- let days = 14
113
- for (let i = 0; i < args.length; i++) {
114
- if (args[i] === "--days" && args[i + 1]) {
115
- days = parseInt(args[i + 1], 10)
116
- if (isNaN(days) || days < 1) {
117
- console.error("Invalid --days value, using default 14")
118
- days = 14
119
- }
120
- }
121
- }
122
- return { days }
123
- }
124
-
125
100
  // --- Database types ---
126
101
 
127
- interface DbSession {
128
- id: string
129
- directory: string
130
- title: string
131
- parent_id: string | null
132
- time_created: number
133
- time_updated: number
134
- }
135
-
136
102
  interface DbMessage {
137
103
  id: string
138
104
  session_id: string
@@ -149,16 +115,6 @@ interface DbPart {
149
115
 
150
116
  // --- Database access ---
151
117
 
152
- function listSessionsFromDb(db: Database, cutoffMs: number): DbSession[] {
153
- return db.query<DbSession, [number]>(`
154
- SELECT id, directory, title, parent_id, time_created, time_updated
155
- FROM session
156
- WHERE time_created >= ?
157
- AND parent_id IS NULL
158
- ORDER BY time_created ASC
159
- `).all(cutoffMs)
160
- }
161
-
162
118
  function getSessionMessages(
163
119
  db: Database,
164
120
  sessionId: string,
@@ -309,17 +265,6 @@ function getUserText(turn: { info: any; parts: any[] }[]): string {
309
265
  return ""
310
266
  }
311
267
 
312
- // Detect memsearch command
313
- async function detectMemsearch(): Promise<string[]> {
314
- try {
315
- await $`which memsearch`.quiet()
316
- return ["memsearch"]
317
- } catch {}
318
- throw new Error(
319
- "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."
320
- )
321
- }
322
-
323
268
  // Summarize a transcript via `opencode run`
324
269
  async function summarizeWithOpencode(transcript: string, tempFile: string, model: string): Promise<string> {
325
270
  // Write transcript to temp file
@@ -354,8 +299,8 @@ async function summarizeWithOpencode(transcript: string, tempFile: string, model
354
299
 
355
300
  // --- Main ---
356
301
 
357
- async function main() {
358
- const { days } = parseArgs()
302
+ export async function seed(opts: { days: number }) {
303
+ const { days } = opts
359
304
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000
360
305
 
361
306
  console.log(`Seeding memories from the last ${days} days...`)
@@ -514,7 +459,4 @@ async function main() {
514
459
  }
515
460
  }
516
461
 
517
- main().catch((err) => {
518
- console.error("Fatal error:", err)
519
- process.exit(1)
520
- })
462
+