@zilliz/memsearch-opencode 0.3.6 → 0.3.10

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 CHANGED
@@ -70,10 +70,43 @@ function deriveCollectionName(projectDir: string): string {
70
70
 
71
71
  /**
72
72
  * Summarize the N most recent daily .md files for cold-start context.
73
- * Extracts headings (## Session, ### turns) and bullet content from each
74
- * file so the agent sees the structure of past days (what sessions existed,
75
- * what topics came up), not just the tail of whichever file is newest.
73
+ * Extracts recent non-empty session sections so empty SessionStart headings
74
+ * do not crowd out useful context.
76
75
  */
76
+ function recentMemoryPreviewLines(content: string, maxLines: number): string[] {
77
+ const sections: string[][] = [];
78
+ let current: string[] = [];
79
+ let hasBody = false;
80
+
81
+ const flush = () => {
82
+ if (current.length > 0 && hasBody) {
83
+ sections.push(current);
84
+ }
85
+ current = [];
86
+ hasBody = false;
87
+ };
88
+
89
+ for (const rawLine of content.split("\n")) {
90
+ const line = rawLine.trimEnd();
91
+ if (/^##\s/.test(line)) {
92
+ flush();
93
+ current = [line];
94
+ continue;
95
+ }
96
+ if (/^#{3,4}\s/.test(line)) {
97
+ current.push(line);
98
+ continue;
99
+ }
100
+ if (line.startsWith("- ") || line.startsWith("[User]") || line.startsWith("[Assistant]")) {
101
+ current.push(line);
102
+ hasBody = true;
103
+ }
104
+ }
105
+
106
+ flush();
107
+ return sections.flat().slice(-maxLines);
108
+ }
109
+
77
110
  function getRecentMemories(
78
111
  memDir: string,
79
112
  count = 2,
@@ -92,9 +125,7 @@ function getRecentMemories(
92
125
  for (const file of files) {
93
126
  try {
94
127
  const content = readFileSync(join(memDir, file), "utf-8");
95
- const lines = content.split("\n")
96
- .filter((l) => /^#{2,4}\s/.test(l) || l.startsWith("- ") || l.startsWith("[User]") || l.startsWith("[Assistant]"))
97
- .slice(0, maxLinesPerFile);
128
+ const lines = recentMemoryPreviewLines(content, maxLinesPerFile);
98
129
  if (lines.length > 0) {
99
130
  summary.push(`[${file}]`, ...lines);
100
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zilliz/memsearch-opencode",
3
- "version": "0.3.6",
3
+ "version": "0.3.10",
4
4
  "description": "memsearch plugin for OpenCode — semantic memory search across sessions",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -9,9 +9,12 @@ error). It is NOT a fact, a decision, or a preference — those belong in
9
9
  PROJECT.md and USER.md.
10
10
 
11
11
  You will receive recent memory journal entries and a list of existing skills
12
- (which you MAY revise). Propose or revise skills only when they clearly earn it.
12
+ (which you MAY revise). Propose or revise candidate skills when the journals show
13
+ the same user-requested operation repeating enough times to be useful.
13
14
 
14
- Be conservative. Quality over quantity. Most runs should return an empty list.
15
+ Candidates are review drafts, not installed skills. Do not enforce a fixed ratio,
16
+ quota, or expectation that most runs return nothing. The number of candidates
17
+ should follow the evidence in the journals.
15
18
 
16
19
  A NEW workflow qualifies as a candidate ONLY when ALL of these hold:
17
20
  - It is a repeatable multi-step procedure, not a one-off action or a fact.
@@ -19,9 +22,10 @@ A NEW workflow qualifies as a candidate ONLY when ALL of these hold:
19
22
  journals below.
20
23
  - It generalizes into a reusable capability. If it is tightly coupled to one
21
24
  specific ticket, one specific value, or a one-time condition, do NOT propose it.
22
- - The recent runs were not immediately corrected, abandoned, or undone (a
23
- workflow the user kept fighting is not a good skill).
24
25
  - It is not already covered by an existing skill.
26
+ - The overall procedure was not abandoned, undone, or contradicted by later
27
+ evidence. Do not reject a workflow merely because individual runs included
28
+ failed attempts, retries, or corrected commands.
25
29
 
26
30
  You may also REVISE an existing skill (re-emit it with the SAME name and a
27
31
  corrected body) when the recent journals show that procedure has genuinely
@@ -30,17 +34,21 @@ there is clear evidence of change; do not rewrite for style.
30
34
 
31
35
  Rules:
32
36
  - Return only a JSON object, with no prose outside JSON.
33
- - Use {"skills": []} when nothing in the journals qualifies. This is the common case.
37
+ - Use {"skills": []} when no repeated workflow qualifies.
34
38
  - For each candidate, write a clean, portable SKILL.md body in "body": step-by-step
35
39
  instructions an agent can follow. Do not include YAML frontmatter — only the
36
40
  markdown body. Keep it concise and concrete.
37
41
  - "name" must be a short lowercase slug (letters, digits, dashes), e.g. "run-tests".
38
- It becomes the skill's command name. Do not reuse an existing skill name.
42
+ It becomes the skill's command name. For new candidates, do not reuse an
43
+ existing skill name; for revisions, reuse the exact existing name.
39
44
  - "description" is one line: what the skill does AND when it should trigger.
40
45
  Lead with the verbs a user would actually type ("run", "deploy", "debug").
41
46
  - "occurrences" is how many distinct sessions/days you saw this workflow.
42
47
  - "sources" lists the journal file names the workflow was observed in.
43
48
  - Do not copy raw journal text wholesale into the body; abstract it into a procedure.
49
+ - If a workflow qualifies but some exact commands, paths, or flags are not
50
+ confirmed, still create the candidate. Keep those details general and add a
51
+ short "Before installing" verification step instead of inventing specifics.
44
52
 
45
53
  How to write a good skill body (this is what makes the skill usable):
46
54
  - Write imperative, numbered steps an agent can follow top to bottom.
@@ -66,6 +74,8 @@ Accuracy — confirm from the original, do not hallucinate:
66
74
  - If you cannot read the transcript or confirm a detail, capture only what the
67
75
  summary clearly states; keep the step general or omit it — a plausible-but-wrong
68
76
  command is worse than none. Never invent specifics.
77
+ - Do not drop the whole candidate only because a few low-level details remain
78
+ unconfirmed; make the uncertainty visible for human review before installation.
69
79
  - Prefer fewer, verified steps over a complete-looking but partly-guessed procedure.
70
80
 
71
81
  JSON examples:
@@ -459,6 +459,28 @@ def summarize_with_llm(
459
459
  return None
460
460
 
461
461
 
462
+ def wake_maintenance(project_dir: str) -> None:
463
+ """Start maintenance in a hook-disabled child environment."""
464
+ runner = Path(__file__).resolve().parent / "maintenance-runner.py"
465
+ subprocess.Popen(
466
+ [
467
+ "python3",
468
+ str(runner),
469
+ "--platform",
470
+ "opencode",
471
+ "--project-dir",
472
+ project_dir,
473
+ "--memsearch-dir",
474
+ os.path.join(project_dir, ".memsearch"),
475
+ ],
476
+ env={**os.environ, "MEMSEARCH_NO_WATCH": "1", "MEMSEARCH_DISABLE": "1"},
477
+ stdin=subprocess.DEVNULL,
478
+ stdout=subprocess.DEVNULL,
479
+ stderr=subprocess.DEVNULL,
480
+ start_new_session=True,
481
+ )
482
+
483
+
462
484
  def get_session_ids(conn: sqlite3.Connection, project_dir: str) -> list[str]:
463
485
  """Find OpenCode sessions that belong to the given project directory."""
464
486
  sessions = conn.execute(
@@ -790,16 +812,8 @@ def main() -> None:
790
812
  )
791
813
 
792
814
  if any_new:
793
- os.system(
794
- f"{args.memsearch_cmd} index '{memory_dir}' "
795
- f"--collection {args.collection_name} &"
796
- )
797
- os.system(
798
- f"python3 {shlex.quote(str(Path(__file__).resolve().parent / 'maintenance-runner.py'))} "
799
- f"--platform opencode "
800
- f"--project-dir {shlex.quote(args.project_dir)} "
801
- f"--memsearch-dir {shlex.quote(os.path.join(args.project_dir, '.memsearch'))} &"
802
- )
815
+ os.system(f"{args.memsearch_cmd} index '{memory_dir}' --collection {args.collection_name} &")
816
+ wake_maintenance(args.project_dir)
803
817
  except Exception:
804
818
  pass
805
819
  finally:
@@ -13,6 +13,7 @@ import contextlib
13
13
  import json
14
14
  import os
15
15
  import re
16
+ import shlex
16
17
  import shutil
17
18
  import subprocess
18
19
  import sys
@@ -264,7 +265,50 @@ def run_command(cmd: list[str], *, env: dict[str, str], cwd: Path, timeout: int)
264
265
  timeout=timeout,
265
266
  check=False,
266
267
  )
267
- return (result.stdout or result.stderr or "").strip()
268
+ stdout = (result.stdout or "").strip()
269
+ stderr = (result.stderr or "").strip()
270
+ if result.returncode != 0:
271
+ detail = "\n".join(part for part in (stdout, stderr) if part)
272
+ if len(detail) > 2000:
273
+ detail = detail[:1975] + "... [truncated]"
274
+ command = " ".join(shlex.quote(_describe_arg(part)) for part in cmd)
275
+ if len(command) > 300:
276
+ command = command[:275] + "... [truncated]"
277
+ message = f"Command failed ({result.returncode}): {command}"
278
+ if detail:
279
+ message = f"{message}\n{detail}"
280
+ raise RuntimeError(message)
281
+ return stdout or stderr
282
+
283
+
284
+ def _describe_arg(arg: str, limit: int = 120) -> str:
285
+ if len(arg) <= limit:
286
+ return arg
287
+ return f"<arg:{len(arg)} chars>"
288
+
289
+
290
+ _CLAUDE_SAFE_MODE_ARGS: list[str] | None = None
291
+
292
+
293
+ def claude_safe_mode_args() -> list[str]:
294
+ """Return Claude args that suppress hooks when supported."""
295
+ global _CLAUDE_SAFE_MODE_ARGS
296
+ if _CLAUDE_SAFE_MODE_ARGS is not None:
297
+ return _CLAUDE_SAFE_MODE_ARGS
298
+ try:
299
+ result = subprocess.run(
300
+ ["claude", "--help"],
301
+ capture_output=True,
302
+ text=True,
303
+ timeout=5,
304
+ check=False,
305
+ )
306
+ except (OSError, subprocess.SubprocessError):
307
+ _CLAUDE_SAFE_MODE_ARGS = []
308
+ else:
309
+ help_text = f"{result.stdout}\n{result.stderr}"
310
+ _CLAUDE_SAFE_MODE_ARGS = ["--safe-mode"] if "--safe-mode" in help_text else []
311
+ return _CLAUDE_SAFE_MODE_ARGS
268
312
 
269
313
 
270
314
  def extract_task_json_output(output: str) -> str:
@@ -376,7 +420,14 @@ def run_native_provider(ctx, prompt: str) -> str:
376
420
  env = {**os.environ, "MEMSEARCH_NO_WATCH": "1"}
377
421
 
378
422
  if ctx.platform == "claude-code":
379
- cmd = ["claude", "-p", "--strict-mcp-config", "--no-session-persistence", "--no-chrome"]
423
+ cmd = [
424
+ "claude",
425
+ "-p",
426
+ *claude_safe_mode_args(),
427
+ "--strict-mcp-config",
428
+ "--no-session-persistence",
429
+ "--no-chrome",
430
+ ]
380
431
  # Skill distillation needs to read original transcripts for exact commands.
381
432
  # Open a narrow hole — Bash limited to the two read-only memsearch drill
382
433
  # commands — only for that task; other maintenance keeps all tools off.
@@ -392,6 +443,7 @@ def run_native_provider(ctx, prompt: str) -> str:
392
443
  prompt,
393
444
  ]
394
445
  env["CLAUDECODE"] = ""
446
+ env["MEMSEARCH_DISABLE"] = "1"
395
447
  return run_command(cmd, env=env, cwd=ctx.project_dir, timeout=120)
396
448
 
397
449
  if ctx.platform == "codex":
@@ -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": make a safe project-scoped change after confirming ambiguous choices.
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 project-specific overrides in project config.
121
- - Use global config for named LLM providers, common model choices, normal input/output defaults, and shared prompt defaults.
122
- - Use project config for one repo's enable/disable switches, unusual output files, special intervals, or repo-specific providers.
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 and relative paths in project 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
 
@@ -189,7 +208,11 @@ Model guidance:
189
208
 
190
209
  Advanced maintenance runs after the plugin wakes it, only when enabled, journal input changed, and `min_interval_hours` elapsed. `PROJECT.md` and `USER.md` are maintenance artifacts by default and are not automatically indexed.
191
210
 
192
- Before enabling advanced maintenance, ask which provider to use, whether the default 24-hour interval is acceptable, and whether `.memsearch/PROJECT.md` / `.memsearch/USER.md` are acceptable output files.
211
+ If advanced maintenance or `memory_to_skill` seems silent, check
212
+ `.memsearch/.maintenance-state.json` for `<plugin>.<task>.last_error` and
213
+ `last_failed_at`; background hook errors may not surface in the chat.
214
+
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.
193
216
 
194
217
  Prompt overrides:
195
218
 
@@ -203,8 +226,8 @@ memory_to_skill = ""
203
226
 
204
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.
205
228
 
206
- 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.
207
230
 
208
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.
209
232
 
210
- 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 interactive setup, and `memsearch config set/get/list` for direct CLI changes.
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.
@@ -11,6 +11,8 @@ OpenCode's built-in skills system.
11
11
  Stages: **0** memory journals → **1** candidate (`.memsearch/skill-candidates/`,
12
12
  a git-tracked store that keeps evolving) → **2** installed (an agent skill dir).
13
13
  Candidates are never installed automatically; installing is always a human step.
14
+ User requests may stop at candidate creation/review, or continue to installation
15
+ in the same turn after explicit approval; match the requested stage.
14
16
 
15
17
  ## Intent routing
16
18
 
@@ -37,24 +39,35 @@ printf '%s' "## <title>\n\n1. ...\n2. ..." | memsearch skills add \
37
39
  ```
38
40
 
39
41
  `add` handles slugging, standard frontmatter, meta.json, and the git commit — no
40
- LLM is involved. Then show it to the user and install it (see **B**). Finally,
41
- check whether background distillation is on; if not, offer to enable it (so
42
- recurring workflows get captured automatically going forward) do not force it.
42
+ LLM is involved. Then show it to the user; install it only if the user asked for
43
+ that or explicitly approves (see **B**). Finally, check whether background
44
+ distillation is on; if not, offer to enable it (so recurring workflows get
45
+ captured automatically going forward) — do not force it.
43
46
 
44
47
  ## B. Review & install candidates (1→2)
45
48
 
46
49
  ```bash
47
50
  memsearch skills list # add -j for sources / installed paths
51
+ git -C .memsearch/skill-candidates log --oneline -5 2>/dev/null || true
48
52
  ```
49
53
 
50
54
  Before recommending or installing, skim the candidate's body: if a step looks uncertain or loosely summarized, re-check it against the source (open the transcript if needed) or flag it to the user and let them decide — installing copies the candidate as-is, so this is the last chance to catch a wrong step.
55
+ When showing candidates, mention the store's recent git history when it helps
56
+ explain whether a candidate is new, evolved, removed, or re-created.
51
57
 
52
- Pick one, resolve where to install (ask if unset), then install:
58
+ Treat installation as an interactive checkpoint. Show the candidate, apply any
59
+ requested tweaks before installing, and confirm the install destination with the
60
+ user. Resolve install targets from config first: if `paths` is a non-empty
61
+ list, present those paths as the proposed destinations and pass each entry as a
62
+ `--path` after confirmation. If it is empty, ask the user where to install; do
63
+ not silently fall back to a default path.
53
64
 
54
65
  ```bash
55
66
  memsearch config get plugins.opencode.memory_to_skill.paths 2>/dev/null || echo "[]"
56
- memsearch skills install <name> --path .agents/skills
67
+ memsearch skills install <name> --path <configured-or-user-approved-path>
57
68
  ```
69
+ After installation, remind the user to start a fresh agent session or reopen the
70
+ conversation so the newly installed skill is loaded.
58
71
 
59
72
  If the list is **empty**, background distillation is likely off or has not run.
60
73
  Offer the user a choice: capture from recent work now (**A**), distill from
@@ -71,20 +84,25 @@ generalize, not one-offs from a single day.
71
84
 
72
85
  **Drill into the original before drafting.** The journal bullets are a lossy summary; the exact commands, flags, and paths live in the original conversation. OpenCode stores transcripts in its session database (not a file), and the journal anchor carries a `session:` id (and `turn:` when present). Run `python3 __INSTALL_DIR__/scripts/parse-transcript.py <session_id>` (add `--turn <id>` when the anchor has one) to read the original turns with their tool calls — that is where the executed commands and output live. Write the skill from that. If the shown excerpt feels incomplete, skim nearby turns in the same original source before committing to exact commands or paths. If you cannot read it or confirm a detail, keep the step general or omit it — never fabricate.
73
86
 
74
- The background pass mines automatically when enabled, but it can only see the summaries; doing it here on demand lets you read the original transcripts, so the result is more accurate.
87
+ The background pass mines automatically when enabled, starting from the summaries; doing it here on demand lets you inspect the original transcripts more deliberately, so the result can be more accurate.
75
88
 
76
89
  ## D. Configure
77
90
 
78
91
  ```bash
79
92
  memsearch config get plugins.opencode.memory_to_skill.enabled 2>/dev/null || echo "false"
80
- # enable the background pass (do not enable silently)
81
- memsearch config set plugins.opencode.memory_to_skill.enabled true --project
93
+ # enable the background pass globally (do not enable silently)
94
+ memsearch config set plugins.opencode.memory_to_skill.enabled true
82
95
  # how eagerly history-mining distils (default 3; lower = more eager)
83
- memsearch config set plugins.opencode.memory_to_skill.min_occurrences 3 --project
96
+ memsearch config set plugins.opencode.memory_to_skill.min_occurrences 3
84
97
  # pre-set install targets (otherwise you are asked at install time)
85
- memsearch config set plugins.opencode.memory_to_skill.paths '[".agents/skills"]' --project
98
+ memsearch config set plugins.opencode.memory_to_skill.paths '[".agents/skills"]'
86
99
  ```
87
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
+
88
106
  Note: `enabled` only gates the **background** (session-end) pass. The explicit
89
107
  commands above (`skills add`, `skills install`) always work, and you can mine history (C) directly.
90
108