open-memex
Version:
Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.
105 lines (96 loc) • 3.58 kB
text/typescript
import type { z } from "zod";
import type { ToolContext, ToolResult } from "@opencode-ai/plugin";
import type { Scope } from "../scope.ts";
import { PERSONAL_SCOPE } from "../scope.ts";
import type { MyOMemoryConfig } from "../config.ts";
import { syncScope } from "../store/sync.ts";
import {
addMemory,
searchMemories,
listMemories,
supersedeMemory,
forgetMemory,
memoryAddArgs,
memorySearchArgs,
memoryListArgs,
memorySupersedeArgs,
memoryForgetArgs,
TOOL_DESCRIPTIONS,
withOutboxNote,
} from "./ops.ts";
// Local stand-in for the SDK's tool() helper — same signature, and the SDK
// function is a runtime identity (`return input`). Do NOT import it from
// @opencode-ai/plugin at runtime: that package is a devDependency, and when
// opencode loads this plugin by file:// URL from a global npm install
// nothing resolves the module — the import fails and opencode silently
// skips the whole plugin: no error, no tools (D58).
function tool<Args extends z.ZodRawShape>(input: {
description: string;
args: Args;
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>;
}): { description: string; args: Args; execute: typeof input.execute } {
return input;
}
export function makeTools(getScope: () => Scope, cfg: MyOMemoryConfig) {
// D64: the plugin host syncs only at bootstrap, so without this the
// index goes stale for the whole session — memories added through the
// CLI or another client stay invisible to the agent, and same-content
// duplicates accumulate. Mirror the MCP server: reconcile from disk
// before every tool call. syncScope is mtime-based, so the steady-state
// cost is one directory scan.
const syncFirst = () => {
try {
syncScope(getScope().key, "request");
syncScope(PERSONAL_SCOPE.key, "request");
} catch (err) {
// Deliberate asymmetry with the MCP adapter (which lets sync errors
// propagate as tool errors): a failed sync must not take the plugin
// tools down — the index may be stale for this call but the tool
// still answers. Loud at debug, silent otherwise.
if (cfg.logLevel === "debug") {
console.error("[open-memex] pre-tool sync failed:", err);
}
}
};
const memory_add = tool({
description: TOOL_DESCRIPTIONS.memory_add,
args: memoryAddArgs,
async execute(args) {
syncFirst();
return withOutboxNote(getScope().key, addMemory(getScope, cfg, args));
},
});
const memory_search = tool({
description: TOOL_DESCRIPTIONS.memory_search,
args: memorySearchArgs,
async execute(args) {
syncFirst();
return searchMemories(getScope, args);
},
});
const memory_list = tool({
description: TOOL_DESCRIPTIONS.memory_list,
args: memoryListArgs,
async execute(args) {
syncFirst();
return listMemories(getScope, args);
},
});
const memory_supersede = tool({
description: TOOL_DESCRIPTIONS.memory_supersede,
args: memorySupersedeArgs,
async execute(args) {
syncFirst();
return withOutboxNote(getScope().key, supersedeMemory(cfg, args));
},
});
const memory_forget = tool({
description: TOOL_DESCRIPTIONS.memory_forget,
args: memoryForgetArgs,
async execute(args) {
syncFirst();
return withOutboxNote(getScope().key, forgetMemory(args));
},
});
return { memory_add, memory_search, memory_list, memory_forget, memory_supersede };
}