@zilliz/memsearch-opencode 0.1.2 → 0.2.1

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
@@ -30,6 +30,8 @@ uv tool install 'memsearch[onnx]'
30
30
  }
31
31
  ```
32
32
 
33
+ > Windows note: the current plugin shells out to `bash` and `python3` helper scripts. Plain Windows installs are not supported yet; use WSL2 (recommended) or a POSIX-compatible shell such as Git Bash. See issue #387.
34
+
33
35
  ### Install from Source (development)
34
36
 
35
37
  ```bash
@@ -73,6 +75,17 @@ OpenCode Session
73
75
  └── memory_transcript ──→ parse-transcript.py (SQLite reader)
74
76
  ```
75
77
 
78
+ ## Verify It Works
79
+
80
+ After restarting OpenCode, chat for a few turns in a project and confirm memory capture is happening:
81
+
82
+ ```bash
83
+ ls .memsearch/memory/
84
+ cat .memsearch/memory/$(date +%Y-%m-%d).md
85
+ ```
86
+
87
+ If the plugin is active, daily markdown files should appear and grow as conversations finish.
88
+
76
89
  ## Recall Memories
77
90
 
78
91
  **Manual invocation** — explicitly invoke the skill with a query:
@@ -130,6 +143,14 @@ memsearch config set embedding.provider openai
130
143
  export OPENAI_API_KEY=sk-...
131
144
  ```
132
145
 
146
+ To override only the OpenCode capture summarization model:
147
+
148
+ ```bash
149
+ memsearch config set plugins.opencode.summarize.model anthropic/claude-haiku
150
+ ```
151
+
152
+ Leave it empty or unset to keep the current `small_model` / plugin default behavior. This setting does not fall back to `llm.model`.
153
+
133
154
  ## How It Works
134
155
 
135
156
  1. **Capture**: After each conversation turn, the plugin extracts the user+assistant exchange, summarizes it via LLM, and appends to a daily markdown file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zilliz/memsearch-opencode",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "memsearch plugin for OpenCode — semantic memory search across sessions",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -9,8 +9,10 @@
9
9
  },
10
10
  "files": [
11
11
  "index.ts",
12
- "scripts/",
12
+ "scripts/*.py",
13
+ "scripts/*.sh",
13
14
  "skills/",
15
+ "prompts/",
14
16
  "README.md"
15
17
  ],
