@zilliz/memsearch-opencode 0.3.1 → 0.3.3
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 +1 -1
- package/index.ts +18 -3
- package/package.json +1 -1
- package/prompts/project_review.txt +42 -0
- package/prompts/summarize.txt +10 -9
- package/prompts/user_profile.txt +42 -0
- package/scripts/capture-daemon.py +32 -2
- package/scripts/maintenance-runner.py +260 -0
- package/scripts/opencode_turns.py +13 -35
- package/skills/memory-config/SKILL.md +203 -0
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ To use a memsearch-managed API provider instead:
|
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
160
|
memsearch config set llm.providers.openai.type openai
|
|
161
|
-
memsearch config set llm.providers.openai.model gpt-
|
|
161
|
+
memsearch config set llm.providers.openai.model gpt-5-mini
|
|
162
162
|
memsearch config set llm.providers.openai.api_key env:OPENAI_API_KEY
|
|
163
163
|
memsearch config set plugins.opencode.summarize.provider openai
|
|
164
164
|
```
|
package/index.ts
CHANGED
|
@@ -19,11 +19,12 @@ import {
|
|
|
19
19
|
existsSync,
|
|
20
20
|
mkdirSync,
|
|
21
21
|
readdirSync,
|
|
22
|
+
realpathSync,
|
|
22
23
|
} from "node:fs";
|
|
23
24
|
import { join, dirname } from "node:path";
|
|
24
25
|
import { fileURLToPath } from "node:url";
|
|
25
26
|
|
|
26
|
-
const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const PLUGIN_DIR = dirname(realpathSync(fileURLToPath(import.meta.url)));
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Helpers
|
|
@@ -92,7 +93,7 @@ function getRecentMemories(
|
|
|
92
93
|
try {
|
|
93
94
|
const content = readFileSync(join(memDir, file), "utf-8");
|
|
94
95
|
const lines = content.split("\n")
|
|
95
|
-
.filter((l) => /^#{2,4}\s/.test(l) || l.startsWith("- ") || l.startsWith("[
|
|
96
|
+
.filter((l) => /^#{2,4}\s/.test(l) || l.startsWith("- ") || l.startsWith("[User]") || l.startsWith("[Assistant]"))
|
|
96
97
|
.slice(0, maxLinesPerFile);
|
|
97
98
|
if (lines.length > 0) {
|
|
98
99
|
summary.push(`[${file}]`, ...lines);
|
|
@@ -167,6 +168,19 @@ function stopCaptureDaemon(projectDir: string): void {
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
function wakeMaintenance(projectDir: string, memsearchDir: string): void {
|
|
172
|
+
const runner = join(PLUGIN_DIR, "scripts", "maintenance-runner.py");
|
|
173
|
+
exec(
|
|
174
|
+
`python3 '${shellEscape(runner)}' --platform opencode ` +
|
|
175
|
+
`--project-dir '${shellEscape(projectDir)}' --memsearch-dir '${shellEscape(memsearchDir)}' &`,
|
|
176
|
+
{
|
|
177
|
+
timeout: 5000,
|
|
178
|
+
env: { ...process.env, MEMSEARCH_NO_WATCH: "1" },
|
|
179
|
+
},
|
|
180
|
+
() => { /* ignore */ }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
170
184
|
// ---------------------------------------------------------------------------
|
|
171
185
|
// Plugin entry
|
|
172
186
|
// ---------------------------------------------------------------------------
|
|
@@ -212,6 +226,7 @@ const MemsearchPlugin: Plugin = async ({ project, directory, worktree }) => {
|
|
|
212
226
|
// Start capture daemon for auto-capture
|
|
213
227
|
if (autoCapture) {
|
|
214
228
|
startCaptureDaemon(projectDir, collectionName, memsearchCmd);
|
|
229
|
+
wakeMaintenance(projectDir, memsearchDir);
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
return {
|
|
@@ -286,7 +301,7 @@ const MemsearchPlugin: Plugin = async ({ project, directory, worktree }) => {
|
|
|
286
301
|
"Retrieve the original conversation from a past OpenCode session. " +
|
|
287
302
|
"Use after memory_get when the expanded result contains a session anchor " +
|
|
288
303
|
"(<!-- session:ID turn:ID db:PATH -->). Returns the formatted " +
|
|
289
|
-
"dialogue with [
|
|
304
|
+
"dialogue with [User] and [Assistant] labels. When turn_id is present, " +
|
|
290
305
|
"the tool returns the target turn plus surrounding context.",
|
|
291
306
|
args: {
|
|
292
307
|
session_id: tool.schema.string().describe("The session ID from the anchor comment"),
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
You are maintaining a durable project memory file for {{AGENT_NAME}}.
|
|
2
|
+
|
|
3
|
+
Task: {{TASK_NAME}}
|
|
4
|
+
Project directory: {{PROJECT_DIR}}
|
|
5
|
+
Input memory directory: {{INPUT_DIR}}
|
|
6
|
+
Output file: {{OUTPUT_FILE}}
|
|
7
|
+
|
|
8
|
+
You will receive the current output file and recent memory journal entries. Update the output only when there is durable project information worth preserving.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Return only a JSON object, with no prose outside JSON.
|
|
12
|
+
- Use action "none" when the recent journals do not add durable project state.
|
|
13
|
+
- Use action "replace" when recent journals contain a durable project decision, risk, constraint, active thread, or next step that is not already represented in the existing output file.
|
|
14
|
+
- Treat journal lines explicitly labeled as project decision, project progress, project risk, project constraint, active thread, open question, or next step as project state unless they are clearly temporary noise.
|
|
15
|
+
- Recent progress should be recorded when it changes implementation status, validation status, release status, or an important debugging/testing behavior.
|
|
16
|
+
- If the existing output file is empty and recent journals contain any durable project state, use action "replace".
|
|
17
|
+
- Use action "replace" only when the output file should be replaced.
|
|
18
|
+
- The "content" field, when present, must be the full replacement Markdown file.
|
|
19
|
+
- Preserve useful existing content. Do not rewrite for style.
|
|
20
|
+
- Do not copy raw journals or raw transcript excerpts wholesale.
|
|
21
|
+
- Keep this file about the project, not the user. Put stable user preferences, communication style, and personal workflow notes in USER.md instead.
|
|
22
|
+
- Include user constraints only when they directly affect this project's execution.
|
|
23
|
+
- Do not include general user preferences such as preferred language, dependency manager, PR wording style, verification style, or communication style unless the project has explicitly decided to encode them as project requirements.
|
|
24
|
+
- Suggested sections are optional. Include only sections that are useful.
|
|
25
|
+
- Keep the file concise and reviewable.
|
|
26
|
+
- Prefer small targeted additions over broad rewrites when the existing output is already useful.
|
|
27
|
+
|
|
28
|
+
Suggested Markdown shape:
|
|
29
|
+
- # Project Memory
|
|
30
|
+
- ## Current Direction
|
|
31
|
+
- ## Active Threads
|
|
32
|
+
- ## Recent Progress
|
|
33
|
+
- ## Decisions
|
|
34
|
+
- ## Open Questions
|
|
35
|
+
- ## Risks and Constraints
|
|
36
|
+
- ## Next Steps
|
|
37
|
+
- ## Cold Items
|
|
38
|
+
|
|
39
|
+
JSON examples:
|
|
40
|
+
{"action":"none","reason":"Recent journals do not change durable project state."}
|
|
41
|
+
|
|
42
|
+
{"action":"replace","reason":"Recent journals add a new active thread.","content":"# Project Memory\n\n## Current Direction\n..."}
|
package/prompts/summarize.txt
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
You are a third-person note-taker. You will receive a transcript of ONE conversation turn between
|
|
1
|
+
You are a third-person note-taker. You will receive a transcript of ONE conversation turn between User and {{AGENT_NAME}}.
|
|
2
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
|
|
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 User's question, do NOT give suggestions, do NOT offer help. ONLY record what occurred.
|
|
4
4
|
|
|
5
|
-
Output 2-
|
|
5
|
+
Output 2-10 bullet points, each starting with '- '. NOTHING else.
|
|
6
|
+
Mandatory language rule: write every bullet in the same primary language as the [User] text. If User mixes languages, use the dominant user-facing language.
|
|
6
7
|
|
|
7
8
|
Rules:
|
|
8
|
-
- Write in third person
|
|
9
|
-
- First bullet: what
|
|
10
|
-
- Remaining bullets: what was done
|
|
11
|
-
- Be specific: mention
|
|
12
|
-
-
|
|
9
|
+
- Write in third person and call the user 'User'
|
|
10
|
+
- First bullet: what User asked or wanted (one sentence)
|
|
11
|
+
- Remaining bullets: what was done, found, changed, configured, tested, explained, decided, or could not be completed
|
|
12
|
+
- Be specific when useful: mention important files read or edited, searches or research performed, refactors, commands or tests run, key findings, and concrete outcomes
|
|
13
|
+
- Prefer the final user-visible outcome over low-level transcript mechanics
|
|
14
|
+
- Do NOT answer User's question yourself — just note what was discussed
|
|
13
15
|
- 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
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
You are maintaining a conservative user/workflow profile for {{AGENT_NAME}}.
|
|
2
|
+
|
|
3
|
+
Task: {{TASK_NAME}}
|
|
4
|
+
Project directory: {{PROJECT_DIR}}
|
|
5
|
+
Input memory directory: {{INPUT_DIR}}
|
|
6
|
+
Output file: {{OUTPUT_FILE}}
|
|
7
|
+
|
|
8
|
+
You will receive the current output file and recent memory journal entries. Update the output only when there is stable evidence about the user's durable preferences, priorities, constraints, or repeated workflows.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Return only a JSON object, with no prose outside JSON.
|
|
12
|
+
- Use action "none" when the recent journals do not add stable user/workflow information.
|
|
13
|
+
- Use action "replace" when recent journals contain a stable user preference, priority, constraint, communication style, or repeated workflow that is not already represented in the existing output file.
|
|
14
|
+
- If the existing output file is empty and recent journals contain stable user/workflow information, use action "replace".
|
|
15
|
+
- Use action "replace" only when the output file should be replaced.
|
|
16
|
+
- The "content" field, when present, must be the full replacement Markdown file.
|
|
17
|
+
- Preserve useful existing content. Do not rewrite for style.
|
|
18
|
+
- Do not infer broad personality traits from one-off messages.
|
|
19
|
+
- Keep this file about the user and reusable workflow patterns, not transient project status. Put active project threads and implementation state in PROJECT.md instead.
|
|
20
|
+
- Do not include project technical constraints, implementation decisions, file paths, bug fixes, or architecture decisions unless they are evidence of a repeated user preference across projects.
|
|
21
|
+
- Lines labeled as project decision, project progress, project risk, implementation note, or project constraint belong in PROJECT.md, not USER.md.
|
|
22
|
+
- Do not rewrite project decisions as "User prefers ...". Only include a preference when the journal explicitly describes a user preference, communication preference, workflow preference, or repeated behavior.
|
|
23
|
+
- Treat raw transcript excerpts, when provided, as stronger evidence for wording, style, and decision context.
|
|
24
|
+
- Do not copy raw journals or raw transcript excerpts wholesale.
|
|
25
|
+
- Suggested sections are optional. Include only sections that are useful.
|
|
26
|
+
- Keep the file concise and reviewable.
|
|
27
|
+
- Prefer small targeted additions over broad rewrites when the existing output is already useful.
|
|
28
|
+
|
|
29
|
+
Suggested Markdown shape:
|
|
30
|
+
- # User Memory
|
|
31
|
+
- ## Priorities
|
|
32
|
+
- ## Preferences
|
|
33
|
+
- ## Working Style
|
|
34
|
+
- ## Technical Defaults
|
|
35
|
+
- ## Communication Notes
|
|
36
|
+
- ## Repeated Workflows
|
|
37
|
+
- ## Constraints
|
|
38
|
+
|
|
39
|
+
JSON examples:
|
|
40
|
+
{"action":"none","reason":"Recent journals do not provide stable new user-profile evidence."}
|
|
41
|
+
|
|
42
|
+
{"action":"replace","reason":"Recent journals confirm a recurring workflow preference.","content":"# User Memory\n\n## Priorities\n..."}
|
|
@@ -40,7 +40,7 @@ from opencode_turns import (
|
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
_ANCHOR_RE = re.compile(r"<!-- session:([^ ]+) turn:([^ ]+) db:")
|
|
43
|
-
_TAIL_TURN_QUIET_PERIOD_MS =
|
|
43
|
+
_TAIL_TURN_QUIET_PERIOD_MS = int(os.environ.get("MEMSEARCH_OPENCODE_TAIL_QUIET_MS", "300000"))
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class TailTurnObservation:
|
|
@@ -106,6 +106,22 @@ def get_plugin_summarize_provider(memsearch_cmd: str | None = None) -> str:
|
|
|
106
106
|
return ""
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def get_plugin_summarize_enabled(memsearch_cmd: str | None = None) -> bool:
|
|
110
|
+
"""Read whether the memsearch OpenCode summarizer is enabled."""
|
|
111
|
+
if not memsearch_cmd:
|
|
112
|
+
return True
|
|
113
|
+
try:
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
[*split_memsearch_cmd(memsearch_cmd), "config", "get", "plugins.opencode.summarize.enabled"],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
timeout=5,
|
|
119
|
+
)
|
|
120
|
+
return result.stdout.strip().lower() != "false"
|
|
121
|
+
except Exception:
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
109
125
|
def ensure_isolated_config() -> str:
|
|
110
126
|
"""Create isolated config dir without plugins/ to prevent recursion."""
|
|
111
127
|
isolated = os.path.expanduser("~/.codex/tmp/opencode-memsearch-summarize/opencode")
|
|
@@ -148,12 +164,18 @@ def _load_summarize_prompt(agent_name: str, memsearch_cmd: str | None = None) ->
|
|
|
148
164
|
# Inline fallback
|
|
149
165
|
return (
|
|
150
166
|
"You are a third-person note-taker. Summarize the transcript as "
|
|
151
|
-
"2-
|
|
167
|
+
"2-10 bullet points. Write in third person. Do NOT answer User's question. "
|
|
168
|
+
"Mandatory language rule: write every bullet in the same primary language as the [User] text. "
|
|
169
|
+
"If User mixes languages, use the dominant user-facing language. "
|
|
170
|
+
"Output ONLY bullet points."
|
|
152
171
|
)
|
|
153
172
|
|
|
154
173
|
|
|
155
174
|
def summarize_with_llm(turn_text: str, small_model: str, memsearch_cmd: str | None = None) -> str | None:
|
|
156
175
|
"""Summarize using configured provider routing."""
|
|
176
|
+
if not get_plugin_summarize_enabled(memsearch_cmd):
|
|
177
|
+
return None
|
|
178
|
+
|
|
157
179
|
system_prompt = _load_summarize_prompt("OpenCode", memsearch_cmd)
|
|
158
180
|
full_prompt = f"{system_prompt}\n\nTranscript:\n{turn_text}"
|
|
159
181
|
|
|
@@ -467,6 +489,8 @@ def capture_session_turns(
|
|
|
467
489
|
# Check anchor first so that sidecar rebuilds repair state without
|
|
468
490
|
# calling the summarizer again for already-captured turns.
|
|
469
491
|
if not capture_exists(memory_dir, session_id, turn.turn_id):
|
|
492
|
+
if not get_plugin_summarize_enabled(memsearch_cmd):
|
|
493
|
+
continue
|
|
470
494
|
summary = summarize_with_llm(turn_text, small_model, memsearch_cmd)
|
|
471
495
|
write_capture(
|
|
472
496
|
memory_dir,
|
|
@@ -544,6 +568,12 @@ def main() -> None:
|
|
|
544
568
|
f"{args.memsearch_cmd} index '{memory_dir}' "
|
|
545
569
|
f"--collection {args.collection_name} &"
|
|
546
570
|
)
|
|
571
|
+
os.system(
|
|
572
|
+
f"python3 {shlex.quote(str(Path(__file__).resolve().parent / 'maintenance-runner.py'))} "
|
|
573
|
+
f"--platform opencode "
|
|
574
|
+
f"--project-dir {shlex.quote(args.project_dir)} "
|
|
575
|
+
f"--memsearch-dir {shlex.quote(os.path.join(args.project_dir, '.memsearch'))} &"
|
|
576
|
+
)
|
|
547
577
|
except Exception:
|
|
548
578
|
pass
|
|
549
579
|
finally:
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Plugin-local runner for MemSearch maintenance tasks.
|
|
3
|
+
|
|
4
|
+
This script belongs to the plugin layer. It handles host-native agent
|
|
5
|
+
invocations, while the Python package provides shared config, due-state,
|
|
6
|
+
prompt, and API-provider logic.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import contextlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
DEFAULT_NATIVE_MODELS = {
|
|
22
|
+
"claude-code": "sonnet",
|
|
23
|
+
"codex": "",
|
|
24
|
+
"opencode": "",
|
|
25
|
+
"openclaw": "",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_command(cmd: list[str], *, env: dict[str, str], cwd: Path, timeout: int) -> str:
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
cmd,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
env=env,
|
|
35
|
+
cwd=str(cwd),
|
|
36
|
+
timeout=timeout,
|
|
37
|
+
check=False,
|
|
38
|
+
)
|
|
39
|
+
return (result.stdout or result.stderr or "").strip()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_task_json_output(output: str) -> str:
|
|
43
|
+
"""Extract the first maintenance JSON object from noisy host output."""
|
|
44
|
+
for line in output.splitlines():
|
|
45
|
+
candidate = line.strip()
|
|
46
|
+
if not candidate.startswith("{"):
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
parsed = json.loads(candidate)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
continue
|
|
52
|
+
if isinstance(parsed, dict) and "action" in parsed:
|
|
53
|
+
return json.dumps(parsed, ensure_ascii=False)
|
|
54
|
+
return output
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ensure_opencode_isolated_config() -> Path:
|
|
58
|
+
home = Path.home()
|
|
59
|
+
isolated = home / ".codex" / "tmp" / "opencode-memsearch-maintenance" / "opencode"
|
|
60
|
+
isolated.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
src = home / ".config" / "opencode" / "opencode.json"
|
|
62
|
+
if src.is_file():
|
|
63
|
+
shutil.copy2(src, isolated / "opencode.json")
|
|
64
|
+
return isolated
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def ensure_memsearch_importable() -> None:
|
|
68
|
+
user_paths = [
|
|
69
|
+
str(Path.home() / ".local" / "bin"),
|
|
70
|
+
str(Path.home() / ".cargo" / "bin"),
|
|
71
|
+
str(Path.home() / "bin"),
|
|
72
|
+
"/usr/local/bin",
|
|
73
|
+
]
|
|
74
|
+
existing_path = os.environ.get("PATH", "")
|
|
75
|
+
path_parts = existing_path.split(os.pathsep) if existing_path else []
|
|
76
|
+
for user_path in reversed(user_paths):
|
|
77
|
+
if Path(user_path).is_dir() and user_path not in path_parts:
|
|
78
|
+
path_parts.insert(0, user_path)
|
|
79
|
+
os.environ["PATH"] = os.pathsep.join(path_parts)
|
|
80
|
+
|
|
81
|
+
for parent in Path(__file__).resolve().parents:
|
|
82
|
+
src_dir = parent / "src"
|
|
83
|
+
if (src_dir / "memsearch").is_dir():
|
|
84
|
+
sys.path.insert(0, str(src_dir))
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
import memsearch # noqa: F401
|
|
89
|
+
|
|
90
|
+
return
|
|
91
|
+
except ModuleNotFoundError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
if os.environ.get("MEMSEARCH_MAINTENANCE_UV_BOOTSTRAP") == "1":
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
memsearch_bin = shutil.which("memsearch")
|
|
98
|
+
if memsearch_bin:
|
|
99
|
+
with contextlib.suppress(OSError, UnicodeDecodeError):
|
|
100
|
+
first_line = Path(memsearch_bin).read_text(encoding="utf-8", errors="ignore").splitlines()[0]
|
|
101
|
+
if first_line.startswith("#!"):
|
|
102
|
+
python_bin = first_line[2:].strip().split()[0]
|
|
103
|
+
if python_bin:
|
|
104
|
+
os.execvpe(
|
|
105
|
+
python_bin,
|
|
106
|
+
[python_bin, str(Path(__file__).resolve()), *sys.argv[1:]],
|
|
107
|
+
{**os.environ, "MEMSEARCH_MAINTENANCE_UV_BOOTSTRAP": "1"},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
uv = shutil.which("uv")
|
|
111
|
+
if not uv:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
env = {**os.environ, "MEMSEARCH_MAINTENANCE_UV_BOOTSTRAP": "1"}
|
|
115
|
+
os.execvpe(
|
|
116
|
+
uv,
|
|
117
|
+
[
|
|
118
|
+
uv,
|
|
119
|
+
"run",
|
|
120
|
+
"--with",
|
|
121
|
+
"memsearch[onnx]",
|
|
122
|
+
"python",
|
|
123
|
+
str(Path(__file__).resolve()),
|
|
124
|
+
*sys.argv[1:],
|
|
125
|
+
],
|
|
126
|
+
env,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def apply_plugin_prompt_defaults(cfg) -> None:
|
|
131
|
+
plugin_dir = Path(__file__).resolve().parent.parent
|
|
132
|
+
prompts_dir = plugin_dir / "prompts"
|
|
133
|
+
for task in ("project_review", "user_profile"):
|
|
134
|
+
if getattr(cfg.prompts, task, ""):
|
|
135
|
+
continue
|
|
136
|
+
prompt_file = prompts_dir / f"{task}.txt"
|
|
137
|
+
if prompt_file.is_file():
|
|
138
|
+
setattr(cfg.prompts, task, str(prompt_file))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run_native_provider(ctx, prompt: str) -> str:
|
|
142
|
+
model = ctx.task_config.model or DEFAULT_NATIVE_MODELS.get(ctx.platform, "")
|
|
143
|
+
env = {**os.environ, "MEMSEARCH_NO_WATCH": "1"}
|
|
144
|
+
|
|
145
|
+
if ctx.platform == "claude-code":
|
|
146
|
+
cmd = ["claude", "-p", "--strict-mcp-config", "--no-session-persistence", "--no-chrome"]
|
|
147
|
+
if model:
|
|
148
|
+
cmd += ["--model", model]
|
|
149
|
+
cmd += [
|
|
150
|
+
"--system-prompt",
|
|
151
|
+
"You are a maintenance task runner. Output only the requested JSON object.",
|
|
152
|
+
prompt,
|
|
153
|
+
]
|
|
154
|
+
env["CLAUDECODE"] = ""
|
|
155
|
+
return run_command(cmd, env=env, cwd=ctx.project_dir, timeout=120)
|
|
156
|
+
|
|
157
|
+
if ctx.platform == "codex":
|
|
158
|
+
with tempfile.NamedTemporaryFile(prefix="memsearch-codex-maintenance-", suffix=".txt", delete=False) as output_file:
|
|
159
|
+
output_path = Path(output_file.name)
|
|
160
|
+
cmd = [
|
|
161
|
+
"codex",
|
|
162
|
+
"exec",
|
|
163
|
+
"--ephemeral",
|
|
164
|
+
"--skip-git-repo-check",
|
|
165
|
+
"-s",
|
|
166
|
+
"read-only",
|
|
167
|
+
"-c",
|
|
168
|
+
"features.hooks=false",
|
|
169
|
+
"-c",
|
|
170
|
+
'model_reasoning_effort="medium"',
|
|
171
|
+
"-o",
|
|
172
|
+
str(output_path),
|
|
173
|
+
]
|
|
174
|
+
profile = os.environ.get("MEMSEARCH_CODEX_PROFILE", "").strip()
|
|
175
|
+
if profile:
|
|
176
|
+
cmd += ["-p", profile]
|
|
177
|
+
if model:
|
|
178
|
+
cmd += ["-m", model]
|
|
179
|
+
cmd.append(prompt)
|
|
180
|
+
env["MEMSEARCH_IN_STOP_WORKER"] = "1"
|
|
181
|
+
try:
|
|
182
|
+
run_command(cmd, env=env, cwd=ctx.project_dir, timeout=120)
|
|
183
|
+
return output_path.read_text(encoding="utf-8", errors="replace").strip()
|
|
184
|
+
finally:
|
|
185
|
+
output_path.unlink(missing_ok=True)
|
|
186
|
+
|
|
187
|
+
if ctx.platform == "openclaw":
|
|
188
|
+
cmd = ["openclaw", "agent", "--local", "--session-id", "memsearch-maintenance"]
|
|
189
|
+
if model:
|
|
190
|
+
cmd += ["--model", model]
|
|
191
|
+
cmd += ["-m", prompt]
|
|
192
|
+
env["MEMSEARCH_DISABLE"] = "1"
|
|
193
|
+
return extract_task_json_output(run_command(cmd, env=env, cwd=ctx.project_dir, timeout=120))
|
|
194
|
+
|
|
195
|
+
if ctx.platform == "opencode":
|
|
196
|
+
isolated = ensure_opencode_isolated_config()
|
|
197
|
+
cmd = ["opencode", "run"]
|
|
198
|
+
if model:
|
|
199
|
+
cmd += ["-m", model]
|
|
200
|
+
cmd.append(prompt)
|
|
201
|
+
env["XDG_CONFIG_HOME"] = str(isolated)
|
|
202
|
+
env["XDG_DATA_HOME"] = str(isolated / "data")
|
|
203
|
+
return run_command(cmd, env=env, cwd=ctx.project_dir, timeout=120)
|
|
204
|
+
|
|
205
|
+
raise RuntimeError(f"Unsupported native maintenance platform {ctx.platform!r}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main() -> int:
|
|
209
|
+
parser = argparse.ArgumentParser(description="Run plugin-local MemSearch maintenance tasks.")
|
|
210
|
+
parser.add_argument("--platform", required=True, choices=["claude-code", "codex", "opencode", "openclaw"])
|
|
211
|
+
parser.add_argument("--project-dir", default=None)
|
|
212
|
+
parser.add_argument("--memsearch-dir", default=None)
|
|
213
|
+
parser.add_argument("--force", action="store_true")
|
|
214
|
+
parser.add_argument("--json-output", action="store_true")
|
|
215
|
+
args = parser.parse_args()
|
|
216
|
+
|
|
217
|
+
old_cwd = Path.cwd()
|
|
218
|
+
try:
|
|
219
|
+
ensure_memsearch_importable()
|
|
220
|
+
from memsearch.config import resolve_config
|
|
221
|
+
from memsearch.maintenance import run_due_tasks, run_task_llm
|
|
222
|
+
|
|
223
|
+
project_dir = Path(args.project_dir or old_cwd).expanduser().resolve()
|
|
224
|
+
os.chdir(project_dir)
|
|
225
|
+
cfg = resolve_config()
|
|
226
|
+
apply_plugin_prompt_defaults(cfg)
|
|
227
|
+
|
|
228
|
+
def llm_runner(ctx, prompt: str) -> str:
|
|
229
|
+
provider_name = (ctx.task_config.provider or "native").strip()
|
|
230
|
+
if provider_name in {"", "native"}:
|
|
231
|
+
return run_native_provider(ctx, prompt)
|
|
232
|
+
return run_task_llm(ctx, prompt, cfg)
|
|
233
|
+
|
|
234
|
+
results = run_due_tasks(
|
|
235
|
+
platform=args.platform,
|
|
236
|
+
project_dir=project_dir,
|
|
237
|
+
memsearch_dir=args.memsearch_dir,
|
|
238
|
+
cfg=cfg,
|
|
239
|
+
force=args.force,
|
|
240
|
+
llm_runner=llm_runner,
|
|
241
|
+
)
|
|
242
|
+
except (KeyError, RuntimeError, ValueError, subprocess.SubprocessError) as exc:
|
|
243
|
+
sys.stderr.write(f"Maintenance error: {exc}\n")
|
|
244
|
+
return 1
|
|
245
|
+
finally:
|
|
246
|
+
with contextlib.suppress(OSError):
|
|
247
|
+
os.chdir(old_cwd)
|
|
248
|
+
|
|
249
|
+
payload = [result.__dict__ for result in results]
|
|
250
|
+
if args.json_output:
|
|
251
|
+
sys.stdout.write(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
252
|
+
else:
|
|
253
|
+
for result in results:
|
|
254
|
+
detail = f": {result.reason}" if result.reason else ""
|
|
255
|
+
sys.stdout.write(f"{result.task}: {result.action}{detail}\n")
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
raise SystemExit(main())
|
|
@@ -17,7 +17,14 @@ from dataclasses import dataclass, field
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
def _tail_truncate(text: str, max_chars: int) -> str:
|
|
21
|
+
"""Keep the end of long text, where final conclusions usually appear."""
|
|
22
|
+
if len(text) <= max_chars:
|
|
23
|
+
return text
|
|
24
|
+
return "...(truncated to tail)\n" + text[-max_chars:]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
21
28
|
class OpenCodeMessage:
|
|
22
29
|
"""A single meaningful OpenCode message with rendered text."""
|
|
23
30
|
|
|
@@ -29,7 +36,7 @@ class OpenCodeMessage:
|
|
|
29
36
|
text: str
|
|
30
37
|
|
|
31
38
|
|
|
32
|
-
@dataclass
|
|
39
|
+
@dataclass
|
|
33
40
|
class OpenCodeTurn:
|
|
34
41
|
"""A user turn plus its assistant follow-up messages."""
|
|
35
42
|
|
|
@@ -51,16 +58,15 @@ class OpenCodeTurn:
|
|
|
51
58
|
f"=== Turn {self.turn_index} ({self.turn_id}) ===",
|
|
52
59
|
]
|
|
53
60
|
for message in self.messages:
|
|
54
|
-
label = "[
|
|
61
|
+
label = "[User]" if message.role == "user" else "[Assistant]"
|
|
55
62
|
text = message.text.strip()
|
|
56
|
-
|
|
57
|
-
text = text[:max_chars] + "\n..."
|
|
63
|
+
text = _tail_truncate(text, max_chars)
|
|
58
64
|
lines.append(f"{label}: {text}")
|
|
59
65
|
lines.append("")
|
|
60
66
|
return "\n".join(lines).strip()
|
|
61
67
|
|
|
62
68
|
|
|
63
|
-
@dataclass
|
|
69
|
+
@dataclass
|
|
64
70
|
class TurnState:
|
|
65
71
|
"""Persistent capture progress for a session."""
|
|
66
72
|
|
|
@@ -283,7 +289,6 @@ def extract_message_text(conn: sqlite3.Connection, message_id: str) -> str:
|
|
|
283
289
|
).fetchall()
|
|
284
290
|
|
|
285
291
|
text_parts: list[str] = []
|
|
286
|
-
tool_parts: list[str] = []
|
|
287
292
|
|
|
288
293
|
for part in parts:
|
|
289
294
|
try:
|
|
@@ -298,36 +303,9 @@ def extract_message_text(conn: sqlite3.Connection, message_id: str) -> str:
|
|
|
298
303
|
text = str(part_data["text"]).strip()
|
|
299
304
|
if text:
|
|
300
305
|
text_parts.append(text)
|
|
301
|
-
|
|
302
|
-
state = part_data.get("state", {})
|
|
303
|
-
status = state.get("status", "unknown")
|
|
304
|
-
tool_name = part_data.get("tool", "unknown")
|
|
305
|
-
|
|
306
|
-
if status == "completed":
|
|
307
|
-
tool_input = state.get("input", {})
|
|
308
|
-
tool_output = state.get("output", "")
|
|
309
|
-
if isinstance(tool_output, str) and len(tool_output) > 300:
|
|
310
|
-
tool_output = tool_output[:300] + "..."
|
|
311
|
-
|
|
312
|
-
input_summary = ""
|
|
313
|
-
if isinstance(tool_input, dict):
|
|
314
|
-
if "command" in tool_input:
|
|
315
|
-
input_summary = f" `{tool_input['command']}`"
|
|
316
|
-
elif "path" in tool_input:
|
|
317
|
-
input_summary = f" {tool_input['path']}"
|
|
318
|
-
elif "query" in tool_input:
|
|
319
|
-
input_summary = f" '{tool_input['query']}'"
|
|
320
|
-
|
|
321
|
-
tool_parts.append(
|
|
322
|
-
f"[Tool: {tool_name}{input_summary}] {tool_output}"
|
|
323
|
-
)
|
|
324
|
-
elif status == "error":
|
|
325
|
-
error = state.get("error", "unknown error")
|
|
326
|
-
tool_parts.append(f"[Tool: {tool_name}] Error: {error}")
|
|
306
|
+
# Skip tool parts; summaries use the readable User/Assistant text.
|
|
327
307
|
|
|
328
308
|
combined = "\n".join(text_parts).strip()
|
|
329
|
-
if tool_parts:
|
|
330
|
-
combined = "\n".join([combined, "\n".join(tool_parts)]).strip()
|
|
331
309
|
return combined
|
|
332
310
|
|
|
333
311
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-config
|
|
3
|
+
description: "Diagnose and configure MemSearch memory behavior for the OpenCode plugin. Use when the user asks about MemSearch configuration, plugin summarization, PROJECT.md/USER.md maintenance, memory directories, index health, provider routing, prompt files, or migration/compatibility questions."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a MemSearch configuration assistant for the OpenCode plugin. This skill manages MemSearch settings only. It is not OpenCode's built-in memory configuration.
|
|
7
|
+
|
|
8
|
+
In diagnostic summaries or final answers, state once that this is MemSearch
|
|
9
|
+
memory configuration, not OpenCode's own memory/config system. Do not prepend
|
|
10
|
+
that sentence to every progress update or every paragraph.
|
|
11
|
+
|
|
12
|
+
When this skill is triggered, inspect the user's request text. If there is no concrete request, run a diagnostic. If they ask for a specific setting or change, route the request using the flows below.
|
|
13
|
+
|
|
14
|
+
## Intent Routing
|
|
15
|
+
|
|
16
|
+
- Empty request or "check": diagnose current MemSearch setup.
|
|
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.
|
|
19
|
+
- "Not capturing/search empty/no memory": troubleshoot files, config, and index health.
|
|
20
|
+
- "Use OpenAI/Gemini/Anthropic/native/model": configure provider routing.
|
|
21
|
+
- "PROJECT.md/USER.md/profile/review": configure advanced maintenance.
|
|
22
|
+
- "Prompt": explain or configure prompt overrides.
|
|
23
|
+
|
|
24
|
+
Ask the user before enabling external or paid providers, changing output paths, re-indexing, deleting state, or broadening what gets indexed.
|
|
25
|
+
|
|
26
|
+
## Diagnose First
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
memsearch config list --resolved
|
|
30
|
+
memsearch config list --global
|
|
31
|
+
memsearch config list --project
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Check CLI and plugin versions before calling the setup healthy:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
memsearch --version
|
|
38
|
+
uv tool list --show-paths | rg -n 'memsearch|Package|Installed|path'
|
|
39
|
+
curl -fsSL https://pypi.org/pypi/memsearch/json \
|
|
40
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["info"]["version"])'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If `memsearch` is unavailable, try `uvx --from memsearch[onnx] memsearch --version`.
|
|
44
|
+
|
|
45
|
+
MemSearch has one shared Python CLI and platform plugins that may be installed
|
|
46
|
+
from different channels:
|
|
47
|
+
|
|
48
|
+
- CLI latest version comes from PyPI package `memsearch`. Update with
|
|
49
|
+
`uv tool install -U "memsearch[onnx]"` or `uv tool upgrade memsearch`.
|
|
50
|
+
- Codex plugin has no independent package/version file. Inspect
|
|
51
|
+
`${CODEX_HOME:-$HOME/.codex}/hooks.json` to find the hook source path, then
|
|
52
|
+
compare that repository with the latest `zilliztech/memsearch` GitHub release:
|
|
53
|
+
`git -C <memsearch-repo> describe --tags --always --dirty` and
|
|
54
|
+
`gh release view --repo zilliztech/memsearch --json tagName,publishedAt,url`.
|
|
55
|
+
Update source installs with `git pull` plus
|
|
56
|
+
`bash plugins/codex/scripts/install.sh`.
|
|
57
|
+
- Claude Code plugin latest marketplace/source version is in
|
|
58
|
+
`plugins/claude-code/.claude-plugin/plugin.json` and
|
|
59
|
+
`.claude-plugin/marketplace.json` in the `zilliztech/memsearch` repo. Check
|
|
60
|
+
the latest source manifest with
|
|
61
|
+
`curl -fsSL https://raw.githubusercontent.com/zilliztech/memsearch/main/plugins/claude-code/.claude-plugin/plugin.json`.
|
|
62
|
+
For marketplace installs, use `claude plugin marketplace update memsearch-plugins`
|
|
63
|
+
then `claude plugin update memsearch`, and restart Claude Code.
|
|
64
|
+
- OpenClaw plugin latest published version comes from
|
|
65
|
+
`clawhub package inspect memsearch`; the source version is
|
|
66
|
+
`plugins/openclaw/package.json`. Update with
|
|
67
|
+
`openclaw plugins install --force clawhub:memsearch`, restore required hook
|
|
68
|
+
permissions, then `openclaw gateway restart`.
|
|
69
|
+
- OpenCode plugin latest published version comes from
|
|
70
|
+
`npm view @zilliz/memsearch-opencode version dist-tags --json`; the source
|
|
71
|
+
version is `plugins/opencode/package.json`. If `~/.config/opencode/opencode.json`
|
|
72
|
+
pins a version, update the pin; otherwise restart OpenCode after package
|
|
73
|
+
refresh.
|
|
74
|
+
|
|
75
|
+
For more detail, fetch the update sections from the public documentation:
|
|
76
|
+
|
|
77
|
+
- Codex: https://zilliztech.github.io/memsearch/platforms/codex/installation/
|
|
78
|
+
- Claude Code: https://zilliztech.github.io/memsearch/platforms/claude-code/installation/
|
|
79
|
+
- OpenClaw: https://zilliztech.github.io/memsearch/platforms/openclaw/installation/
|
|
80
|
+
- OpenCode: https://zilliztech.github.io/memsearch/platforms/opencode/installation/
|
|
81
|
+
|
|
82
|
+
Check memory files:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
MDIR="${MEMSEARCH_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.memsearch}/memory"
|
|
86
|
+
ls -la "$MDIR"
|
|
87
|
+
find "$MDIR" -maxdepth 1 -type f -name '*.md' | sort | tail -10
|
|
88
|
+
tail -120 "$MDIR/$(date +%Y-%m-%d).md"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Check index health:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
memsearch stats
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
OpenCode transcript recall reads from OpenCode's SQLite database, while captured MemSearch memory lives as markdown under `.memsearch/memory/`.
|
|
98
|
+
|
|
99
|
+
## Background and Compatibility
|
|
100
|
+
|
|
101
|
+
Some plugin config fields may be missing or empty. That is usually normal:
|
|
102
|
+
|
|
103
|
+
- `summarize.enabled`, advanced maintenance, and task-specific provider/model fields are newer settings.
|
|
104
|
+
- Existing users' TOML files are not rewritten automatically after package/plugin upgrades.
|
|
105
|
+
- Empty strings usually mean "use the built-in or host-native default"; they do not necessarily mean "disabled" or "broken".
|
|
106
|
+
- Missing fields should be interpreted through `memsearch config list --resolved`, not by reading raw TOML alone.
|
|
107
|
+
- New users who run `memsearch config init` may see more fields than old users because the template includes newer options.
|
|
108
|
+
- Advanced maintenance is intentionally disabled by default to avoid surprise background model calls.
|
|
109
|
+
|
|
110
|
+
## Configuration Logic
|
|
111
|
+
|
|
112
|
+
Config is resolved from built-in defaults, global config, project config, env refs like `env:OPENAI_API_KEY`, and runtime env such as `MEMSEARCH_DIR`.
|
|
113
|
+
|
|
114
|
+
Use `memsearch config list --resolved` for effective behavior, `--global` for global overrides, and `--project` for repository-specific overrides.
|
|
115
|
+
|
|
116
|
+
Default recommendation:
|
|
117
|
+
|
|
118
|
+
- Put reusable defaults in global config so users do not repeat setup in every project.
|
|
119
|
+
- Put only project-specific overrides in project config.
|
|
120
|
+
- Use global config for named LLM providers, common model choices, normal input/output defaults, and shared prompt defaults.
|
|
121
|
+
- Use project config for one repo's enable/disable switches, unusual output files, special intervals, or repo-specific providers.
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
OpenCode plugin keys:
|
|
126
|
+
|
|
127
|
+
```toml
|
|
128
|
+
[plugins.opencode.summarize]
|
|
129
|
+
enabled = true
|
|
130
|
+
provider = "" # empty/native = OpenCode native summarizer
|
|
131
|
+
model = ""
|
|
132
|
+
|
|
133
|
+
[plugins.opencode.project_review]
|
|
134
|
+
enabled = false
|
|
135
|
+
provider = "native"
|
|
136
|
+
model = ""
|
|
137
|
+
min_interval_hours = 24
|
|
138
|
+
input_dir = ".memsearch/memory"
|
|
139
|
+
output_file = ".memsearch/PROJECT.md"
|
|
140
|
+
|
|
141
|
+
[plugins.opencode.user_profile]
|
|
142
|
+
enabled = false
|
|
143
|
+
provider = "native"
|
|
144
|
+
model = ""
|
|
145
|
+
min_interval_hours = 24
|
|
146
|
+
input_dir = ".memsearch/memory"
|
|
147
|
+
output_file = ".memsearch/USER.md"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Provider rules:
|
|
151
|
+
|
|
152
|
+
- `provider = ""` or `native` uses OpenCode's non-interactive native path.
|
|
153
|
+
- Any other provider value is a name that must exist under `[llm.providers.<name>]`.
|
|
154
|
+
- Model resolution order is task-level `plugins.opencode.<task>.model`, then named provider model, then built-in default.
|
|
155
|
+
- API keys should be configured as env refs, not pasted into chat.
|
|
156
|
+
- If a raw TOML field is blank, check resolved config before calling it unset or broken.
|
|
157
|
+
|
|
158
|
+
Common provider examples:
|
|
159
|
+
|
|
160
|
+
```toml
|
|
161
|
+
[llm.providers.openai]
|
|
162
|
+
type = "openai"
|
|
163
|
+
model = "gpt-5-mini"
|
|
164
|
+
api_key = "env:OPENAI_API_KEY"
|
|
165
|
+
|
|
166
|
+
[llm.providers.anthropic]
|
|
167
|
+
type = "anthropic"
|
|
168
|
+
model = "claude-sonnet-4-6"
|
|
169
|
+
api_key = "env:ANTHROPIC_API_KEY"
|
|
170
|
+
|
|
171
|
+
[llm.providers.gemini]
|
|
172
|
+
type = "gemini"
|
|
173
|
+
model = "gemini-3-flash-preview"
|
|
174
|
+
api_key = "env:GEMINI_API_KEY"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Model guidance:
|
|
178
|
+
|
|
179
|
+
- Normal turn summaries can use small/fast models. OpenCode native summarize uses OpenCode `small_model`, then its configured model/default behavior.
|
|
180
|
+
- Advanced maintenance needs better judgment. OpenCode native maintenance uses OpenCode's default unless `plugins.opencode.<task>.model` is set.
|
|
181
|
+
- For API providers, defaults are `openai -> gpt-5-mini`, `anthropic -> claude-sonnet-4-6`, and `gemini -> gemini-3-flash-preview`.
|
|
182
|
+
- If quality matters more than cost for maintenance, set `plugins.opencode.project_review.model` and `plugins.opencode.user_profile.model` explicitly.
|
|
183
|
+
|
|
184
|
+
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.
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
Prompt overrides:
|
|
189
|
+
|
|
190
|
+
```toml
|
|
191
|
+
[prompts]
|
|
192
|
+
summarize = ""
|
|
193
|
+
project_review = ""
|
|
194
|
+
user_profile = ""
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
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.
|
|
198
|
+
|
|
199
|
+
Use `memsearch config set` for changes. After changing anything, show the command, the resolved value, and whether a new session is needed.
|
|
200
|
+
|
|
201
|
+
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.
|
|
202
|
+
|
|
203
|
+
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.
|