@zilliz/memsearch-opencode 0.3.10 → 0.3.11
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.ts +71 -16
- package/package.json +4 -1
- package/scripts/capture-daemon.py +21 -0
package/index.ts
CHANGED
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
15
15
|
import { tool } from "@opencode-ai/plugin";
|
|
16
|
-
import { execSync, exec, spawnSync } from "node:child_process";
|
|
16
|
+
import { execSync, exec, spawn, spawnSync } from "node:child_process";
|
|
17
17
|
import {
|
|
18
18
|
readFileSync,
|
|
19
19
|
existsSync,
|
|
20
20
|
mkdirSync,
|
|
21
21
|
readdirSync,
|
|
22
22
|
realpathSync,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
writeFileSync,
|
|
23
25
|
} from "node:fs";
|
|
24
26
|
import { join, dirname } from "node:path";
|
|
25
27
|
import { fileURLToPath } from "node:url";
|
|
@@ -107,7 +109,11 @@ function recentMemoryPreviewLines(content: string, maxLines: number): string[] {
|
|
|
107
109
|
return sections.flat().slice(-maxLines);
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
function
|
|
112
|
+
export function isDailyJournalFile(file: string): boolean {
|
|
113
|
+
return /^\d{4}-\d{2}-\d{2}\.md$/.test(file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getRecentMemories(
|
|
111
117
|
memDir: string,
|
|
112
118
|
count = 2,
|
|
113
119
|
maxLinesPerFile = 30
|
|
@@ -115,7 +121,7 @@ function getRecentMemories(
|
|
|
115
121
|
if (!existsSync(memDir)) return "";
|
|
116
122
|
|
|
117
123
|
const files = readdirSync(memDir)
|
|
118
|
-
.filter(
|
|
124
|
+
.filter(isDailyJournalFile)
|
|
119
125
|
.sort()
|
|
120
126
|
.slice(-count);
|
|
121
127
|
|
|
@@ -144,6 +150,34 @@ function shellEscape(s: string): string {
|
|
|
144
150
|
return s.replace(/'/g, "'\\''");
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
/** Marks the start of memsearch's injected block within a system message. */
|
|
154
|
+
export const MEMSEARCH_SYSTEM_MARKER = "[memsearch] Memory available.";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Merge memsearch's memory context into an `output.system` array without
|
|
158
|
+
* growing it. Some backends (litellm/vllm serving e.g. Qwen models) reject a
|
|
159
|
+
* multi-entry `output.system` array with "system message must be first", so
|
|
160
|
+
* the memory block is folded into the first entry instead of pushed as a new
|
|
161
|
+
* one. If a memsearch block from a previous transform call is already present
|
|
162
|
+
* (identified by MEMSEARCH_SYSTEM_MARKER), it is replaced in place rather than
|
|
163
|
+
* appended again, so repeated calls against the same output stay idempotent.
|
|
164
|
+
*/
|
|
165
|
+
export function mergeSystemMemoryContext(
|
|
166
|
+
system: string[] | undefined,
|
|
167
|
+
memoryText: string
|
|
168
|
+
): string[] {
|
|
169
|
+
if (!Array.isArray(system) || system.length === 0) {
|
|
170
|
+
return [memoryText];
|
|
171
|
+
}
|
|
172
|
+
const result = [...system];
|
|
173
|
+
const existing = result[0];
|
|
174
|
+
const markerIndex = existing.indexOf(MEMSEARCH_SYSTEM_MARKER);
|
|
175
|
+
const base =
|
|
176
|
+
markerIndex === -1 ? existing : existing.slice(0, markerIndex).replace(/\n+$/, "");
|
|
177
|
+
result[0] = base ? `${base}\n\n${memoryText}` : memoryText;
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
147
181
|
/**
|
|
148
182
|
* Start the capture daemon as a background process.
|
|
149
183
|
* The daemon polls OpenCode's SQLite for completed turns and writes to daily .md files.
|
|
@@ -153,6 +187,7 @@ function startCaptureDaemon(
|
|
|
153
187
|
collectionName: string,
|
|
154
188
|
memsearchCmd: string
|
|
155
189
|
): void {
|
|
190
|
+
const stateDir = join(projectDir, ".memsearch");
|
|
156
191
|
const pidFile = join(projectDir, ".memsearch", ".capture.pid");
|
|
157
192
|
const daemonScript = join(PLUGIN_DIR, "scripts", "capture-daemon.py");
|
|
158
193
|
|
|
@@ -167,21 +202,40 @@ function startCaptureDaemon(
|
|
|
167
202
|
return; // Already running
|
|
168
203
|
} catch {
|
|
169
204
|
// Process is dead, clean up stale PID file
|
|
205
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
170
206
|
}
|
|
171
207
|
}
|
|
172
208
|
} catch { /* ignore */ }
|
|
173
209
|
}
|
|
174
210
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
try {
|
|
212
|
+
mkdirSync(stateDir, { recursive: true });
|
|
213
|
+
const child = spawn(
|
|
214
|
+
"python3",
|
|
215
|
+
[
|
|
216
|
+
daemonScript,
|
|
217
|
+
projectDir,
|
|
218
|
+
collectionName,
|
|
219
|
+
"--memsearch-cmd",
|
|
220
|
+
memsearchCmd,
|
|
221
|
+
"--poll-interval",
|
|
222
|
+
"10",
|
|
223
|
+
"--parent-pid",
|
|
224
|
+
String(process.pid),
|
|
225
|
+
],
|
|
226
|
+
{
|
|
227
|
+
detached: true,
|
|
228
|
+
stdio: "ignore",
|
|
229
|
+
env: { ...process.env, MEMSEARCH_NO_WATCH: "1" },
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
child.unref();
|
|
233
|
+
if (child.pid) {
|
|
234
|
+
try { writeFileSync(pidFile, String(child.pid), "utf-8"); } catch { /* ignore */ }
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Capture is best-effort; tools still work without the background daemon.
|
|
238
|
+
}
|
|
185
239
|
}
|
|
186
240
|
|
|
187
241
|
/**
|
|
@@ -380,9 +434,10 @@ const MemsearchPlugin: Plugin = async ({ project, directory, worktree }) => {
|
|
|
380
434
|
try {
|
|
381
435
|
const context = getRecentMemories(memoryDir);
|
|
382
436
|
if (context) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
437
|
+
const memoryText =
|
|
438
|
+
`${MEMSEARCH_SYSTEM_MARKER} You have access to memory_search, ` +
|
|
439
|
+
`memory_get, and memory_transcript tools for recalling past sessions.\n\n${context}`;
|
|
440
|
+
output.system = mergeSystemMemoryContext(output.system, memoryText);
|
|
386
441
|
}
|
|
387
442
|
} catch { /* ignore */ }
|
|
388
443
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zilliz/memsearch-opencode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "memsearch plugin for OpenCode — semantic memory search across sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"semantic-search",
|
|
24
24
|
"milvus"
|
|
25
25
|
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test"
|
|
28
|
+
},
|
|
26
29
|
"author": "memsearch contributors",
|
|
27
30
|
"license": "MIT",
|
|
28
31
|
"peerDependencies": {
|
|
@@ -481,6 +481,21 @@ def wake_maintenance(project_dir: str) -> None:
|
|
|
481
481
|
)
|
|
482
482
|
|
|
483
483
|
|
|
484
|
+
def process_is_alive(pid: int) -> bool:
|
|
485
|
+
"""Return whether a process exists without sending it a signal."""
|
|
486
|
+
if pid <= 0:
|
|
487
|
+
return True
|
|
488
|
+
try:
|
|
489
|
+
os.kill(pid, 0)
|
|
490
|
+
except ProcessLookupError:
|
|
491
|
+
return False
|
|
492
|
+
except PermissionError:
|
|
493
|
+
return True
|
|
494
|
+
except OSError:
|
|
495
|
+
return True
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
|
|
484
499
|
def get_session_ids(conn: sqlite3.Connection, project_dir: str) -> list[str]:
|
|
485
500
|
"""Find OpenCode sessions that belong to the given project directory."""
|
|
486
501
|
sessions = conn.execute(
|
|
@@ -764,6 +779,7 @@ def main() -> None:
|
|
|
764
779
|
parser.add_argument("collection_name", help="Milvus collection name")
|
|
765
780
|
parser.add_argument("--memsearch-cmd", default="memsearch", help="memsearch command")
|
|
766
781
|
parser.add_argument("--poll-interval", type=int, default=10, help="Poll interval in seconds")
|
|
782
|
+
parser.add_argument("--parent-pid", type=int, default=0, help="Exit when this parent process is gone")
|
|
767
783
|
args = parser.parse_args()
|
|
768
784
|
|
|
769
785
|
db_path = get_db_path()
|
|
@@ -791,6 +807,9 @@ def main() -> None:
|
|
|
791
807
|
tail_turn_cache: dict[str, TailTurnObservation] = {}
|
|
792
808
|
|
|
793
809
|
while True:
|
|
810
|
+
if args.parent_pid and not process_is_alive(args.parent_pid):
|
|
811
|
+
cleanup()
|
|
812
|
+
|
|
794
813
|
any_new = False
|
|
795
814
|
conn = None
|
|
796
815
|
try:
|
|
@@ -820,6 +839,8 @@ def main() -> None:
|
|
|
820
839
|
if conn is not None:
|
|
821
840
|
conn.close()
|
|
822
841
|
|
|
842
|
+
if args.parent_pid and not process_is_alive(args.parent_pid):
|
|
843
|
+
cleanup()
|
|
823
844
|
time.sleep(args.poll_interval)
|
|
824
845
|
|
|
825
846
|
|