16
18
  "keywords": [
@@ -0,0 +1,14 @@
1
+ You are a third-person note-taker. You will receive a transcript of ONE conversation turn between a human and {{AGENT_NAME}}.
2
+
3
+ Your job is to record what happened as factual third-person notes. You are an EXTERNAL OBSERVER — you are NOT {{AGENT_NAME}}, NOT an assistant. Do NOT answer the human's question, do NOT give suggestions, do NOT offer help. ONLY record what occurred.
4
+
5
+ Output 2-6 bullet points, each starting with '- '. NOTHING else.
6
+
7
+ Rules:
8
+ - Write in third person: 'User asked...', '{{AGENT_NAME}} read file X', '{{AGENT_NAME}} ran command Y'
9
+ - First bullet: what the user asked or wanted (one sentence)
10
+ - Remaining bullets: what was done — tools called, files read/edited, commands run, key findings
11
+ - Be specific: mention file names, function names, tool names, and concrete outcomes
12
+ - Do NOT answer the human's question yourself — just note what was discussed
13
+ - Do NOT add any text before or after the bullet points
14
+ - Write in the same language as the human's message in the transcript
@@ -11,19 +11,26 @@ extracts the last turn, summarizes it as bullet points, and writes to
11
11
  It also triggers memsearch indexing after writing.
12
12
  """
13
13
 
14
- import sqlite3
14
+ import argparse
15
+ import contextlib
15
16
  import json
16
- import sys
17
17
  import os
18
- import time
18
+ import shlex
19
19
  import shutil
20
- import subprocess
21
- import argparse
22
20
  import signal
21
+ import sqlite3
22
+ import subprocess
23
+ import sys
24
+ import time
23
25
  from datetime import datetime
24
26
  from pathlib import Path
25
27
 
26
28
 
29
+ def split_memsearch_cmd(memsearch_cmd):
30
+ """Split the configured memsearch command preserving quoted arguments."""
31
+ return shlex.split(memsearch_cmd)
32
+
33
+
27
34
  def get_small_model():
28
35
  """Read small_model from opencode.json config (fallback to model, then empty)."""
29
36
  config_paths = [
@@ -41,6 +48,20 @@ def get_small_model():
41
48
  return ""
42
49
 
43
50
 
51
+ def get_plugin_summarize_model(memsearch_cmd=None):
52
+ """Read the memsearch OpenCode summarize model override."""
53
+ if not memsearch_cmd:
54
+ return ""
55
+ try:
56
+ result = subprocess.run(
57
+ [*split_memsearch_cmd(memsearch_cmd), "config", "get", "plugins.opencode.summarize.model"],
58
+ capture_output=True, text=True, timeout=5,
59
+ )
60
+ return result.stdout.strip()
61
+ except Exception:
62
+ return ""
63
+
64
+
44
65
  def ensure_isolated_config():
45
66
  """Create isolated config dir without plugins/ to prevent recursion."""
46
67
  isolated = "/tmp/opencode-memsearch-summarize/opencode"
@@ -48,31 +69,56 @@ def ensure_isolated_config():
48
69
  # Copy opencode.json (provider config) but NOT plugins/
49
70
  src = os.path.expanduser("~/.config/opencode/opencode.json")
50
71
  dst = os.path.join(isolated, "opencode.json")
72
+ if os.path.islink(dst) or (os.path.lexists(dst) and not os.path.isfile(dst)):
73
+ with contextlib.suppress(OSError):
74
+ os.remove(dst)
51
75
  if os.path.exists(src) and not os.path.exists(dst):
52
76
  shutil.copy2(src, dst)
53
77
  return os.path.dirname(isolated) # /tmp/opencode-memsearch-summarize
54
78
 
55
79
 
56
- def summarize_with_llm(turn_text, small_model):
80
+ def _load_summarize_prompt(agent_name, memsearch_cmd=None):
81
+ """Load summarization prompt: user custom > plugin built-in > inline fallback."""
82
+ # Try user-custom prompt via config
83
+ if memsearch_cmd:
84
+ try:
85
+ result = subprocess.run(
86
+ [*split_memsearch_cmd(memsearch_cmd), "config", "get", "prompts.summarize"],
87
+ capture_output=True, text=True, timeout=5,
88
+ )
89
+ custom_path = result.stdout.strip()
90
+ if custom_path and os.path.isfile(custom_path):
91
+ template = Path(custom_path).read_text(encoding="utf-8")
92
+ return template.replace("{{AGENT_NAME}}", agent_name)
93
+ except Exception:
94
+ pass
95
+
96
+ # Plugin built-in template
97
+ builtin = Path(__file__).resolve().parent.parent / "prompts" / "summarize.txt"
98
+ if builtin.is_file():
99
+ template = builtin.read_text(encoding="utf-8")
100
+ return template.replace("{{AGENT_NAME}}", agent_name)
101
+
102
+ # Inline fallback
103
+ return (
104
+ "You are a third-person note-taker. Summarize the transcript as "
105
+ "2-6 bullet points. Write in third person. Output ONLY bullet points."
106
+ )
107
+
108
+
109
+ def summarize_with_llm(turn_text, small_model, memsearch_cmd=None):
57
110
  """Summarize using opencode run in isolated env (no plugins -> no recursion)."""
58
- # Keep the instruction short and put it AFTER the transcript to avoid
59
- # the LLM treating the instruction itself as the conversation content.
60
- # Clear delimiters (===) separate the transcript from the task.
111
+ system_prompt = _load_summarize_prompt("OpenCode", memsearch_cmd)
61
112
  full_prompt = (
62
- f"===TRANSCRIPT START===\n"
63
- f"{turn_text}\n"
64
- f"===TRANSCRIPT END===\n\n"
65
- f"Summarize the transcript above as 2-6 third-person bullet points (each starting with '- '). "
66
- f"Write 'User asked/did...' and 'OpenCode replied/did...'. "
67
- f"Be specific (file names, tools, outcomes). "
68
- f"Same language as the human. "
69
- f"Output ONLY bullet points, nothing else."
113
+ f"{system_prompt}\n\n"
114
+ f"Transcript:\n{turn_text}"
70
115
  )
71
116
  isolated_dir = ensure_isolated_config()
72
117
 
118
+ summarize_model = get_plugin_summarize_model(memsearch_cmd) or small_model
73
119
  cmd = ["opencode", "run"]
74
- if small_model:
75
- cmd += ["-m", small_model]
120
+ if summarize_model:
121
+ cmd += ["-m", summarize_model]
76
122
  cmd.append(full_prompt)
77
123
 
78
124
  try:
@@ -89,7 +135,7 @@ def summarize_with_llm(turn_text, small_model):
89
135
  output = result.stdout.strip()
90
136
  # Extract bullet points (skip any opencode run header lines)
91
137
  lines = output.split("\n")
92
- bullets = [l for l in lines if l.strip().startswith("- ")]
138
+ bullets = [line for line in lines if line.strip().startswith("- ")]
93
139
  if bullets:
94
140
  return "\n".join(bullets)
95
141
  except Exception:
@@ -239,7 +285,7 @@ def main():
239
285
 
240
286
  db_path = get_db_path()
241
287
  if not os.path.exists(db_path):
242
- print(f"OpenCode database not found at {db_path}", file=sys.stderr)
288
+ sys.stderr.write(f"OpenCode database not found at {db_path}\n")
243
289
  sys.exit(1)
244
290
 
245
291
  memory_dir = os.path.join(args.project_dir, ".memsearch", "memory")
@@ -252,10 +298,8 @@ def main():
252
298
 
253
299
  # Clean up on exit
254
300
  def cleanup(signum=None, frame=None):
255
- try:
301
+ with contextlib.suppress(OSError):
256
302
  os.remove(pid_file)
257
- except OSError:
258
- pass
259
303
  sys.exit(0)
260
304
 
261
305
  signal.signal(signal.SIGTERM, cleanup)
@@ -266,10 +310,8 @@ def main():
266
310
  state_file = os.path.join(args.project_dir, ".memsearch", ".last_msg_time")
267
311
  last_msg_time = 0
268
312
  if os.path.exists(state_file):
269
- try:
270
- last_msg_time = int(open(state_file).read().strip())
271
- except (ValueError, OSError):
272
- pass
313
+ with contextlib.suppress(ValueError, OSError), open(state_file) as f:
314
+ last_msg_time = int(f.read().strip())
273
315
 
274
316
  while True:
275
317
  try:
@@ -279,7 +321,7 @@ def main():
279
321
  for session_id, turn_text, msg_time in new_turns:
280
322
  if turn_text and len(turn_text) > 10:
281
323
  # Summarize with LLM, fallback to raw text
282
- summary = summarize_with_llm(turn_text, small_model)
324
+ summary = summarize_with_llm(turn_text, small_model, args.memsearch_cmd)
283
325
  write_capture(memory_dir, summary if summary else turn_text, session_id, db_path)
284
326
  if msg_time > last_msg_time:
285
327
  last_msg_time = msg_time