@dzhi/ocpg 0.4.1 → 0.6.0
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 +94 -64
- 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 {
|
|
@@ -171,8 +166,12 @@ async function recall(
|
|
|
171
166
|
? sql``
|
|
172
167
|
: sql`AND project = ${ctx.directory}`;
|
|
173
168
|
const queryCond = args.query
|
|
174
|
-
? sql`AND search_vector @@
|
|
169
|
+
? sql`AND search_vector @@ websearch_to_tsquery('english', ${args.query})`
|
|
175
170
|
: sql``;
|
|
171
|
+
// Relevance-ranked when searching; recency-ordered for a plain project browse.
|
|
172
|
+
const orderBy = args.query
|
|
173
|
+
? sql`ORDER BY ts_rank(search_vector, websearch_to_tsquery('english', ${args.query})) DESC`
|
|
174
|
+
: sql`ORDER BY created_at DESC`;
|
|
176
175
|
|
|
177
176
|
const rows = await sql`
|
|
178
177
|
SELECT id, content, coalesce(tags, '{}') AS tags,
|
|
@@ -180,7 +179,7 @@ async function recall(
|
|
|
180
179
|
project
|
|
181
180
|
FROM memories
|
|
182
181
|
WHERE 1=1 ${projectCond} ${queryCond}
|
|
183
|
-
|
|
182
|
+
${orderBy}
|
|
184
183
|
LIMIT ${limit}
|
|
185
184
|
` as MemoryRow[];
|
|
186
185
|
|
|
@@ -194,13 +193,13 @@ async function recall(
|
|
|
194
193
|
})
|
|
195
194
|
.join('\n---\n');
|
|
196
195
|
} catch (e: unknown) {
|
|
197
|
-
logError(
|
|
196
|
+
logError(`ocpg recall failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
198
197
|
return `ERROR: ${e instanceof Error ? e.message : String(e)}`;
|
|
199
198
|
}
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
async function remember(
|
|
203
|
-
args:
|
|
202
|
+
args: RememberArgs,
|
|
204
203
|
ctx: { directory: string; sessionID: string },
|
|
205
204
|
): Promise<string> {
|
|
206
205
|
try {
|
|
@@ -232,14 +231,14 @@ async function remember(
|
|
|
232
231
|
|
|
233
232
|
const inserted = await sql`
|
|
234
233
|
INSERT INTO memories (content, tags, session_id, project)
|
|
235
|
-
VALUES (${args.content}, ${normalizedTags}, ${ctx.sessionID}, ${ctx.directory})
|
|
234
|
+
VALUES (${args.content}, ${sql.array(normalizedTags)}, ${ctx.sessionID}, ${ctx.directory})
|
|
236
235
|
RETURNING id
|
|
237
236
|
` as { id: number }[];
|
|
238
237
|
|
|
239
238
|
invalidateInjection(ctx.sessionID);
|
|
240
239
|
return `Stored memory #${inserted[0].id} for project ${basename}.`;
|
|
241
240
|
} catch (e: unknown) {
|
|
242
|
-
logError(
|
|
241
|
+
logError(`ocpg remember failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
243
242
|
return `ERROR: ${e instanceof Error ? e.message : String(e)}`;
|
|
244
243
|
}
|
|
245
244
|
}
|
|
@@ -248,6 +247,73 @@ function invalidateInjection(sessionID: string): void {
|
|
|
248
247
|
injectionCache.delete(sessionID);
|
|
249
248
|
}
|
|
250
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
|
+
|
|
251
317
|
const __internals = {
|
|
252
318
|
get sql() {
|
|
253
319
|
return sql;
|
|
@@ -262,48 +328,12 @@ const __internals = {
|
|
|
262
328
|
invalidateInjection,
|
|
263
329
|
logError,
|
|
264
330
|
rateLimitOk,
|
|
265
|
-
|
|
331
|
+
resetRateLimit,
|
|
266
332
|
dispose,
|
|
267
333
|
};
|
|
268
334
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
274
|
-
await handleTransform(input, output, client.directory);
|
|
275
|
-
},
|
|
276
|
-
tool: {
|
|
277
|
-
memory_recall: tool({
|
|
278
|
-
description:
|
|
279
|
-
"Search past memories stored for this project. Use before non-trivial work to check for relevant lessons, fixes, and decisions.",
|
|
280
|
-
args: {
|
|
281
|
-
query: z.string().optional(),
|
|
282
|
-
global: z.boolean().optional(),
|
|
283
|
-
limit: z.number().optional(),
|
|
284
|
-
},
|
|
285
|
-
execute: async (args, ctx) => {
|
|
286
|
-
return await recall(args, ctx);
|
|
287
|
-
},
|
|
288
|
-
}),
|
|
289
|
-
memory_remember: tool({
|
|
290
|
-
description:
|
|
291
|
-
"Store a memory for this project. Use after user corrections, architecture decisions, or non-trivial fixes.",
|
|
292
|
-
args: {
|
|
293
|
-
content: z.string().min(10),
|
|
294
|
-
tags: z.array(z.string()).optional(),
|
|
295
|
-
},
|
|
296
|
-
execute: async (args, ctx) => {
|
|
297
|
-
return await remember(args, ctx);
|
|
298
|
-
},
|
|
299
|
-
}),
|
|
300
|
-
},
|
|
301
|
-
dispose,
|
|
302
|
-
};
|
|
303
|
-
}) satisfies Plugin;
|
|
304
|
-
|
|
305
|
-
// Attach internals to the default export instead of as a named export: opencode's
|
|
306
|
-
// plugin loader rejects modules whose exports aren't all plugin entry functions.
|
|
307
|
-
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 & {
|
|
308
338
|
__internals: typeof __internals;
|
|
309
339
|
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhi/ocpg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|