@dzhi/ocpg 0.5.0 → 0.6.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/ocpg.ts +88 -62
- package/package.json +3 -5
package/ocpg.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// ocpg - DB access layer for OpenCode persistent memory plugin.
|
|
2
2
|
import { SQL } from "bun";
|
|
3
|
-
import
|
|
4
|
-
import { tool } from "@opencode-ai/plugin";
|
|
5
|
-
import { z } from "zod";
|
|
3
|
+
import { Plugin } from "@opencode/plugin";
|
|
6
4
|
|
|
7
5
|
// --- DB config: plugin options > env > defaults. Password is deliberately env-only (never in config).
|
|
8
6
|
type DbOptions = {
|
|
@@ -12,6 +10,8 @@ type DbOptions = {
|
|
|
12
10
|
database?: string;
|
|
13
11
|
};
|
|
14
12
|
type DbConfig = Required<DbOptions>;
|
|
13
|
+
type RecallArgs = { query?: string; global?: boolean; limit?: number };
|
|
14
|
+
type RememberArgs = { content: string; tags?: string[] };
|
|
15
15
|
|
|
16
16
|
// Defaults resolved ONCE at module init — the pass lookup here is the only permitted spawn in this file.
|
|
17
17
|
const defaultConfig: DbConfig = {
|
|
@@ -41,7 +41,7 @@ function makeSql(cfg: DbConfig): SQL {
|
|
|
41
41
|
|
|
42
42
|
let sql = makeSql(defaultConfig);
|
|
43
43
|
|
|
44
|
-
// Swap the pool when the plugin loads with config options (
|
|
44
|
+
// Swap the pool when the plugin loads with config options ({ "package": "@dzhi/ocpg", "options": {...} } in opencode.jsonc).
|
|
45
45
|
// No-op without options so the module-level env/default config stands. Pools are lazy — a never-connected pool closes cleanly.
|
|
46
46
|
function reconfigure(options?: DbOptions): void {
|
|
47
47
|
if (!options) return;
|
|
@@ -69,20 +69,15 @@ function rateLimitOk(kind: string): boolean {
|
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
pluginClient = client;
|
|
72
|
+
// Test hook: the 60s rate-limit window is module state; tests clear it for determinism.
|
|
73
|
+
function resetRateLimit(): void {
|
|
74
|
+
lastLogTime.clear();
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
message: string,
|
|
81
|
-
): void {
|
|
82
|
-
const c = client ?? pluginClient;
|
|
83
|
-
if (!c?.app?.log) return;
|
|
77
|
+
// V2 plugins have no client.app.log; console.error from plugin code lands in the server log.
|
|
78
|
+
function logError(message: string): void {
|
|
84
79
|
if (!rateLimitOk("db-error")) return;
|
|
85
|
-
|
|
80
|
+
console.error(`[ocpg] ${message}`);
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
// --- Injection pipeline ---
|
|
@@ -114,7 +109,7 @@ function formatBlock(rows: MemoryRow[], projectDir: string): string {
|
|
|
114
109
|
const injectionCache = new Map<string, string>();
|
|
115
110
|
|
|
116
111
|
async function handleTransform(
|
|
117
|
-
input: { sessionID?: string; model
|
|
112
|
+
input: { sessionID?: string; model?: unknown },
|
|
118
113
|
output: { system: string[] },
|
|
119
114
|
directory: string,
|
|
120
115
|
): Promise<void> {
|
|
@@ -143,7 +138,7 @@ async function handleTransform(
|
|
|
143
138
|
injectionCache.set(sid, block);
|
|
144
139
|
output.system.push(block);
|
|
145
140
|
} catch (e: unknown) {
|
|
146
|
-
logError(
|
|
141
|
+
logError(`ocpg injection failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
147
142
|
}
|
|
148
143
|
}
|
|
149
144
|
|
|
@@ -162,7 +157,7 @@ function normalizeTags(tags: string[] | undefined, projectDir: string): string[]
|
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
async function recall(
|
|
165
|
-
args:
|
|
160
|
+
args: RecallArgs,
|
|
166
161
|
ctx: { directory: string },
|
|
167
162
|
): Promise<string> {
|
|
168
163
|
try {
|
|
@@ -198,13 +193,13 @@ async function recall(
|
|
|
198
193
|
})
|
|
199
194
|
.join('\n---\n');
|
|
200
195
|
} catch (e: unknown) {
|
|
201
|
-
logError(
|
|
196
|
+
logError(`ocpg recall failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
202
197
|
return `ERROR: ${e instanceof Error ? e.message : String(e)}`;
|
|
203
198
|
}
|
|
204
199
|
}
|
|
205
200
|
|
|
206
201
|
async function remember(
|
|
207
|
-
args:
|
|
202
|
+
args: RememberArgs,
|
|
208
203
|
ctx: { directory: string; sessionID: string },
|
|
209
204
|
): Promise<string> {
|
|
210
205
|
try {
|
|
@@ -236,14 +231,14 @@ async function remember(
|
|
|
236
231
|
|
|
237
232
|
const inserted = await sql`
|
|
238
233
|
INSERT INTO memories (content, tags, session_id, project)
|
|
239
|
-
VALUES (${args.content}, ${normalizedTags}, ${ctx.sessionID}, ${ctx.directory})
|
|
234
|
+
VALUES (${args.content}, ${sql.array(normalizedTags)}, ${ctx.sessionID}, ${ctx.directory})
|
|
240
235
|
RETURNING id
|
|
241
236
|
` as { id: number }[];
|
|
242
237
|
|
|
243
238
|
invalidateInjection(ctx.sessionID);
|
|
244
239
|
return `Stored memory #${inserted[0].id} for project ${basename}.`;
|
|
245
240
|
} catch (e: unknown) {
|
|
246
|
-
logError(
|
|
241
|
+
logError(`ocpg remember failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
247
242
|
return `ERROR: ${e instanceof Error ? e.message : String(e)}`;
|
|
248
243
|
}
|
|
249
244
|
}
|
|
@@ -252,6 +247,73 @@ function invalidateInjection(sessionID: string): void {
|
|
|
252
247
|
injectionCache.delete(sessionID);
|
|
253
248
|
}
|
|
254
249
|
|
|
250
|
+
// V2 entrypoint: registers the system-context injection hook and the agent tools
|
|
251
|
+
// through the plugin context. Directory comes from the plugin's load location
|
|
252
|
+
// (per-project instance, same semantics as V1's client.directory); sessionID
|
|
253
|
+
// comes from the tool execution context.
|
|
254
|
+
const ocpg = Plugin.define({
|
|
255
|
+
id: "ocpg",
|
|
256
|
+
async setup(ctx) {
|
|
257
|
+
reconfigure(ctx.options as DbOptions | undefined);
|
|
258
|
+
const directory = ctx.location.directory;
|
|
259
|
+
|
|
260
|
+
// Inject project memories into every model request's system context.
|
|
261
|
+
// handleTransform owns the per-session cache (32-slot, invalidated on remember).
|
|
262
|
+
await ctx.session.hook("context", async (event) => {
|
|
263
|
+
const output: { system: string[] } = { system: [] };
|
|
264
|
+
await handleTransform({ sessionID: event.sessionID, model: event.model }, output, directory);
|
|
265
|
+
for (const text of output.system) event.system.push({ type: "text", text });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Agent tools: recall + remember with dedup-on-write. Input schemas are raw
|
|
269
|
+
// JSON Schema (V2 contract); content length is enforced in remember().
|
|
270
|
+
await ctx.tool.transform((editor) => {
|
|
271
|
+
editor.add({
|
|
272
|
+
name: "memory_recall",
|
|
273
|
+
description:
|
|
274
|
+
"Search past memories stored for this project. Use before non-trivial work to check for relevant lessons, fixes, and decisions.",
|
|
275
|
+
input: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
query: { type: "string", description: "Full-text search string; omit for the latest memories" },
|
|
279
|
+
global: { type: "boolean", description: "Search across all projects (default: current project only)" },
|
|
280
|
+
limit: { type: "number", description: "1-20, default 5" },
|
|
281
|
+
},
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
},
|
|
284
|
+
execute: async (input) => {
|
|
285
|
+
return { content: await recall(input as RecallArgs, { directory }) };
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
editor.add({
|
|
289
|
+
name: "memory_remember",
|
|
290
|
+
description:
|
|
291
|
+
"Store a memory for this project. Use after user corrections, architecture decisions, or non-trivial fixes.",
|
|
292
|
+
input: {
|
|
293
|
+
type: "object",
|
|
294
|
+
properties: {
|
|
295
|
+
content: { type: "string", description: "1-3 self-contained sentences capturing the why" },
|
|
296
|
+
tags: {
|
|
297
|
+
type: "array",
|
|
298
|
+
items: { type: "string" },
|
|
299
|
+
description:
|
|
300
|
+
"Category prefixes: preference, decision, debug, env, architecture, workaround, language:<x>, framework:<x>, tool:<x>",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
required: ["content"],
|
|
304
|
+
additionalProperties: false,
|
|
305
|
+
},
|
|
306
|
+
execute: async (input, tool) => {
|
|
307
|
+
return { content: await remember(input as RememberArgs, { directory, sessionID: tool.sessionID }) };
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Close the SQL pool when the plugin unloads.
|
|
313
|
+
return dispose;
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
255
317
|
const __internals = {
|
|
256
318
|
get sql() {
|
|
257
319
|
return sql;
|
|
@@ -266,48 +328,12 @@ const __internals = {
|
|
|
266
328
|
invalidateInjection,
|
|
267
329
|
logError,
|
|
268
330
|
rateLimitOk,
|
|
269
|
-
|
|
331
|
+
resetRateLimit,
|
|
270
332
|
dispose,
|
|
271
333
|
};
|
|
272
334
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
278
|
-
await handleTransform(input, output, client.directory);
|
|
279
|
-
},
|
|
280
|
-
tool: {
|
|
281
|
-
memory_recall: tool({
|
|
282
|
-
description:
|
|
283
|
-
"Search past memories stored for this project. Use before non-trivial work to check for relevant lessons, fixes, and decisions.",
|
|
284
|
-
args: {
|
|
285
|
-
query: z.string().optional(),
|
|
286
|
-
global: z.boolean().optional(),
|
|
287
|
-
limit: z.number().optional(),
|
|
288
|
-
},
|
|
289
|
-
execute: async (args, ctx) => {
|
|
290
|
-
return await recall(args, ctx);
|
|
291
|
-
},
|
|
292
|
-
}),
|
|
293
|
-
memory_remember: tool({
|
|
294
|
-
description:
|
|
295
|
-
"Store a memory for this project. Use after user corrections, architecture decisions, or non-trivial fixes.",
|
|
296
|
-
args: {
|
|
297
|
-
content: z.string().min(10),
|
|
298
|
-
tags: z.array(z.string()).optional(),
|
|
299
|
-
},
|
|
300
|
-
execute: async (args, ctx) => {
|
|
301
|
-
return await remember(args, ctx);
|
|
302
|
-
},
|
|
303
|
-
}),
|
|
304
|
-
},
|
|
305
|
-
dispose,
|
|
306
|
-
};
|
|
307
|
-
}) satisfies Plugin;
|
|
308
|
-
|
|
309
|
-
// Attach internals to the default export instead of as a named export: opencode's
|
|
310
|
-
// plugin loader rejects modules whose exports aren't all plugin entry functions.
|
|
311
|
-
export default Object.assign(plugin, { __internals }) as typeof plugin & {
|
|
335
|
+
// Attach test internals to the default export instead of as a named export
|
|
336
|
+
// (established contract; module exports stay limited to default).
|
|
337
|
+
export default Object.assign(ocpg, { __internals }) as typeof ocpg & {
|
|
312
338
|
__internals: typeof __internals;
|
|
313
339
|
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhi/ocpg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Postgres-backed persistent memory plugin for OpenCode",
|
|
6
6
|
"main": "ocpg.ts",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./ocpg.ts"
|
|
9
|
-
"./server": "./ocpg.ts"
|
|
8
|
+
".": "./ocpg.ts"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
12
11
|
"ocpg.ts"
|
|
13
12
|
],
|
|
14
13
|
"dependencies": {
|
|
15
|
-
"@opencode
|
|
16
|
-
"zod": "^4.1.8"
|
|
14
|
+
"@opencode/plugin": "^2.0.4"
|
|
17
15
|
},
|
|
18
16
|
"keywords": [
|
|
19
17
|
"opencode-plugin",
|