opencode-memoir 2.0.5 → 2.1.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 +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -29
- package/dist/index.js.map +2 -2
- package/dist/store.d.ts +1 -3
- package/dist/store.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
OpenCode plugin for [Memoir](https://github.com/zhangfengcdt/memoir): git-versioned, taxonomy-structured memory for coding agents.
|
|
4
4
|
|
|
5
|
-
Dynamically loads the `memoir-mcp` MCP server
|
|
5
|
+
Dynamically loads the `memoir-mcp` MCP server.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Prerequisite: install the Memoir CLI and MCP server first:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv tool install --python 3.13 "memoir-ai[mcp]"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then add the plugin to your OpenCode config (`~/.config/opencode/opencode.jsonc`):
|
|
10
16
|
|
|
11
17
|
```jsonc
|
|
12
18
|
{
|
|
@@ -16,7 +22,7 @@ Add the package name to your OpenCode config (`~/.config/opencode/opencode.jsonc
|
|
|
16
22
|
}
|
|
17
23
|
```
|
|
18
24
|
|
|
19
|
-
OpenCode downloads and resolves the package from npm automatically
|
|
25
|
+
OpenCode downloads and resolves the package from npm automatically. To pin a version, append it: `"opencode-memoir@1.0.0"`.
|
|
20
26
|
|
|
21
27
|
## Quick start
|
|
22
28
|
|
|
@@ -58,14 +64,14 @@ All optional:
|
|
|
58
64
|
|
|
59
65
|
## How it works
|
|
60
66
|
|
|
61
|
-
Instead of wrapping the `memoir` CLI and re-implementing tools in TypeScript, this plugin registers `memoir-mcp` as a dynamic MCP server. OpenCode starts the server via `
|
|
67
|
+
Instead of wrapping the `memoir` CLI and re-implementing tools in TypeScript, this plugin registers `memoir-mcp` as a dynamic MCP server. OpenCode starts the server via the `memoir-mcp` command, and all memoir tools (`memoir_memoir_recall`, `memoir_memoir_remember`, `memoir_memoir_get`, etc.) are available natively to the LLM.
|
|
62
68
|
|
|
63
69
|
## Development
|
|
64
70
|
|
|
65
71
|
### Prerequisites
|
|
66
72
|
|
|
67
73
|
- Node.js >= 20
|
|
68
|
-
- `
|
|
74
|
+
- `memoir-ai[mcp]` installed (see Install above)
|
|
69
75
|
|
|
70
76
|
### Setup
|
|
71
77
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAoJxE,QAAA,MAAM,MAAM,EAAE,YAGb,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,6 @@ import { execFile, execFileSync } from "node:child_process";
|
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { join, resolve } from "node:path";
|
|
28
28
|
import { promisify } from "node:util";
|
|
29
|
-
var UVX_PYTHON = "3.13";
|
|
30
29
|
var execFileAsync = promisify(execFile);
|
|
31
30
|
function errorMessage(e) {
|
|
32
31
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -73,15 +72,11 @@ function setCachedBranch(sessionID, branch) {
|
|
|
73
72
|
}
|
|
74
73
|
async function callMemoir(args, store) {
|
|
75
74
|
try {
|
|
76
|
-
const { stdout } = await execFileAsync(
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
timeout: 15e3,
|
|
82
|
-
maxBuffer: 1024 * 1024
|
|
83
|
-
}
|
|
84
|
-
);
|
|
75
|
+
const { stdout } = await execFileAsync("memoir", ["--store", store, ...args], {
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
timeout: 15e3,
|
|
78
|
+
maxBuffer: 1024 * 1024
|
|
79
|
+
});
|
|
85
80
|
return stdout.trim();
|
|
86
81
|
} catch (e) {
|
|
87
82
|
debugLog("callMemoir failed:", errorMessage(e));
|
|
@@ -120,24 +115,23 @@ var MemoirOpenCode = async (_input, rawOptions) => {
|
|
|
120
115
|
const opts = rawOptions ?? {};
|
|
121
116
|
if (opts.store) setPluginStoreOverride(opts.store);
|
|
122
117
|
const storePath = deriveStorePath();
|
|
123
|
-
const
|
|
124
|
-
const mcpCommand = [
|
|
125
|
-
"uvx",
|
|
126
|
-
"--python",
|
|
127
|
-
UVX_PYTHON,
|
|
128
|
-
"--from",
|
|
129
|
-
"memoir-ai[mcp]",
|
|
130
|
-
"sh",
|
|
131
|
-
"-c",
|
|
132
|
-
mcpFailover,
|
|
133
|
-
"memoir-mcp"
|
|
134
|
-
];
|
|
118
|
+
const mcpCommand = ["memoir-mcp"];
|
|
135
119
|
if (storePath) {
|
|
136
120
|
mcpCommand.push("--store", storePath);
|
|
137
121
|
}
|
|
122
|
+
const mcpServer = {
|
|
123
|
+
type: "local",
|
|
124
|
+
command: mcpCommand,
|
|
125
|
+
environment: storePath ? { MEMOIR_STORE: storePath } : void 0
|
|
126
|
+
};
|
|
138
127
|
const hooks = {
|
|
139
128
|
name: "memoir",
|
|
140
129
|
config: async (config) => {
|
|
130
|
+
debugLog("config hook: mcpCommand =", JSON.stringify(mcpCommand));
|
|
131
|
+
debugLog("config hook: mcpCommand length =", mcpCommand.length);
|
|
132
|
+
const mcp = config.mcp ?? {};
|
|
133
|
+
mcp.memoir = mcpServer;
|
|
134
|
+
config.mcp = mcp;
|
|
141
135
|
config.command = config.command ?? {};
|
|
142
136
|
config.command["memoir:onboard"] = {
|
|
143
137
|
description: "Populate or refresh Memoir onboarding for this project",
|
|
@@ -156,13 +150,7 @@ Memory rules:
|
|
|
156
150
|
|
|
157
151
|
Then call memoir_memoir_remember with replace=true for durable onboarding facts. Use namespace codebase:onboard in git repositories and project:onboard outside git.`
|
|
158
152
|
};
|
|
159
|
-
|
|
160
|
-
mcp.memoir = {
|
|
161
|
-
type: "local",
|
|
162
|
-
command: mcpCommand,
|
|
163
|
-
environment: storePath ? { MEMOIR_STORE: storePath } : void 0
|
|
164
|
-
};
|
|
165
|
-
config.mcp = mcp;
|
|
153
|
+
debugLog("config hook: mcpCommand =", JSON.stringify(mcpCommand));
|
|
166
154
|
},
|
|
167
155
|
"shell.env": async (_input2, output) => {
|
|
168
156
|
try {
|
|
@@ -224,6 +212,7 @@ Then call memoir_memoir_remember with replace=true for durable onboarding facts.
|
|
|
224
212
|
pruneAll();
|
|
225
213
|
}
|
|
226
214
|
};
|
|
215
|
+
hooks.mcp = { memoir: mcpServer };
|
|
227
216
|
return hooks;
|
|
228
217
|
};
|
|
229
218
|
var plugin = {
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/debug.ts", "../src/memory-saver.ts", "../src/store.ts", "../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Debug logger writing to stderr. Prefixes with [memoir] for easy filtering.\n * Only writes when MEMOIR_DEBUG=1 is set.\n */\nexport function debugLog(...args: unknown[]): void {\n if (process.env.MEMOIR_DEBUG !== \"1\") return;\n const ts = new Date().toISOString();\n process.stderr.write(`[memoir ${ts}] ${args.map((a) => String(a)).join(\" \")}\\n`);\n}\n", "/** Per-session message count for periodic reminders. */\nexport const sessionMsgCount = new Map<string, number>();\n\nconst REMINDER_INTERVAL = Math.max(0, parseInt(process.env.MEMOIR_REMINDER_INTERVAL ?? \"5\", 10));\n\nexport function incrementMsgCount(sessionID: string): number {\n const count = (sessionMsgCount.get(sessionID) ?? 0) + 1;\n sessionMsgCount.set(sessionID, count);\n return count;\n}\n\nexport function shouldRemind(count: number): boolean {\n return REMINDER_INTERVAL > 0 && count > 1 && count % REMINDER_INTERVAL === 0;\n}\n\nexport function pruneAll(): void {\n sessionMsgCount.clear();\n}\n", "import { execFile, execFileSync } from \"node:child_process\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { debugLog } from \"./debug.js\";\n\n/** Python version pinned for uvx to avoid litellm build failure on 3.14+. */\nexport const UVX_PYTHON = \"3.13\" as const;\n\nconst execFileAsync = promisify(execFile);\n\nfunction errorMessage(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/** Resolve memoir store path.\n * 1. Plugin `store` option override\n * 2. MEMOIR_STORE env var\n * 3. Git root \u2192 `~/.memoir/<slug>` (all worktrees share one store)\n * 4. Resolved cwd \u2192 `~/.memoir/<slug>` for non-git dirs\n */\nexport function deriveStorePath(cwd = process.cwd()): string {\n if (pluginStoreOverride) return pluginStoreOverride;\n const envStore = process.env.MEMOIR_STORE;\n if (envStore) return envStore;\n\n let projectDir: string;\n try {\n const gitRoot = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n encoding: \"utf8\",\n timeout: 3_000,\n }).trim();\n projectDir = gitRoot;\n } catch {\n projectDir = resolve(cwd);\n }\n\n const slug = projectDir.replace(/[/.]/g, \"-\");\n return join(homedir(), \".memoir\", slug);\n}\n\nexport let pluginStoreOverride: string | undefined;\n\nexport function setPluginStoreOverride(store: string | undefined): void {\n pluginStoreOverride = store;\n}\n\n/** Get current git branch (empty string if not in a git repo). */\nexport function currentGitBranch(cwd = process.cwd()): string {\n try {\n return execFileSync(\"git\", [\"branch\", \"--show-current\"], {\n cwd,\n encoding: \"utf8\",\n timeout: 3_000,\n }).trim();\n } catch {\n return \"\";\n }\n}\n\nconst branchCache = new Map<string, string>();\n\nexport function getCachedBranch(sessionID: string): string {\n return branchCache.get(sessionID) ?? \"\";\n}\n\nexport function setCachedBranch(sessionID: string, branch: string): void {\n branchCache.set(sessionID, branch);\n}\n\n/** Call the memoir CLI via uvx. */\nexport async function callMemoir(args: string[], store: string): Promise<string | null> {\n try {\n const { stdout } = (await execFileAsync(\n \"uvx\",\n [\"--python\", UVX_PYTHON, \"--from\", \"memoir-ai\", \"memoir\", \"--store\", store, ...args],\n {\n encoding: \"utf8\",\n timeout: 15_000,\n maxBuffer: 1_024 * 1_024,\n },\n )) as { stdout: string };\n return stdout.trim();\n } catch (e) {\n debugLog(\"callMemoir failed:\", errorMessage(e));\n return null;\n }\n}\n\nexport async function autoMatchMemoirBranch(store: string, sessionID: string): Promise<void> {\n const codeBranch = currentGitBranch();\n if (!codeBranch) return;\n\n const cached = getCachedBranch(sessionID);\n if (cached === codeBranch) return;\n\n // Check if branch exists in memoir\n const branchResult = await callMemoir([\"--json\", \"branch\"], store);\n if (branchResult === null) return;\n\n try {\n const data = JSON.parse(branchResult);\n const branches: string[] = data?.branches ?? [];\n if (!branches.includes(codeBranch)) {\n await callMemoir([\"branch\", codeBranch], store);\n }\n await callMemoir([\"checkout\", codeBranch], store);\n setCachedBranch(sessionID, codeBranch);\n } catch (e) {\n debugLog(\"autoMatchMemoirBranch: parsing failed:\", errorMessage(e));\n }\n}\n\nexport function pruneBranchCache(): void {\n branchCache.clear();\n}\n", "import type { Config, Plugin, PluginModule } from \"@opencode-ai/plugin\";\nimport { debugLog } from \"./debug.js\";\nimport { incrementMsgCount, pruneAll, sessionMsgCount, shouldRemind } from \"./memory-saver.js\";\nimport {\n autoMatchMemoirBranch,\n callMemoir,\n deriveStorePath,\n pruneBranchCache,\n setPluginStoreOverride,\n UVX_PYTHON,\n} from \"./store.js\";\n\nconst sessionsWithStartupHint = new Set<string>();\n\nfunction errorMessage(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\nconst MemoirOpenCode: Plugin = async (_input, rawOptions) => {\n const opts = (rawOptions ?? {}) as { store?: string };\n if (opts.store) setPluginStoreOverride(opts.store);\n\n const storePath = deriveStorePath();\n\n // Failover: try `memoir-mcp` console script first (works once upstream\n // registers [project.scripts]), fall back to direct python -c invocation.\n const mcpFailover =\n 'exec memoir-mcp \"$@\" 2>/dev/null || exec python -c \"from memoir.mcp.server import main; main()\" \"$@\"';\n const mcpCommand = [\n \"uvx\",\n \"--python\",\n UVX_PYTHON,\n \"--from\",\n \"memoir-ai[mcp]\",\n \"sh\",\n \"-c\",\n mcpFailover,\n \"memoir-mcp\",\n ];\n if (storePath) {\n mcpCommand.push(\"--store\", storePath);\n }\n\n const hooks: Record<string, unknown> = {\n name: \"memoir\",\n\n config: async (config: Config): Promise<void> => {\n config.command = config.command ?? {};\n config.command[\"memoir:onboard\"] = {\n description: \"Populate or refresh Memoir onboarding for this project\",\n template: `Populate or refresh Memoir onboarding for the current project.\n\nWorkflow:\n- First obtain a project file tree to understand structure.\n- Start studying from project documentation.\n- Continue only based on what the tree and documentation show.\n\nMemory rules:\n- Record only verified facts from files/docs/code or explicit user statements.\n- Do not write inferred user thoughts, intentions, preferences, or opinions.\n- Do not use preferences.* paths unless the user explicitly stated a preference.\n- If a fact is your interpretation, do not save it; report it as uncertain instead.\n\nThen call memoir_memoir_remember with replace=true for durable onboarding facts. Use namespace codebase:onboard in git repositories and project:onboard outside git.`,\n };\n\n // Register memoir MCP server\n const mcp = config.mcp ?? {};\n mcp.memoir = {\n type: \"local\" as const,\n command: mcpCommand,\n environment: storePath ? { MEMOIR_STORE: storePath } : undefined,\n };\n config.mcp = mcp;\n },\n\n \"shell.env\": async (\n _input: unknown,\n output: { env: Record<string, string> },\n ): Promise<void> => {\n try {\n if (storePath) {\n output.env.MEMOIR_STORE = storePath;\n }\n } catch (e) {\n debugLog(\"shell.env: failed:\", errorMessage(e));\n }\n },\n\n \"chat.message\": async (\n input: { sessionID?: string },\n _output: { parts?: Array<{ type: string; text: string }> },\n ): Promise<void> => {\n try {\n const sid = input.sessionID ?? \"default\";\n incrementMsgCount(sid);\n\n await autoMatchMemoirBranch(storePath, sid);\n } catch (e) {\n debugLog(\"chat.message: failed:\", errorMessage(e));\n }\n },\n\n \"experimental.chat.system.transform\": async (\n input: { sessionID?: string },\n output: { system?: string[] },\n ): Promise<void> => {\n try {\n const sid = input.sessionID ?? \"default\";\n\n if (input.sessionID && !sessionsWithStartupHint.has(sid)) {\n sessionsWithStartupHint.add(sid);\n output.system?.unshift(\n '[memoir] You have Memoir MCP tools (memoir_memoir_recall, memoir_memoir_remember, memoir_memoir_get, memoir_memoir_summarize, memoir_memoir_status, memoir_memoir_branches, memoir_memoir_checkout).\\nSAVE (memoir_memoir_remember) after: task completion, user stating preferences/constraints, discovering project facts. RECALL (memoir_memoir_recall \\u2192 memoir_memoir_get) at session start for prior context, when user asks about prior work.\\nNamespaces: \"default\" (user context), \"codebase:onboard\" (project facts in git repos). Taxonomy paths: preferences.*, project.*, codebase.*, decisions.*, learning.*.',\n );\n }\n\n const count = sessionMsgCount.get(sid) ?? 0;\n if (shouldRemind(count)) {\n output.system?.push(\n \"\\n[memoir] Reminder: Use memoir_memoir_remember to save context, memoir_memoir_recall to retrieve prior context.\",\n );\n }\n } catch (e) {\n debugLog(\"system.transform: failed:\", errorMessage(e));\n }\n },\n\n dispose: async (): Promise<void> => {\n try {\n if (process.env.MEMOIR_AUTO_SAVE === \"1\") {\n for (const [sid, count] of sessionMsgCount) {\n await callMemoir(\n [\n \"remember\",\n `session.${sid}`,\n \"--message\",\n `Session ended \u2014 ${count} user messages exchanged`,\n ],\n storePath,\n );\n }\n }\n } catch (e) {\n debugLog(\"dispose: save failed:\", errorMessage(e));\n }\n sessionsWithStartupHint.clear();\n pruneBranchCache();\n pruneAll();\n },\n };\n\n return hooks;\n};\n\nconst plugin: PluginModule = {\n id: \"opencode-memoir\",\n server: MemoirOpenCode,\n};\n\nexport default plugin;\n"],
|
|
5
|
-
"mappings": ";AAIO,SAAS,YAAY,MAAuB;AACjD,MAAI,QAAQ,IAAI,iBAAiB,IAAK;AACtC,QAAM,MAAK,oBAAI,KAAK,GAAE,YAAY;AAClC,UAAQ,OAAO,MAAM,WAAW,EAAE,KAAK,KAAK,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC;AAAA,CAAI;AACjF;;;ACPO,IAAM,kBAAkB,oBAAI,IAAoB;AAEvD,IAAM,oBAAoB,KAAK,IAAI,GAAG,SAAS,QAAQ,IAAI,4BAA4B,KAAK,EAAE,CAAC;AAExF,SAAS,kBAAkB,WAA2B;AAC3D,QAAM,SAAS,gBAAgB,IAAI,SAAS,KAAK,KAAK;AACtD,kBAAgB,IAAI,WAAW,KAAK;AACpC,SAAO;AACT;AAEO,SAAS,aAAa,OAAwB;AACnD,SAAO,oBAAoB,KAAK,QAAQ,KAAK,QAAQ,sBAAsB;AAC7E;AAEO,SAAS,WAAiB;AAC/B,kBAAgB,MAAM;AACxB;;;ACjBA,SAAS,UAAU,oBAAoB;AACvC,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAC9B,SAAS,iBAAiB;
|
|
4
|
+
"sourcesContent": ["/**\n * Debug logger writing to stderr. Prefixes with [memoir] for easy filtering.\n * Only writes when MEMOIR_DEBUG=1 is set.\n */\nexport function debugLog(...args: unknown[]): void {\n if (process.env.MEMOIR_DEBUG !== \"1\") return;\n const ts = new Date().toISOString();\n process.stderr.write(`[memoir ${ts}] ${args.map((a) => String(a)).join(\" \")}\\n`);\n}\n", "/** Per-session message count for periodic reminders. */\nexport const sessionMsgCount = new Map<string, number>();\n\nconst REMINDER_INTERVAL = Math.max(0, parseInt(process.env.MEMOIR_REMINDER_INTERVAL ?? \"5\", 10));\n\nexport function incrementMsgCount(sessionID: string): number {\n const count = (sessionMsgCount.get(sessionID) ?? 0) + 1;\n sessionMsgCount.set(sessionID, count);\n return count;\n}\n\nexport function shouldRemind(count: number): boolean {\n return REMINDER_INTERVAL > 0 && count > 1 && count % REMINDER_INTERVAL === 0;\n}\n\nexport function pruneAll(): void {\n sessionMsgCount.clear();\n}\n", "import { execFile, execFileSync } from \"node:child_process\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { debugLog } from \"./debug.js\";\n\nconst execFileAsync = promisify(execFile);\n\nfunction errorMessage(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/** Resolve memoir store path.\n * 1. Plugin `store` option override\n * 2. MEMOIR_STORE env var\n * 3. Git root \u2192 `~/.memoir/<slug>` (all worktrees share one store)\n * 4. Resolved cwd \u2192 `~/.memoir/<slug>` for non-git dirs\n */\nexport function deriveStorePath(cwd = process.cwd()): string {\n if (pluginStoreOverride) return pluginStoreOverride;\n const envStore = process.env.MEMOIR_STORE;\n if (envStore) return envStore;\n\n let projectDir: string;\n try {\n const gitRoot = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n encoding: \"utf8\",\n timeout: 3_000,\n }).trim();\n projectDir = gitRoot;\n } catch {\n projectDir = resolve(cwd);\n }\n\n const slug = projectDir.replace(/[/.]/g, \"-\");\n return join(homedir(), \".memoir\", slug);\n}\n\nexport let pluginStoreOverride: string | undefined;\n\nexport function setPluginStoreOverride(store: string | undefined): void {\n pluginStoreOverride = store;\n}\n\n/** Get current git branch (empty string if not in a git repo). */\nexport function currentGitBranch(cwd = process.cwd()): string {\n try {\n return execFileSync(\"git\", [\"branch\", \"--show-current\"], {\n cwd,\n encoding: \"utf8\",\n timeout: 3_000,\n }).trim();\n } catch {\n return \"\";\n }\n}\n\nconst branchCache = new Map<string, string>();\n\nexport function getCachedBranch(sessionID: string): string {\n return branchCache.get(sessionID) ?? \"\";\n}\n\nexport function setCachedBranch(sessionID: string, branch: string): void {\n branchCache.set(sessionID, branch);\n}\n\n/** Call the memoir CLI directly. */\nexport async function callMemoir(args: string[], store: string): Promise<string | null> {\n try {\n const { stdout } = (await execFileAsync(\"memoir\", [\"--store\", store, ...args], {\n encoding: \"utf8\",\n timeout: 15_000,\n maxBuffer: 1_024 * 1_024,\n })) as { stdout: string };\n return stdout.trim();\n } catch (e) {\n debugLog(\"callMemoir failed:\", errorMessage(e));\n return null;\n }\n}\n\nexport async function autoMatchMemoirBranch(store: string, sessionID: string): Promise<void> {\n const codeBranch = currentGitBranch();\n if (!codeBranch) return;\n\n const cached = getCachedBranch(sessionID);\n if (cached === codeBranch) return;\n\n // Check if branch exists in memoir\n const branchResult = await callMemoir([\"--json\", \"branch\"], store);\n if (branchResult === null) return;\n\n try {\n const data = JSON.parse(branchResult);\n const branches: string[] = data?.branches ?? [];\n if (!branches.includes(codeBranch)) {\n await callMemoir([\"branch\", codeBranch], store);\n }\n await callMemoir([\"checkout\", codeBranch], store);\n setCachedBranch(sessionID, codeBranch);\n } catch (e) {\n debugLog(\"autoMatchMemoirBranch: parsing failed:\", errorMessage(e));\n }\n}\n\nexport function pruneBranchCache(): void {\n branchCache.clear();\n}\n", "import type { Config, Plugin, PluginModule } from \"@opencode-ai/plugin\";\nimport { debugLog } from \"./debug.js\";\nimport { incrementMsgCount, pruneAll, sessionMsgCount, shouldRemind } from \"./memory-saver.js\";\nimport {\n autoMatchMemoirBranch,\n callMemoir,\n deriveStorePath,\n pruneBranchCache,\n setPluginStoreOverride,\n} from \"./store.js\";\n\nconst sessionsWithStartupHint = new Set<string>();\n\nfunction errorMessage(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\nconst MemoirOpenCode: Plugin = async (_input, rawOptions) => {\n const opts = (rawOptions ?? {}) as { store?: string };\n if (opts.store) setPluginStoreOverride(opts.store);\n\n const storePath = deriveStorePath();\n\n const mcpCommand = [\"memoir-mcp\"];\n if (storePath) {\n mcpCommand.push(\"--store\", storePath);\n }\n\n const mcpServer = {\n type: \"local\" as const,\n command: mcpCommand,\n environment: storePath ? { MEMOIR_STORE: storePath } : undefined,\n };\n\n const hooks: Record<string, unknown> = {\n name: \"memoir\",\n\n config: async (config: Config): Promise<void> => {\n debugLog(\"config hook: mcpCommand =\", JSON.stringify(mcpCommand));\n debugLog(\"config hook: mcpCommand length =\", mcpCommand.length);\n\n const mcp = config.mcp ?? {};\n mcp.memoir = mcpServer;\n config.mcp = mcp;\n\n config.command = config.command ?? {};\n config.command[\"memoir:onboard\"] = {\n description: \"Populate or refresh Memoir onboarding for this project\",\n template: `Populate or refresh Memoir onboarding for the current project.\n\nWorkflow:\n- First obtain a project file tree to understand structure.\n- Start studying from project documentation.\n- Continue only based on what the tree and documentation show.\n\nMemory rules:\n- Record only verified facts from files/docs/code or explicit user statements.\n- Do not write inferred user thoughts, intentions, preferences, or opinions.\n- Do not use preferences.* paths unless the user explicitly stated a preference.\n- If a fact is your interpretation, do not save it; report it as uncertain instead.\n\nThen call memoir_memoir_remember with replace=true for durable onboarding facts. Use namespace codebase:onboard in git repositories and project:onboard outside git.`,\n };\n\n debugLog(\"config hook: mcpCommand =\", JSON.stringify(mcpCommand));\n },\n\n \"shell.env\": async (\n _input: unknown,\n output: { env: Record<string, string> },\n ): Promise<void> => {\n try {\n if (storePath) {\n output.env.MEMOIR_STORE = storePath;\n }\n } catch (e) {\n debugLog(\"shell.env: failed:\", errorMessage(e));\n }\n },\n\n \"chat.message\": async (\n input: { sessionID?: string },\n _output: { parts?: Array<{ type: string; text: string }> },\n ): Promise<void> => {\n try {\n const sid = input.sessionID ?? \"default\";\n incrementMsgCount(sid);\n\n await autoMatchMemoirBranch(storePath, sid);\n } catch (e) {\n debugLog(\"chat.message: failed:\", errorMessage(e));\n }\n },\n\n \"experimental.chat.system.transform\": async (\n input: { sessionID?: string },\n output: { system?: string[] },\n ): Promise<void> => {\n try {\n const sid = input.sessionID ?? \"default\";\n\n if (input.sessionID && !sessionsWithStartupHint.has(sid)) {\n sessionsWithStartupHint.add(sid);\n output.system?.unshift(\n '[memoir] You have Memoir MCP tools (memoir_memoir_recall, memoir_memoir_remember, memoir_memoir_get, memoir_memoir_summarize, memoir_memoir_status, memoir_memoir_branches, memoir_memoir_checkout).\\nSAVE (memoir_memoir_remember) after: task completion, user stating preferences/constraints, discovering project facts. RECALL (memoir_memoir_recall \\u2192 memoir_memoir_get) at session start for prior context, when user asks about prior work.\\nNamespaces: \"default\" (user context), \"codebase:onboard\" (project facts in git repos). Taxonomy paths: preferences.*, project.*, codebase.*, decisions.*, learning.*.',\n );\n }\n\n const count = sessionMsgCount.get(sid) ?? 0;\n if (shouldRemind(count)) {\n output.system?.push(\n \"\\n[memoir] Reminder: Use memoir_memoir_remember to save context, memoir_memoir_recall to retrieve prior context.\",\n );\n }\n } catch (e) {\n debugLog(\"system.transform: failed:\", errorMessage(e));\n }\n },\n\n dispose: async (): Promise<void> => {\n try {\n if (process.env.MEMOIR_AUTO_SAVE === \"1\") {\n for (const [sid, count] of sessionMsgCount) {\n await callMemoir(\n [\n \"remember\",\n `session.${sid}`,\n \"--message\",\n `Session ended \u2014 ${count} user messages exchanged`,\n ],\n storePath,\n );\n }\n }\n } catch (e) {\n debugLog(\"dispose: save failed:\", errorMessage(e));\n }\n sessionsWithStartupHint.clear();\n pruneBranchCache();\n pruneAll();\n },\n };\n\n hooks.mcp = { memoir: mcpServer };\n\n return hooks;\n};\n\nconst plugin: PluginModule = {\n id: \"opencode-memoir\",\n server: MemoirOpenCode,\n};\n\nexport default plugin;\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["errorMessage", "_input"]
|
|
7
7
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Python version pinned for uvx to avoid litellm build failure on 3.14+. */
|
|
2
|
-
export declare const UVX_PYTHON: "3.13";
|
|
3
1
|
/** Resolve memoir store path.
|
|
4
2
|
* 1. Plugin `store` option override
|
|
5
3
|
* 2. MEMOIR_STORE env var
|
|
@@ -13,7 +11,7 @@ export declare function setPluginStoreOverride(store: string | undefined): void;
|
|
|
13
11
|
export declare function currentGitBranch(cwd?: string): string;
|
|
14
12
|
export declare function getCachedBranch(sessionID: string): string;
|
|
15
13
|
export declare function setCachedBranch(sessionID: string, branch: string): void;
|
|
16
|
-
/** Call the memoir CLI
|
|
14
|
+
/** Call the memoir CLI directly. */
|
|
17
15
|
export declare function callMemoir(args: string[], store: string): Promise<string | null>;
|
|
18
16
|
export declare function autoMatchMemoirBranch(store: string, sessionID: string): Promise<void>;
|
|
19
17
|
export declare function pruneBranchCache(): void;
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,SAAgB,GAAG,MAAM,CAmB3D;AAED,eAAO,IAAI,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAC;AAEnD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEtE;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,GAAG,SAAgB,GAAG,MAAM,CAU5D;AAID,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEvE;AAED,oCAAoC;AACpC,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYtF;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB3F;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|