@zilliz/memsearch-opencode 0.3.12 → 0.3.13

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/context.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * Summarize the N most recent daily .md files for cold-start context.
7
+ * Extracts recent non-empty session sections so empty SessionStart headings
8
+ * do not crowd out useful context.
9
+ */
10
+ function recentMemoryPreviewLines(content: string, maxLines: number): string[] {
11
+ const sections: string[][] = [];
12
+ let current: string[] = [];
13
+ let hasBody = false;
14
+
15
+ const flush = () => {
16
+ if (current.length > 0 && hasBody) {
17
+ sections.push(current);
18
+ }
19
+ current = [];
20
+ hasBody = false;
21
+ };
22
+
23
+ for (const rawLine of content.split("\n")) {
24
+ const line = rawLine.trimEnd();
25
+ if (/^##\s/.test(line)) {
26
+ flush();
27
+ current = [line];
28
+ continue;
29
+ }
30
+ if (/^#{3,4}\s/.test(line)) {
31
+ current.push(line);
32
+ continue;
33
+ }
34
+ if (line.startsWith("- ") || line.startsWith("[User]") || line.startsWith("[Assistant]")) {
35
+ current.push(line);
36
+ hasBody = true;
37
+ }
38
+ }
39
+
40
+ flush();
41
+ return sections.flat().slice(-maxLines);
42
+ }
43
+
44
+ export function isDailyJournalFile(file: string): boolean {
45
+ return /^\d{4}-\d{2}-\d{2}\.md$/.test(file);
46
+ }
47
+
48
+ export function getRecentMemories(
49
+ memDir: string,
50
+ count = 2,
51
+ maxLinesPerFile = 30
52
+ ): string {
53
+ if (!existsSync(memDir)) return "";
54
+
55
+ const files = readdirSync(memDir)
56
+ .filter(isDailyJournalFile)
57
+ .sort()
58
+ .slice(-count);
59
+
60
+ if (files.length === 0) return "";
61
+
62
+ const summary: string[] = [];
63
+ for (const file of files) {
64
+ try {
65
+ const content = readFileSync(join(memDir, file), "utf-8");
66
+ const lines = recentMemoryPreviewLines(content, maxLinesPerFile);
67
+ if (lines.length > 0) {
68
+ summary.push(`[${file}]`, ...lines);
69
+ }
70
+ } catch { /* skip */ }
71
+ }
72
+
73
+ if (summary.length === 0) {
74
+ return `You have ${files.length} past memory file(s). Use the memory_search tool when the user's question could benefit from historical context.`;
75
+ }
76
+
77
+ return `Recent memories (use memory_search for full search):\n${summary.join("\n")}`;
78
+ }
79
+
80
+ /** Shell-escape a string for safe use inside single quotes. */
81
+ export function shellEscape(s: string): string {
82
+ return s.replace(/'/g, "'\\''");
83
+ }
84
+
85
+ export function getSkillCandidateHint(memsearchDir: string, memsearchCmd: string): string {
86
+ try {
87
+ const result = spawnSync(
88
+ "bash",
89
+ [
90
+ "-c",
91
+ `MEMSEARCH_DIR='${shellEscape(memsearchDir)}' ${memsearchCmd} skills status --hint`,
92
+ ],
93
+ { encoding: "utf-8", timeout: 5000 }
94
+ );
95
+ if (result.status !== 0) return "";
96
+ return (result.stdout || "").trim().split("\n")[0] || "";
97
+ } catch {
98
+ return "";
99
+ }
100
+ }
101
+
102
+ /** Marks the start of memsearch's injected block within a system message. */
103
+ export const MEMSEARCH_SYSTEM_MARKER = "[memsearch] Memory available.";
104
+
105
+ /**
106
+ * Merge memsearch's memory context into an `output.system` array without
107
+ * growing it. Some backends (litellm/vllm serving e.g. Qwen models) reject a
108
+ * multi-entry `output.system` array with "system message must be first", so
109
+ * the memory block is folded into the first entry instead of pushed as a new
110
+ * one. If a memsearch block from a previous transform call is already present
111
+ * (identified by MEMSEARCH_SYSTEM_MARKER), it is replaced in place rather than
112
+ * appended again, so repeated calls against the same output stay idempotent.
113
+ */
114
+ export function mergeSystemMemoryContext(
115
+ system: string[] | undefined,
116
+ memoryText: string
117
+ ): string[] {
118
+ if (!Array.isArray(system) || system.length === 0) {
119
+ return [memoryText];
120
+ }
121
+ const result = [...system];
122
+ const existing = result[0];
123
+ const markerIndex = existing.indexOf(MEMSEARCH_SYSTEM_MARKER);
124
+ const base =
125
+ markerIndex === -1 ? existing : existing.slice(0, markerIndex).replace(/\n+$/, "");
126
+ result[0] = base ? `${base}\n\n${memoryText}` : memoryText;
127
+ return result;
128
+ }
package/index.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  readFileSync,
19
19
  existsSync,
20
20
  mkdirSync,
21
- readdirSync,
22
21
  realpathSync,
23
22
  unlinkSync,
24
23
  writeFileSync,
@@ -26,6 +25,14 @@ import {
26
25
  import { join, dirname } from "node:path";
27
26
  import { fileURLToPath } from "node:url";
28
27
 
28
+ import {
29
+ getRecentMemories,
30
+ getSkillCandidateHint,
31
+ MEMSEARCH_SYSTEM_MARKER,
32
+ mergeSystemMemoryContext,
33
+ shellEscape,
34
+ } from "./context.ts";
35
+
29
36
  const PLUGIN_DIR = dirname(realpathSync(fileURLToPath(import.meta.url)));
30
37
 
31
38
  // ---------------------------------------------------------------------------
@@ -70,131 +77,6 @@ function deriveCollectionName(projectDir: string): string {
70
77
  }
71
78
  }
72
79
 
73
- /**
74
- * Summarize the N most recent daily .md files for cold-start context.
75
- * Extracts recent non-empty session sections so empty SessionStart headings
76
- * do not crowd out useful context.
77
- */
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(
117
- memDir: string,
118
- count = 2,
119
- maxLinesPerFile = 30
120
- ): string {
121
- if (!existsSync(memDir)) return "";
122
-
123
- const files = readdirSync(memDir)
124
- .filter(isDailyJournalFile)
125
- .sort()
126
- .slice(-count);
127
-
128
- if (files.length === 0) return "";
129
-
130
- const summary: string[] = [];
131
- for (const file of files) {
132
- try {
133
- const content = readFileSync(join(memDir, file), "utf-8");
134
- const lines = recentMemoryPreviewLines(content, maxLinesPerFile);
135
- if (lines.length > 0) {
136
- summary.push(`[${file}]`, ...lines);
137
- }
138
- } catch { /* skip */ }
139
- }
140
-
141
- if (summary.length === 0) {
142
- return `You have ${files.length} past memory file(s). Use the memory_search tool when the user's question could benefit from historical context.`;
143
- }
144
-
145
- return `Recent memories (use memory_search for full search):\n${summary.join("\n")}`;
146
- }
147
-
148
- /** Shell-escape a string for safe use inside single quotes. */
149
- function shellEscape(s: string): string {
150
- return s.replace(/'/g, "'\\''");
151
- }
152
-
153
- export function getSkillCandidateHint(memsearchDir: string, memsearchCmd: string): string {
154
- try {
155
- const result = spawnSync(
156
- "bash",
157
- [
158
- "-c",
159
- `MEMSEARCH_DIR='${shellEscape(memsearchDir)}' ${memsearchCmd} skills status --hint`,
160
- ],
161
- { encoding: "utf-8", timeout: 5000 }
162
- );
163
- if (result.status !== 0) return "";
164
- return (result.stdout || "").trim().split("\n")[0] || "";
165
- } catch {
166
- return "";
167
- }
168
- }
169
-
170
- /** Marks the start of memsearch's injected block within a system message. */
171
- export const MEMSEARCH_SYSTEM_MARKER = "[memsearch] Memory available.";
172
-
173
- /**
174
- * Merge memsearch's memory context into an `output.system` array without
175
- * growing it. Some backends (litellm/vllm serving e.g. Qwen models) reject a
176
- * multi-entry `output.system` array with "system message must be first", so
177
- * the memory block is folded into the first entry instead of pushed as a new
178
- * one. If a memsearch block from a previous transform call is already present
179
- * (identified by MEMSEARCH_SYSTEM_MARKER), it is replaced in place rather than
180
- * appended again, so repeated calls against the same output stay idempotent.
181
- */
182
- export function mergeSystemMemoryContext(
183
- system: string[] | undefined,
184
- memoryText: string
185
- ): string[] {
186
- if (!Array.isArray(system) || system.length === 0) {
187
- return [memoryText];
188
- }
189
- const result = [...system];
190
- const existing = result[0];
191
- const markerIndex = existing.indexOf(MEMSEARCH_SYSTEM_MARKER);
192
- const base =
193
- markerIndex === -1 ? existing : existing.slice(0, markerIndex).replace(/\n+$/, "");
194
- result[0] = base ? `${base}\n\n${memoryText}` : memoryText;
195
- return result;
196
- }
197
-
198
80
  /**
199
81
  * Start the capture daemon as a background process.
200
82
  * The daemon polls OpenCode's SQLite for completed turns and writes to daily .md files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zilliz/memsearch-opencode",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "memsearch plugin for OpenCode — semantic memory search across sessions",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -8,6 +8,7 @@
8
8
  ".": "./index.ts"
9
9
  },
10
10
  "files": [
11
+ "context.ts",
11
12
  "index.ts",
12
13
  "scripts/*.py",
13
14
  "scripts/*.sh",
@@ -123,8 +123,16 @@ Only these low-risk local indexing keys are honored from project config:
123
123
  - `embedding.batch_size`
124
124
  - `chunking.max_chunk_size`
125
125
  - `chunking.overlap_lines`
126
+ - `indexing.ignore_files`
127
+ - `indexing.exclude`
126
128
  - `watch.debounce_ms`
127
129
 
130
+ Index exclusions are opt-in for compatibility. Missing or empty
131
+ `indexing.ignore_files` and `indexing.exclude` keep the old scan-all behavior;
132
+ new files created by `memsearch config init` explicitly write
133
+ `ignore_files = [".gitignore"]`. Each directory passed to index/watch is its own
134
+ root, and ignore discovery never walks into parent directories.
135
+
128
136
  Trusted settings are ignored or rejected in project config. Put these in global
129
137
  config (`~/.memsearch/config.toml`) or pass explicit CLI flags instead:
130
138