@eleboucher/opencode-memini 0.7.8 → 0.7.10
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/memini.js +110 -55
- package/package.json +1 -1
package/memini.js
CHANGED
|
@@ -55,11 +55,16 @@ const CLIENT_VERSION = readPluginVersion();
|
|
|
55
55
|
|
|
56
56
|
// Auto-update: opencode never re-fetches cached npm plugins, so the plugin
|
|
57
57
|
// checks npm dist-tags once per process and self-updates (same major version
|
|
58
|
-
// only) so the running copy stays current.
|
|
58
|
+
// only) so the running copy stays current. opencode installs each plugin spec
|
|
59
|
+
// into its own isolated wrapper directory under
|
|
60
|
+
// ~/.cache/opencode/packages/<spec>/ (e.g. .../opencode-memini@latest/) — the
|
|
61
|
+
// wrapper holds a package.json listing the plugin as a dependency plus a
|
|
62
|
+
// node_modules/ tree — so the wrapper dir is where we rewrite the pin and
|
|
63
|
+
// re-run npm install.
|
|
59
64
|
const PACKAGE_NAME = "@eleboucher/opencode-memini";
|
|
60
65
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
|
|
61
66
|
const NPM_FETCH_TIMEOUT = 5000;
|
|
62
|
-
const
|
|
67
|
+
const INSTALL_TIMEOUT_MS = 60000;
|
|
63
68
|
let autoUpdateChecked = false;
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -87,23 +92,45 @@ export function compareVersions(a, b) {
|
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
95
|
+
* resolveInstallContextFrom walks up from `startPath` looking for the opencode
|
|
96
|
+
* plugin cache wrapper dir: the first ancestor whose child is a `node_modules`
|
|
97
|
+
* directory AND that also has a `package.json` sibling. Returns
|
|
98
|
+
* { installDir, packageJsonPath } or null. Pure (no `import.meta.url` read) so
|
|
99
|
+
* it can be driven from a synthetic on-disk layout in tests. Exported for
|
|
100
|
+
* testing.
|
|
93
101
|
*/
|
|
94
|
-
function
|
|
102
|
+
export function resolveInstallContextFrom(startPath) {
|
|
95
103
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
let dir = startPath;
|
|
105
|
+
for (let i = 0; i < 20; i++) {
|
|
106
|
+
const nodeModules = join(dir, "node_modules");
|
|
107
|
+
if (existsSync(nodeModules) && statSync(nodeModules).isDirectory()) {
|
|
108
|
+
const packageJsonPath = join(dir, "package.json");
|
|
109
|
+
if (existsSync(packageJsonPath)) {
|
|
110
|
+
return { installDir: dir, packageJsonPath };
|
|
111
|
+
}
|
|
112
|
+
return null; // node_modules found but no package.json — not a wrapper
|
|
113
|
+
}
|
|
114
|
+
const parent = dirname(dir);
|
|
115
|
+
if (parent === dir) break; // reached root
|
|
116
|
+
dir = parent;
|
|
102
117
|
}
|
|
103
118
|
} catch {}
|
|
104
119
|
return null;
|
|
105
120
|
}
|
|
106
121
|
|
|
122
|
+
/**
|
|
123
|
+
* resolveInstallContext finds the opencode plugin cache wrapper directory
|
|
124
|
+
* holding this running plugin instance, by walking up from import.meta.url.
|
|
125
|
+
* Exported for testing.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveInstallContext() {
|
|
128
|
+
try {
|
|
129
|
+
return resolveInstallContextFrom(dirname(fileURLToPath(import.meta.url)));
|
|
130
|
+
} catch {}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
/**
|
|
108
135
|
* fetchLatestVersion queries npm dist-tags with a timeout. Returns the version
|
|
109
136
|
* string or null on failure.
|
|
@@ -129,14 +156,21 @@ async function fetchLatestVersion() {
|
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
/**
|
|
132
|
-
* prepareCacheUpdate rewrites the
|
|
133
|
-
* removes the installed node_modules package, and cleans
|
|
134
|
-
* the installDir on success, null on failure.
|
|
159
|
+
* prepareCacheUpdate rewrites the wrapper package.json to pin the new version,
|
|
160
|
+
* removes the installed node_modules package, and cleans any lockfile so the
|
|
161
|
+
* next install re-fetches. Returns the installDir on success, null on failure.
|
|
162
|
+
*
|
|
163
|
+
* `ctx` is optional: when omitted, resolves from `import.meta.url` (the live
|
|
164
|
+
* path); tests pass a synthetic { installDir, packageJsonPath } so the full
|
|
165
|
+
* rewrite-and-clean flow can be exercised against a temp dir without touching
|
|
166
|
+
* the dev checkout. Exported for testing.
|
|
135
167
|
*/
|
|
136
|
-
function prepareCacheUpdate(newVersion, log) {
|
|
137
|
-
|
|
168
|
+
export function prepareCacheUpdate(newVersion, log, ctx) {
|
|
169
|
+
if (!ctx) {
|
|
170
|
+
ctx = resolveInstallContext();
|
|
171
|
+
}
|
|
138
172
|
if (!ctx) {
|
|
139
|
-
log.
|
|
173
|
+
log.error("auto-update: could not resolve install context");
|
|
140
174
|
return null;
|
|
141
175
|
}
|
|
142
176
|
// Rewrite package.json with the new version pin
|
|
@@ -148,50 +182,44 @@ function prepareCacheUpdate(newVersion, log) {
|
|
|
148
182
|
pkg.dependencies = { ...pkg.dependencies, [PACKAGE_NAME]: newVersion };
|
|
149
183
|
writeFileSync(ctx.packageJsonPath, JSON.stringify(pkg, null, 2));
|
|
150
184
|
} catch (err) {
|
|
151
|
-
log.
|
|
185
|
+
log.error(`auto-update: failed to rewrite cache package.json: ${String(err)}`);
|
|
152
186
|
return null;
|
|
153
187
|
}
|
|
154
|
-
// Remove installed node_modules so
|
|
188
|
+
// Remove installed node_modules so the install re-fetches
|
|
155
189
|
try {
|
|
156
190
|
const pkgDir = join(ctx.installDir, "node_modules", "@eleboucher", "opencode-memini");
|
|
157
191
|
if (existsSync(pkgDir)) rmSync(pkgDir, { recursive: true, force: true });
|
|
158
192
|
} catch (err) {
|
|
159
|
-
log.
|
|
193
|
+
log.error(`auto-update: failed to remove cached node_modules: ${String(err)}`);
|
|
160
194
|
return null;
|
|
161
195
|
}
|
|
162
|
-
// Clean
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
// Clean lockfiles: opencode's installer may write either package-lock.json
|
|
197
|
+
// (npm/arborist) or bun.lock depending on the bundler. Remove both if
|
|
198
|
+
// present; the install regenerates them.
|
|
199
|
+
for (const lockName of ["package-lock.json", "bun.lock"]) {
|
|
200
|
+
const lockPath = join(ctx.installDir, lockName);
|
|
201
|
+
if (!existsSync(lockPath)) continue;
|
|
165
202
|
try {
|
|
166
|
-
|
|
167
|
-
let modified = false;
|
|
168
|
-
if (lock.workspaces?.[""]?.dependencies?.[PACKAGE_NAME]) {
|
|
169
|
-
delete lock.workspaces[""].dependencies[PACKAGE_NAME];
|
|
170
|
-
modified = true;
|
|
171
|
-
}
|
|
172
|
-
if (lock.packages?.[PACKAGE_NAME]) {
|
|
173
|
-
delete lock.packages[PACKAGE_NAME];
|
|
174
|
-
modified = true;
|
|
175
|
-
}
|
|
176
|
-
if (modified) writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
203
|
+
rmSync(lockPath, { force: true });
|
|
177
204
|
} catch {
|
|
178
|
-
//
|
|
179
|
-
// will reconcile.
|
|
205
|
+
// A lock we can't remove isn't fatal — npm install reconciles it.
|
|
180
206
|
}
|
|
181
207
|
}
|
|
182
208
|
return ctx.installDir;
|
|
183
209
|
}
|
|
184
210
|
|
|
185
211
|
/**
|
|
186
|
-
*
|
|
187
|
-
* Returns true on success (exit code 0), false otherwise.
|
|
212
|
+
* runNpmInstall runs `npm install` in the given directory with a timeout.
|
|
213
|
+
* Returns true on success (exit code 0), false otherwise. opencode uses
|
|
214
|
+
* @npmcli/arborist under the hood, so `npm install` matches the lockfile
|
|
215
|
+
* format it produces; `bun install` would rewrite it. Exported for testing.
|
|
188
216
|
*/
|
|
189
|
-
function
|
|
217
|
+
export function runNpmInstall(installDir) {
|
|
190
218
|
try {
|
|
191
|
-
const result = spawnSync(
|
|
219
|
+
const result = spawnSync("npm", ["install"], {
|
|
192
220
|
cwd: installDir,
|
|
193
221
|
stdio: ["ignore", "pipe", "pipe"],
|
|
194
|
-
timeout:
|
|
222
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
195
223
|
});
|
|
196
224
|
return result.status === 0;
|
|
197
225
|
} catch {
|
|
@@ -1098,15 +1126,42 @@ export function lastAssistantFailed(messages) {
|
|
|
1098
1126
|
}
|
|
1099
1127
|
|
|
1100
1128
|
export const MeminiPlugin = async ({ client, worktree, directory }, options) => {
|
|
1129
|
+
// Structured logger is primary; console.error is the fallback when it
|
|
1130
|
+
// throws (absent client.app.log). Direct call so a missing app.log reaches
|
|
1131
|
+
// the catch. Symbols match oh-my-opencode-slim: [ok]/[x]/[!]/[i].
|
|
1132
|
+
const GREEN = "\x1b[32m";
|
|
1133
|
+
const RED = "\x1b[31m";
|
|
1134
|
+
const YELLOW = "\x1b[33m";
|
|
1135
|
+
const BLUE = "\x1b[34m";
|
|
1136
|
+
const RESET = "\x1b[0m";
|
|
1101
1137
|
const log = {
|
|
1138
|
+
error: (message) => {
|
|
1139
|
+
try {
|
|
1140
|
+
client.app.log({ body: { service: "memini", level: "error", message } });
|
|
1141
|
+
} catch {
|
|
1142
|
+
console.error(`${RED}[x]${RESET} [memini] ${message}`);
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1102
1145
|
warn: (message) => {
|
|
1103
|
-
// client.app.log is opencode's structured logger; fall back to stderr.
|
|
1104
1146
|
try {
|
|
1105
|
-
client
|
|
1147
|
+
client.app.log({ body: { service: "memini", level: "warn", message } });
|
|
1148
|
+
} catch {
|
|
1149
|
+
console.error(`${YELLOW}[!]${RESET} [memini] ${message}`);
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
info: (message) => {
|
|
1153
|
+
try {
|
|
1154
|
+
client.app.log({ body: { service: "memini", level: "info", message } });
|
|
1155
|
+
} catch {
|
|
1156
|
+
console.error(`${BLUE}[i]${RESET} [memini] ${message}`);
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
success: (message) => {
|
|
1160
|
+
try {
|
|
1161
|
+
client.app.log({ body: { service: "memini", level: "info", message } });
|
|
1106
1162
|
} catch {
|
|
1107
|
-
|
|
1163
|
+
console.error(`${GREEN}[ok]${RESET} [memini] ${message}`);
|
|
1108
1164
|
}
|
|
1109
|
-
console.error(`[memini] ${message}`);
|
|
1110
1165
|
},
|
|
1111
1166
|
};
|
|
1112
1167
|
|
|
@@ -1212,7 +1267,7 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
1212
1267
|
excludeIds: serverExcludeIds ? excludeIds : [],
|
|
1213
1268
|
onExcludeIdsUnsupported: () => {
|
|
1214
1269
|
serverExcludeIds = false;
|
|
1215
|
-
log.
|
|
1270
|
+
log.info("memini: server does not accept exclude_ids; using client-side dedupe only");
|
|
1216
1271
|
},
|
|
1217
1272
|
});
|
|
1218
1273
|
|
|
@@ -1223,7 +1278,7 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
1223
1278
|
try {
|
|
1224
1279
|
return await fn(...args);
|
|
1225
1280
|
} catch (error) {
|
|
1226
|
-
log.
|
|
1281
|
+
log.error(`${name} hook failed: ${String(error)}`);
|
|
1227
1282
|
}
|
|
1228
1283
|
};
|
|
1229
1284
|
|
|
@@ -1342,7 +1397,7 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
1342
1397
|
result = await Promise.race([settled, budget]);
|
|
1343
1398
|
clearTimeout(timer);
|
|
1344
1399
|
if (result === BUDGET_EXPIRED) {
|
|
1345
|
-
log.
|
|
1400
|
+
log.info(
|
|
1346
1401
|
`recall exceeded its ${live.recall_budget_ms}ms budget; late results will inject next turn`,
|
|
1347
1402
|
);
|
|
1348
1403
|
if (sessionID) {
|
|
@@ -1447,20 +1502,20 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
1447
1502
|
const cur = parseVersion(CLIENT_VERSION);
|
|
1448
1503
|
const nxt = parseVersion(latest);
|
|
1449
1504
|
if (!cur || !nxt || cur.major !== nxt.major) {
|
|
1450
|
-
log.
|
|
1505
|
+
log.info(`auto-update: v${latest} available (major bump — update manually: pin @eleboucher/opencode-memini@${latest} in opencode.json)`);
|
|
1451
1506
|
return;
|
|
1452
1507
|
}
|
|
1453
|
-
log.
|
|
1508
|
+
log.info(`auto-update: updating ${CLIENT_VERSION} → ${latest}`);
|
|
1454
1509
|
const installDir = prepareCacheUpdate(latest, log);
|
|
1455
1510
|
if (!installDir) return;
|
|
1456
|
-
const ok =
|
|
1511
|
+
const ok = runNpmInstall(installDir);
|
|
1457
1512
|
if (ok) {
|
|
1458
|
-
log.
|
|
1513
|
+
log.success(`auto-update: installed v${latest} — restart opencode to apply`);
|
|
1459
1514
|
} else {
|
|
1460
|
-
log.
|
|
1515
|
+
log.error(`auto-update: npm install failed; will retry next session`);
|
|
1461
1516
|
}
|
|
1462
1517
|
} catch (err) {
|
|
1463
|
-
log.
|
|
1518
|
+
log.error(`auto-update: check failed: ${String(err)}`);
|
|
1464
1519
|
}
|
|
1465
1520
|
})();
|
|
1466
1521
|
}
|