@zilliz/memsearch-opencode 0.3.7 → 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 +108 -22
- package/package.json +4 -1
- package/scripts/capture-daemon.py +21 -0
- package/skills/memory-config/SKILL.md +27 -8
- package/skills/memory-to-skill/SKILL.md +9 -4
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";
|
|
@@ -70,11 +72,48 @@ function deriveCollectionName(projectDir: string): string {
|
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
74
|
* Summarize the N most recent daily .md files for cold-start context.
|
|
73
|
-
* Extracts
|
|
74
|
-
*
|
|
75
|
-
* what topics came up), not just the tail of whichever file is newest.
|
|
75
|
+
* Extracts recent non-empty session sections so empty SessionStart headings
|
|
76
|
+
* do not crowd out useful context.
|
|
76
77
|
*/
|
|
77
|
-
function
|
|
78
|
+
function recentMemoryPreviewLines(content: string, maxLines: number): string[] {
|
|
79
|
+
const sections: string[][] = [];
|
|
80
|
+
let current: string[] = [];
|
|
81
|
+
let hasBody = false;
|
|
82
|
+
|
|
83
|
+
const flush = () => {
|
|
84
|
+
if (current.length > 0 && hasBody) {
|
|
85
|
+
sections.push(current);
|
|
86
|
+
}
|
|
87
|
+
current = [];
|
|
88
|
+
hasBody = false;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const rawLine of content.split("\n")) {
|
|
92
|
+
const line = rawLine.trimEnd();
|
|
93
|
+
if (/^##\s/.test(line)) {
|
|
94
|
+
flush();
|
|
95
|
+
current = [line];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (/^#{3,4}\s/.test(line)) {
|
|
99
|
+
current.push(line);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (line.startsWith("- ") || line.startsWith("[User]") || line.startsWith("[Assistant]")) {
|
|
103
|
+
current.push(line);
|
|
104
|
+
hasBody = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
flush();
|
|
109
|
+
return sections.flat().slice(-maxLines);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isDailyJournalFile(file: string): boolean {
|
|
113
|
+
return /^\d{4}-\d{2}-\d{2}\.md$/.test(file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getRecentMemories(
|
|
78
117
|
memDir: string,
|
|
79
118
|
count = 2,
|
|
80
119
|
maxLinesPerFile = 30
|
|
@@ -82,7 +121,7 @@ function getRecentMemories(
|
|
|
82
121
|
if (!existsSync(memDir)) return "";
|
|
83
122
|
|
|
84
123
|
const files = readdirSync(memDir)
|
|
85
|
-
.filter(
|
|
124
|
+
.filter(isDailyJournalFile)
|
|
86
125
|
.sort()
|
|
87
126
|
.slice(-count);
|
|
88
127
|
|
|
@@ -92,9 +131,7 @@ function getRecentMemories(
|
|
|
92
131
|
for (const file of files) {
|
|
93
132
|
try {
|
|
94
133
|
const content = readFileSync(join(memDir, file), "utf-8");
|
|
95
|
-
const lines = content
|
|
96
|
-
.filter((l) => /^#{2,4}\s/.test(l) || l.startsWith("- ") || l.startsWith("[User]") || l.startsWith("[Assistant]"))
|
|
97
|
-
.slice(0, maxLinesPerFile);
|
|
134
|
+
const lines = recentMemoryPreviewLines(content, maxLinesPerFile);
|
|
98
135
|
if (lines.length > 0) {
|
|
99
136
|
summary.push(`[${file}]`, ...lines);
|
|
100
137
|
}
|
|
@@ -113,6 +150,34 @@ function shellEscape(s: string): string {
|
|
|
113
150
|
return s.replace(/'/g, "'\\''");
|
|
114
151
|
}
|
|
115
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
|
+
|
|
116
181
|
/**
|
|
117
182
|
* Start the capture daemon as a background process.
|
|
118
183
|
* The daemon polls OpenCode's SQLite for completed turns and writes to daily .md files.
|
|
@@ -122,6 +187,7 @@ function startCaptureDaemon(
|
|
|
122
187
|
collectionName: string,
|
|
123
188
|
memsearchCmd: string
|
|
124
189
|
): void {
|
|
190
|
+
const stateDir = join(projectDir, ".memsearch");
|
|
125
191
|
const pidFile = join(projectDir, ".memsearch", ".capture.pid");
|
|
126
192
|
const daemonScript = join(PLUGIN_DIR, "scripts", "capture-daemon.py");
|
|
127
193
|
|
|
@@ -136,21 +202,40 @@ function startCaptureDaemon(
|
|
|
136
202
|
return; // Already running
|
|
137
203
|
} catch {
|
|
138
204
|
// Process is dead, clean up stale PID file
|
|
205
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
139
206
|
}
|
|
140
207
|
}
|
|
141
208
|
} catch { /* ignore */ }
|
|
142
209
|
}
|
|
143
210
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
239
|
}
|
|
155
240
|
|
|
156
241
|
/**
|
|
@@ -349,9 +434,10 @@ const MemsearchPlugin: Plugin = async ({ project, directory, worktree }) => {
|
|
|
349
434
|
try {
|
|
350
435
|
const context = getRecentMemories(memoryDir);
|
|
351
436
|
if (context) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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);
|
|
355
441
|
}
|
|
356
442
|
} catch { /* ignore */ }
|
|
357
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
|
|
|
@@ -15,7 +15,7 @@ When this skill is triggered, inspect the user's request text. If there is no co
|
|
|
15
15
|
|
|
16
16
|
- Empty request or "check": diagnose current MemSearch setup.
|
|
17
17
|
- "Show/get setting": read the requested resolved/global/project value.
|
|
18
|
-
- "Set/enable/disable/change":
|
|
18
|
+
- "Set/enable/disable/change": choose global vs project scope explicitly; use global config for trusted plugin automation/provider/prompt/endpoint settings and project config only for allowlisted local indexing knobs.
|
|
19
19
|
- "Not capturing/search empty/no memory": troubleshoot files, config, and index health.
|
|
20
20
|
- "Use OpenAI/Gemini/Anthropic/native/model": configure provider routing.
|
|
21
21
|
- "PROJECT.md/USER.md/profile/review": configure advanced maintenance.
|
|
@@ -114,14 +114,33 @@ Config is resolved from built-in defaults, global config, project config, env re
|
|
|
114
114
|
|
|
115
115
|
Use `memsearch config list --resolved` for effective behavior, `--global` for global overrides, and `--project` for repository-specific overrides.
|
|
116
116
|
|
|
117
|
+
Since v0.4.11, project-local `.memsearch.toml` is restricted before it is merged.
|
|
118
|
+
Only these low-risk local indexing keys are honored from project config:
|
|
119
|
+
|
|
120
|
+
- `milvus.collection`
|
|
121
|
+
- `embedding.batch_size`
|
|
122
|
+
- `chunking.max_chunk_size`
|
|
123
|
+
- `chunking.overlap_lines`
|
|
124
|
+
- `watch.debounce_ms`
|
|
125
|
+
|
|
126
|
+
Trusted settings are ignored or rejected in project config. Put these in global
|
|
127
|
+
config (`~/.memsearch/config.toml`) or pass explicit CLI flags instead:
|
|
128
|
+
|
|
129
|
+
- provider/model/API endpoint/API key settings
|
|
130
|
+
- `[llm]` and `[llm.providers.*]`
|
|
131
|
+
- `[prompts]`
|
|
132
|
+
- plugin automation such as `plugins.opencode.project_review.enabled`,
|
|
133
|
+
`plugins.opencode.user_profile.enabled`, and
|
|
134
|
+
`plugins.opencode.memory_to_skill.enabled`
|
|
135
|
+
|
|
117
136
|
Default recommendation:
|
|
118
137
|
|
|
119
138
|
- Put reusable defaults in global config so users do not repeat setup in every project.
|
|
120
|
-
- Put only
|
|
121
|
-
- Use global config for named LLM providers, common model choices,
|
|
122
|
-
-
|
|
139
|
+
- Put only allowlisted local indexing overrides in project config.
|
|
140
|
+
- Use global config for named LLM providers, common model choices, plugin enable/disable switches, task intervals, install paths, and shared prompt defaults.
|
|
141
|
+
- If the user wants advanced maintenance enabled for all projects, set the plugin keys globally. The default relative `input_dir` / `output_file` values still resolve inside each current project.
|
|
123
142
|
|
|
124
|
-
Maintenance `input_dir` and `output_file` may be relative even when configured globally. They are resolved from the current project directory at runtime. For custom prompt paths, prefer absolute paths in global config
|
|
143
|
+
Maintenance `input_dir` and `output_file` may be relative even when configured globally. They are resolved from the current project directory at runtime, so a global `output_file = ".memsearch/PROJECT.md"` writes to each project's own `.memsearch/PROJECT.md`. For custom prompt paths, prefer absolute paths in global config; project prompt paths are not trusted.
|
|
125
144
|
|
|
126
145
|
OpenCode plugin keys:
|
|
127
146
|
|
|
@@ -193,7 +212,7 @@ If advanced maintenance or `memory_to_skill` seems silent, check
|
|
|
193
212
|
`.memsearch/.maintenance-state.json` for `<plugin>.<task>.last_error` and
|
|
194
213
|
`last_failed_at`; background hook errors may not surface in the chat.
|
|
195
214
|
|
|
196
|
-
Before enabling advanced maintenance, ask which provider to use, whether the default 24-hour interval is acceptable,
|
|
215
|
+
Before enabling advanced maintenance, ask which provider to use, whether the default 24-hour interval is acceptable, whether `.memsearch/PROJECT.md` / `.memsearch/USER.md` are acceptable output files, and whether the user wants the enablement global. Do not write plugin automation keys with `--project`; v0.4.11+ project config ignores or rejects them.
|
|
197
216
|
|
|
198
217
|
Prompt overrides:
|
|
199
218
|
|
|
@@ -207,8 +226,8 @@ memory_to_skill = ""
|
|
|
207
226
|
|
|
208
227
|
Empty prompt paths mean use the built-in MemSearch prompts. Custom prompt files may use `{{AGENT_NAME}}`, `{{TASK_NAME}}`, `{{PROJECT_DIR}}`, `{{INPUT_DIR}}`, and `{{OUTPUT_FILE}}`; the runner appends existing output, recent journals, and digest automatically.
|
|
209
228
|
|
|
210
|
-
Use `memsearch config set` for changes. After changing anything, show the command, the resolved value, and whether a new session is needed.
|
|
229
|
+
Use `memsearch config set` for changes. For trusted keys such as `plugins.*`, `[llm.providers.*]`, `[prompts]`, `embedding.provider`, or `milvus.uri`, set global config by omitting `--project`. Use `--project` only for allowlisted local indexing keys. After changing anything, show the command, the resolved value, and whether a new session is needed.
|
|
211
230
|
|
|
212
231
|
MemSearch TOML changes are read lazily by the CLI, capture daemon, and maintenance runner, so values such as `plugins.opencode.summarize.*`, `plugins.opencode.project_review.*`, `plugins.opencode.user_profile.*`, `[llm.providers.*]`, `[prompts]`, `milvus.*`, and `embedding.*` usually apply on the next capture, recall, index, or maintenance invocation. Restart OpenCode after `opencode.json` or plugin package changes; if capture behavior still looks stale after TOML edits, restart OpenCode or the capture daemon. In final diagnostic/change summaries, make clear that this is MemSearch memory configuration, not OpenCode's own memory/config system.
|
|
213
232
|
|
|
214
|
-
When useful, remind the user that they can either continue using this `memory-config` skill for guided configuration, or manually run `memsearch config init` for global interactive setup, `memsearch config init --project` for project
|
|
233
|
+
When useful, remind the user that they can either continue using this `memory-config` skill for guided configuration, or manually run `memsearch config init` for global interactive setup, `memsearch config init --project` for allowlisted project indexing setup, and `memsearch config set/get/list` for direct CLI changes.
|
|
@@ -90,14 +90,19 @@ The background pass mines automatically when enabled, starting from the summarie
|
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
memsearch config get plugins.opencode.memory_to_skill.enabled 2>/dev/null || echo "false"
|
|
93
|
-
# enable the background pass (do not enable silently)
|
|
94
|
-
memsearch config set plugins.opencode.memory_to_skill.enabled true
|
|
93
|
+
# enable the background pass globally (do not enable silently)
|
|
94
|
+
memsearch config set plugins.opencode.memory_to_skill.enabled true
|
|
95
95
|
# how eagerly history-mining distils (default 3; lower = more eager)
|
|
96
|
-
memsearch config set plugins.opencode.memory_to_skill.min_occurrences 3
|
|
96
|
+
memsearch config set plugins.opencode.memory_to_skill.min_occurrences 3
|
|
97
97
|
# pre-set install targets (otherwise you are asked at install time)
|
|
98
|
-
memsearch config set plugins.opencode.memory_to_skill.paths '[".agents/skills"]'
|
|
98
|
+
memsearch config set plugins.opencode.memory_to_skill.paths '[".agents/skills"]'
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
Since v0.4.11, project-local `.memsearch.toml` accepts only allowlisted local
|
|
102
|
+
indexing keys. Do not use `--project` for `plugins.*` settings such as
|
|
103
|
+
`memory_to_skill.enabled`, `min_occurrences`, or `paths`; put them in global
|
|
104
|
+
config instead.
|
|
105
|
+
|
|
101
106
|
Note: `enabled` only gates the **background** (session-end) pass. The explicit
|
|
102
107
|
commands above (`skills add`, `skills install`) always work, and you can mine history (C) directly.
|
|
103
108
|
|