opencode-self-improvement 0.8.11 → 0.8.12
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 +4 -0
- package/package.json +4 -1
- package/src/index.ts +57 -6
- package/src/runtime.ts +40 -0
package/README.md
CHANGED
|
@@ -225,6 +225,10 @@ Opencode installs the package (and its dependencies) automatically with Bun at
|
|
|
225
225
|
startup. Quit and restart opencode; the plugin loads and `/repo-init` installs
|
|
226
226
|
itself. Nothing else to do.
|
|
227
227
|
|
|
228
|
+
The plugin runs on the Bun embedded in your opencode build; if that Bun is
|
|
229
|
+
older than 1.3.0 (this repo's tested floor) the plugin says so once at startup
|
|
230
|
+
and memory features may misbehave.
|
|
231
|
+
|
|
228
232
|
An unpinned entry resolves `@latest` exactly once and is never re-resolved
|
|
229
233
|
afterwards, so updates are deliberate: `opencode plugin update
|
|
230
234
|
opencode-self-improvement`. To install a specific release, append
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-self-improvement",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.12",
|
|
4
4
|
"description": "Durable memory for opencode: remember facts, decisions, and commands across sessions, plus a one-command per-repo setup (/repo-init).",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"module": "index.ts",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.3.0"
|
|
15
|
+
},
|
|
13
16
|
"author": "Alex Bogartz",
|
|
14
17
|
"repository": {
|
|
15
18
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ import { readLines } from "./logfmt.ts";
|
|
|
68
68
|
import { buildCapturePrompt, flattenTranscript, parseCandidates, parseModelRef } from "./capture.ts";
|
|
69
69
|
import { redactSecrets } from "./secrets.ts";
|
|
70
70
|
import { flattenMemoryText } from "./sanitize.ts";
|
|
71
|
+
import { bunFloorWarning } from "./runtime.ts";
|
|
71
72
|
|
|
72
73
|
// Tool/input boundaries are zod schemas (src/schema.ts): the JSON Schema the
|
|
73
74
|
// host sees is GENERATED from them, and raw args are re-parsed at execution.
|
|
@@ -130,6 +131,7 @@ interface PluginCtx {
|
|
|
130
131
|
delivery?: "steer" | "queue";
|
|
131
132
|
}) => Promise<unknown>;
|
|
132
133
|
context?: (input: { sessionID: string }) => Promise<unknown>;
|
|
134
|
+
get?: (input: { sessionID: string }) => Promise<unknown>;
|
|
133
135
|
};
|
|
134
136
|
event?: {
|
|
135
137
|
subscribe: (o?: { signal?: AbortSignal }) => AsyncIterable<unknown>;
|
|
@@ -167,6 +169,10 @@ function resultMeta(result: unknown): Record<string, unknown> {
|
|
|
167
169
|
|
|
168
170
|
const PLUGIN_ID = "opencode-self-improvement";
|
|
169
171
|
|
|
172
|
+
// The setup-time Bun-floor warning fires once per process (setup can run for
|
|
173
|
+
// more than one location).
|
|
174
|
+
let bunFloorChecked = false;
|
|
175
|
+
|
|
170
176
|
// The real setup implementation, written against the loose local PluginCtx
|
|
171
177
|
// (the only members of the V2 context this plugin actually touches).
|
|
172
178
|
const impl = {
|
|
@@ -186,6 +192,15 @@ const impl = {
|
|
|
186
192
|
const budget = Number(ctx.options?.injectBudget) || injectBudget();
|
|
187
193
|
const openBudget = Number(ctx.options?.frontierBudget) || frontierBudget(budget);
|
|
188
194
|
|
|
195
|
+
// Warn-once Bun floor check (same setup-time warning style as the
|
|
196
|
+
// captureModel warnings below): opencode embeds Bun, and the store code's
|
|
197
|
+
// fs failure modes are Bun-version dependent, so flag an old host.
|
|
198
|
+
if (!bunFloorChecked) {
|
|
199
|
+
bunFloorChecked = true;
|
|
200
|
+
const floorWarning = bunFloorWarning(typeof Bun === "undefined" ? undefined : Bun.version);
|
|
201
|
+
if (floorWarning) console.warn(floorWarning);
|
|
202
|
+
}
|
|
203
|
+
|
|
189
204
|
// Mirror the /repo-init slash command into opencode's global commands dir
|
|
190
205
|
// (idempotent, write-if-missing). OpenCode 2.x loads `*.md` from
|
|
191
206
|
// ~/.config/opencode/commands, so this is the canonical V2 install path.
|
|
@@ -212,8 +227,10 @@ const impl = {
|
|
|
212
227
|
// Identity-event race backstop: a resumed session's FIRST turn can write
|
|
213
228
|
// before the subscription has delivered its execution.started, and the
|
|
214
229
|
// module-scope fallback is the last setup instance — possibly another
|
|
215
|
-
// workspace.
|
|
216
|
-
//
|
|
230
|
+
// workspace. Bind once from the session context API for unmapped sids —
|
|
231
|
+
// but note host 2.1.4 sends message payloads WITHOUT any location field,
|
|
232
|
+
// so this path is a legacy-shape best-effort; the session-row fallback
|
|
233
|
+
// below is what actually resolves serve-host sessions.
|
|
217
234
|
// One-shot: never overwrites an event-bound entry.
|
|
218
235
|
const bindingSids = new Set<string>();
|
|
219
236
|
async function bindSessionFromMessages(sid: string | undefined): Promise<void> {
|
|
@@ -239,6 +256,37 @@ const impl = {
|
|
|
239
256
|
}
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
// Serve-host reality (recorded on 2.1.4 `serve --service`): the plugin
|
|
260
|
+
// instance loads AFTER the session was created, so `session.created` — the
|
|
261
|
+
// only event carrying a location — is never delivered; `execution.started`
|
|
262
|
+
// arrives with just {sessionID}. The session ROW is the remaining source
|
|
263
|
+
// of truth: the host's session get returns `location.directory`. One
|
|
264
|
+
// lookup per session; a miss is remembered so a host without the API is
|
|
265
|
+
// not re-asked on every turn (bounded LRU, same cap as the scope cache).
|
|
266
|
+
const apiBindTried = new Set<string>();
|
|
267
|
+
const apiBindOrder = new SessionLru(SESSION_CAP, (evicted) => apiBindTried.delete(evicted));
|
|
268
|
+
async function bindSessionFromApi(sid: string | undefined): Promise<void> {
|
|
269
|
+
if (!sid || sid === "?" || sessionDir(sid) || apiBindTried.has(sid)) return;
|
|
270
|
+
apiBindTried.add(sid);
|
|
271
|
+
apiBindOrder.touch(sid);
|
|
272
|
+
try {
|
|
273
|
+
const got = (await ctx.session?.get?.({ sessionID: sid })) as Record<string, any> | undefined;
|
|
274
|
+
const row = (got?.data ?? got) as Record<string, any> | undefined;
|
|
275
|
+
const dir = row?.location?.directory ?? row?.directory;
|
|
276
|
+
if (typeof dir === "string" && dir) rememberSessionDir(sid, dir);
|
|
277
|
+
} catch {
|
|
278
|
+
// no session.get on this host (or unknown session): stay unbound
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Identity must resolve BEFORE a row is stamped or a read is partitioned:
|
|
283
|
+
// try the (legacy) message payloads, then the session row.
|
|
284
|
+
async function bindSessionIdentity(sid: string | undefined): Promise<void> {
|
|
285
|
+
if (!sid || sid === "?" || sessionDir(sid)) return;
|
|
286
|
+
await bindSessionFromMessages(sid);
|
|
287
|
+
if (!sessionDir(sid)) await bindSessionFromApi(sid);
|
|
288
|
+
}
|
|
289
|
+
|
|
242
290
|
// ---- tools --------------------------------------------------------------
|
|
243
291
|
await ctx.tool?.transform((editor) => {
|
|
244
292
|
const direct = { codemode: false } as const;
|
|
@@ -258,7 +306,7 @@ const impl = {
|
|
|
258
306
|
status: args.status,
|
|
259
307
|
repo: args.global ? "global" : undefined,
|
|
260
308
|
};
|
|
261
|
-
await
|
|
309
|
+
await bindSessionIdentity(getCurrent().sid);
|
|
262
310
|
const outcome = await runWriteGate(candidate, gateContext());
|
|
263
311
|
if (outcome.action === "promoted") return { content: `Remembered: ${args.type} in ${args.scope}` };
|
|
264
312
|
if (outcome.action === "collapsed") return { content: `Duplicate ignored: ${args.type}/${args.scope}` };
|
|
@@ -276,7 +324,7 @@ const impl = {
|
|
|
276
324
|
"Initialize repository memory and knowledge graph. Ensures the codebase-memory graph for this repo is indexed (one-shot `cli` spawn; no session MCP needed) and re-derives build/lint/test/typecheck command memories through the write gate, then stores an init stamp keyed on git HEAD. Idempotent: a no-op when the stamp matches HEAD, unless force=true (refresh after a refactor).",
|
|
277
325
|
options: direct,
|
|
278
326
|
async execute(args): Promise<{ content: string }> {
|
|
279
|
-
await
|
|
327
|
+
await bindSessionIdentity(getCurrent().sid);
|
|
280
328
|
const summary = await runInit({ liveRoot: gateContext().liveRoot, force: args.force, mode: args.mode });
|
|
281
329
|
return { content: formatInit(summary) };
|
|
282
330
|
},
|
|
@@ -323,7 +371,7 @@ const impl = {
|
|
|
323
371
|
"Update an existing memory (gated like remember; the prior entry is superseded, not erased). `query` selects which row to update: its exact id (ts), a clock fragment (e.g. HH:MM:SS.mmm), or a unique substring of its content. Omitting `query` targets the most recent active row of scope+type. An unmatched or ambiguous `query` fails closed and writes nothing.",
|
|
324
372
|
options: direct,
|
|
325
373
|
async execute(args): Promise<{ content: string }> {
|
|
326
|
-
await
|
|
374
|
+
await bindSessionIdentity(getCurrent().sid);
|
|
327
375
|
const prior = (await loadAll()).filter(
|
|
328
376
|
(m) => m.scope === args.scope && m.type === args.type && isActive(m) && isVisibleHere(m),
|
|
329
377
|
);
|
|
@@ -794,7 +842,7 @@ const impl = {
|
|
|
794
842
|
sessions.touch(sid);
|
|
795
843
|
markTurn(sid);
|
|
796
844
|
await checkNewSession(sid);
|
|
797
|
-
await
|
|
845
|
+
await bindSessionIdentity(sid);
|
|
798
846
|
latestPrompt.set(sid, event?.prompt?.text ?? "");
|
|
799
847
|
await refreshEpoch(sid);
|
|
800
848
|
}
|
|
@@ -850,6 +898,9 @@ const impl = {
|
|
|
850
898
|
const dir =
|
|
851
899
|
info.location?.directory ?? d.location?.directory ?? ev.location?.directory ?? info.directory;
|
|
852
900
|
if (typeof sid === "string" && typeof dir === "string") rememberSessionDir(sid, dir);
|
|
901
|
+
// 2.1.4 serve sends execution.started with no location at all:
|
|
902
|
+
// resolve the session row instead of leaving the session unbound.
|
|
903
|
+
else if (typeof sid === "string") void guarded("session bind (api)", () => bindSessionFromApi(sid));
|
|
853
904
|
continue;
|
|
854
905
|
}
|
|
855
906
|
if (ev?.type !== "message.updated") continue;
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Bun version floor. The plugin runs inside opencode, whose v2 compiled
|
|
2
|
+
// binaries embed Bun (upstream builds with BUN_COMPILE_RELEASE=bun-v1.4.4;
|
|
3
|
+
// older v2 builds embed older bun). The store code's filesystem calls have
|
|
4
|
+
// version-divergent failure modes — e.g. DirHandle.close() behaves differently
|
|
5
|
+
// on some Bun+Windows combos — so an old host shows up as odd memory errors,
|
|
6
|
+
// not as a clean "unsupported" signal. MIN_BUN is the lower bound of the Bun
|
|
7
|
+
// versions this repo has actually been run against: 1.3.10 and 1.4.4. Pure
|
|
8
|
+
// module: nothing here reads the environment; the caller (src/index.ts)
|
|
9
|
+
// injects the version string, so these helpers are unit-testable anywhere.
|
|
10
|
+
|
|
11
|
+
export const MIN_BUN: readonly [number, number, number] = [1, 3, 0];
|
|
12
|
+
|
|
13
|
+
// Pulls the first x.y.z triple out of a version string: "1.4.4",
|
|
14
|
+
// "v1.4.4-canary" and "bun-v1.4.4" all parse; junk or a partial version
|
|
15
|
+
// (no patch component) yields null. Never throws.
|
|
16
|
+
export function parseBunVersion(raw: string): [number, number, number] | null {
|
|
17
|
+
const m = /v?(\d+)\.(\d+)\.(\d+)/.exec(raw);
|
|
18
|
+
if (!m) return null;
|
|
19
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Numeric (not lexical) triple compare: 1.10.0 > 1.3.0.
|
|
23
|
+
function cmpTriples(a: readonly number[], b: readonly number[]): number {
|
|
24
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// One-line startup warning when the host's Bun is below MIN_BUN or its version
|
|
28
|
+
// could not be parsed; null when the version looks fine. Warn-only by design:
|
|
29
|
+
// an old embedded bun should be visible, but must not stop memory from
|
|
30
|
+
// loading. The caller decides how often to emit it (src/index.ts: once).
|
|
31
|
+
export function bunFloorWarning(versionString: string | undefined): string | null {
|
|
32
|
+
const floor = MIN_BUN.join(".");
|
|
33
|
+
const seen = typeof versionString === "string" ? parseBunVersion(versionString) : null;
|
|
34
|
+
if (seen) {
|
|
35
|
+
if (cmpTriples(seen, MIN_BUN) >= 0) return null;
|
|
36
|
+
return `[opencode-self-improvement] Bun ${seen.join(".")} is below the supported minimum ${floor}; memory features may misbehave.`;
|
|
37
|
+
}
|
|
38
|
+
const shown = versionString === undefined ? "unknown" : JSON.stringify(versionString);
|
|
39
|
+
return `[opencode-self-improvement] Bun version ${shown} is unrecognized (supported minimum ${floor}); memory features may misbehave.`;
|
|
40
|
+
}
|