@codemem/opencode-plugin 0.43.1 → 0.44.0-alpha.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.
@@ -27,10 +27,20 @@ export const resolveUpgradeGuidance = ({ runner, runnerFrom }) => {
27
27
  };
28
28
  }
29
29
 
30
+ if (normalizedRunner === "codemem") {
31
+ return {
32
+ mode: "global",
33
+ action:
34
+ "Run `npm install -g codemem @codemem/embeddings` to update both the CLI and the optional semantic runtime, then restart OpenCode. On Linux, prefix with `ONNXRUNTIME_NODE_INSTALL=skip` to avoid the unused GPU provider download.",
35
+ note: "detected global codemem runner mode",
36
+ };
37
+ }
38
+
30
39
  if (normalizedRunner === "npx") {
31
40
  return {
32
41
  mode: "npx",
33
- action: "Run `npm install -g codemem` to update, then restart OpenCode.",
42
+ action:
43
+ "Run `npm install -g codemem @codemem/embeddings` to update the CLI and optional semantic runtime, then restart OpenCode. On Linux, prefix with `ONNXRUNTIME_NODE_INSTALL=skip` to avoid the unused GPU provider download.",
34
44
  note: "detected npx runner mode",
35
45
  };
36
46
  }
@@ -106,9 +116,38 @@ const isPinnedGitSource = (runnerFrom) => {
106
116
  }
107
117
  };
108
118
 
109
- export const resolveAutoUpdatePlan = ({ runner, runnerFrom, runnerFromExplicit = false }) => {
119
+ const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
120
+
121
+ const createNpmUpdatePlan = (targetVersion) => {
122
+ const command = [
123
+ "npm",
124
+ "install",
125
+ "-g",
126
+ "--registry",
127
+ PUBLIC_NPM_REGISTRY,
128
+ `--@codemem:registry=${PUBLIC_NPM_REGISTRY}`,
129
+ `codemem@${targetVersion}`,
130
+ `@codemem/embeddings@${targetVersion}`,
131
+ ];
132
+ return { allowed: true, reason: null, command, commandText: command.join(" ") };
133
+ };
134
+
135
+ export const resolveAutoUpdatePlan = ({
136
+ runner,
137
+ runnerFrom,
138
+ runnerFromExplicit = false,
139
+ targetVersion = "latest",
140
+ platform = process.platform,
141
+ }) => {
110
142
  const normalizedRunner = String(runner || "").trim();
111
143
  const source = String(runnerFrom || "").trim();
144
+ const normalizedTargetVersion = String(targetVersion || "").trim();
145
+ if (platform === "win32") {
146
+ return { allowed: false, reason: "unsupported-platform", command: null, commandText: null };
147
+ }
148
+ if (normalizedTargetVersion !== "latest" && !/^\d+\.\d+\.\d+$/.test(normalizedTargetVersion)) {
149
+ return { allowed: false, reason: "invalid-version", command: null, commandText: null };
150
+ }
112
151
  if (isPinnedGitSource(source) || /^codemem@(?!latest$|next$|\*$)[^\s]+$/i.test(source)) {
113
152
  return {
114
153
  allowed: false,
@@ -136,12 +175,7 @@ export const resolveAutoUpdatePlan = ({ runner, runnerFrom, runnerFromExplicit =
136
175
  commandText: null,
137
176
  };
138
177
  }
139
- return {
140
- allowed: true,
141
- reason: null,
142
- command: ["npm", "install", "-g", "codemem@latest"],
143
- commandText: "npm install -g codemem@latest",
144
- };
178
+ return createNpmUpdatePlan(normalizedTargetVersion);
145
179
  }
146
180
 
147
181
  if (normalizedRunner === "uv") {
@@ -163,12 +197,7 @@ export const resolveAutoUpdatePlan = ({ runner, runnerFrom, runnerFromExplicit =
163
197
  }
164
198
 
165
199
  if (normalizedRunner === "codemem") {
166
- return {
167
- allowed: true,
168
- reason: null,
169
- command: ["npm", "install", "-g", "codemem@latest"],
170
- commandText: "npm install -g codemem@latest",
171
- };
200
+ return createNpmUpdatePlan(normalizedTargetVersion);
172
201
  }
173
202
 
174
203
  return { allowed: false, reason: "unknown-runner", command: null, commandText: null };
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  const TRUTHY_VALUES = ["1", "true", "yes"];
18
18
  const DISABLED_VALUES = ["0", "false", "off"];
19
- const PINNED_BACKEND_VERSION = "0.43.1";
19
+ const PINNED_BACKEND_VERSION = "0.44.0-alpha.1";
20
20
  const COMPAT_CHECK_DELAY_MS = 1500;
21
21
  const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
22
22
  const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
@@ -34,6 +34,8 @@ const VIEWER_HEALTH_TIMEOUT_MS = 5_000;
34
34
  const VIEWER_HEALTH_RESTART_THRESHOLD = 3;
35
35
  const VIEWER_HEALTH_RESTART_COOLDOWN_MS = 5 * 60_000;
36
36
  const RAW_EVENTS_STATUS_TIMEOUT_MS = 5_000;
37
+ const DEFAULT_EMBEDDING_MODEL = "Xenova/bge-small-en-v1.5";
38
+ const DEFAULT_EMBEDDING_REVISION = "ea104dacec62c0de699686887e3f920caeb4f3e3";
37
39
 
38
40
  let compatCheckCache = null;
39
41
  const notifiedReleaseVersions = new Set();
@@ -386,6 +388,14 @@ const parseReleaseNotification = (result) => {
386
388
  }
387
389
  };
388
390
 
391
+ const resolveAutoUpdateEnvironment = ({
392
+ platform = process.platform,
393
+ env = process.env,
394
+ } = {}) => {
395
+ if (platform !== "linux") return env;
396
+ return { ...env, ONNXRUNTIME_NODE_INSTALL: "skip" };
397
+ };
398
+
389
399
  const createLogLine = (logPath) => async (line) => {
390
400
  if (!logPath) {
391
401
  return;
@@ -632,6 +642,17 @@ const buildViewerIdentityTarget = (env = process.env, cwd = process.cwd()) => {
632
642
  : trimmed;
633
643
  return resolve(cwd, expanded);
634
644
  };
645
+ const embeddingModel = env.CODEMEM_EMBEDDING_MODEL || DEFAULT_EMBEDDING_MODEL;
646
+ const configuredEmbeddingRevision = env.CODEMEM_EMBEDDING_REVISION?.trim();
647
+ // Match core's configured request identity. The runtime resolves mutable refs
648
+ // to canonical commits before vectors are persisted.
649
+ let embeddingRevision;
650
+ if (configuredEmbeddingRevision) {
651
+ embeddingRevision = configuredEmbeddingRevision;
652
+ } else {
653
+ embeddingRevision =
654
+ embeddingModel === DEFAULT_EMBEDDING_MODEL ? DEFAULT_EMBEDDING_REVISION : null;
655
+ }
635
656
  return {
636
657
  device_id: env.CODEMEM_DEVICE_ID?.trim() || null,
637
658
  actor_id_present: Object.hasOwn(env, "CODEMEM_ACTOR_ID"),
@@ -644,7 +665,11 @@ const buildViewerIdentityTarget = (env = process.env, cwd = process.cwd()) => {
644
665
  embedding_disabled: ["1", "true", "yes"].includes(
645
666
  String(env.CODEMEM_EMBEDDING_DISABLED || "").toLowerCase(),
646
667
  ),
647
- embedding_model: env.CODEMEM_EMBEDDING_MODEL || "Xenova/bge-small-en-v1.5",
668
+ embedding_offline: ["1", "true", "yes"].includes(
669
+ String(env.CODEMEM_EMBEDDING_OFFLINE || "").toLowerCase(),
670
+ ),
671
+ embedding_model: embeddingModel,
672
+ embedding_revision: embeddingRevision,
648
673
  };
649
674
  };
650
675
 
@@ -1704,13 +1729,47 @@ const tsCliAvailable = (cliPath) => {
1704
1729
  }
1705
1730
  };
1706
1731
 
1732
+ // When an explicit npx override targets the codemem package, pair it with the
1733
+ // matching @codemem/embeddings spec so the detached Viewer can resolve the
1734
+ // optional runtime instead of silently falling back to lexical search. Returns
1735
+ // null for any spec that is not a bare `codemem`/`codemem@<version>`, so custom
1736
+ // package overrides pass through unchanged.
1737
+ const pairedEmbeddingsForCodememSpec = (spec) => {
1738
+ if (typeof spec !== "string") return null;
1739
+ const trimmed = spec.trim();
1740
+ if (trimmed === "codemem") return "@codemem/embeddings";
1741
+ const match = /^codemem@([^\s]+)$/.exec(trimmed);
1742
+ if (!match || !/^[A-Za-z0-9~^<>=*][A-Za-z0-9._~^<>=*|-]*$/.test(match[1])) return null;
1743
+ return `@codemem/embeddings@${match[1]}`;
1744
+ };
1745
+
1707
1746
  const buildRunnerArgs = ({ runner, runnerFrom, runnerFromExplicit }) => {
1708
1747
  if (runner === "codemem") {
1709
1748
  return [];
1710
1749
  }
1711
1750
  if (runner === "npx") {
1712
- const pkg = runnerFromExplicit ? runnerFrom : `codemem@${PINNED_BACKEND_VERSION}`;
1713
- return ["-y", pkg];
1751
+ if (runnerFromExplicit) {
1752
+ const pairedEmbeddings = pairedEmbeddingsForCodememSpec(runnerFrom);
1753
+ if (pairedEmbeddings) {
1754
+ return [
1755
+ "-y",
1756
+ "--package",
1757
+ runnerFrom,
1758
+ "--package",
1759
+ pairedEmbeddings,
1760
+ "codemem",
1761
+ ];
1762
+ }
1763
+ return ["-y", runnerFrom];
1764
+ }
1765
+ return [
1766
+ "-y",
1767
+ "--package",
1768
+ `codemem@${PINNED_BACKEND_VERSION}`,
1769
+ "--package",
1770
+ `@codemem/embeddings@${PINNED_BACKEND_VERSION}`,
1771
+ "codemem",
1772
+ ];
1714
1773
  }
1715
1774
  if (runner === "node") {
1716
1775
  const cliPath = runnerFromExplicit
@@ -2421,12 +2480,16 @@ export const CodememPlugin = async ({
2421
2480
  };
2422
2481
 
2423
2482
  const runCommand = async (cmd, options = {}) => {
2424
- const { stdinText = null, timeoutMs = commandTimeout } = options;
2483
+ const {
2484
+ env = process.env,
2485
+ stdinText = null,
2486
+ timeoutMs = commandTimeout,
2487
+ } = options;
2425
2488
  const [command, ...args] = cmd;
2426
2489
  return new Promise((resolve) => {
2427
2490
  const proc = nodeSpawn(command, args, {
2428
2491
  cwd,
2429
- env: process.env,
2492
+ env,
2430
2493
  stdio: ["pipe", "pipe", "pipe"],
2431
2494
  });
2432
2495
  let stdout = "";
@@ -2847,11 +2910,36 @@ export const CodememPlugin = async ({
2847
2910
 
2848
2911
  if (backendUpdatePolicy === "auto") {
2849
2912
  compatibilityAutoUpdateAttempted = true;
2850
- await logLine("compat.auto_update_start cmd=codemem update install --json");
2851
- const updateResult = await runCli(
2852
- ["update", "install", "--json"],
2853
- { timeoutMs: 420_000 }
2913
+ const notification = parseReleaseNotification(
2914
+ await runCli(["update", "check", "--json"])
2854
2915
  );
2916
+ if (!notification?.autoUpdateEligible) {
2917
+ await logLine("compat.auto_update_skipped reason=release_not_eligible");
2918
+ await showToast(
2919
+ `${message}. Auto-update skipped (not eligible). Suggested action: ${guidance.action}`,
2920
+ "warning"
2921
+ );
2922
+ return;
2923
+ }
2924
+ const autoPlan = resolveAutoUpdatePlan({
2925
+ runner,
2926
+ runnerFrom,
2927
+ runnerFromExplicit,
2928
+ targetVersion: notification.latestVersion,
2929
+ });
2930
+ if (!autoPlan.allowed) {
2931
+ await logLine(`compat.auto_update_skipped reason=${autoPlan.reason || "not_eligible"}`);
2932
+ await showToast(
2933
+ `${message}. Auto-update skipped (not eligible). Suggested action: ${guidance.action}`,
2934
+ "warning"
2935
+ );
2936
+ return;
2937
+ }
2938
+ await logLine(`compat.auto_update_start cmd=${autoPlan.commandText}`);
2939
+ const updateResult = await runCommand(autoPlan.command, {
2940
+ env: resolveAutoUpdateEnvironment(),
2941
+ timeoutMs: 480_000,
2942
+ });
2855
2943
  if (updateResult?.exitCode === 0) {
2856
2944
  await logLine(
2857
2945
  `compat.auto_update_result exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog(
@@ -2863,6 +2951,7 @@ export const CodememPlugin = async ({
2863
2951
  const refreshedVersion = (refreshedResult?.stdout || "").trim();
2864
2952
  if (
2865
2953
  refreshedResult?.exitCode === 0
2954
+ && refreshedVersion === notification.latestVersion
2866
2955
  && isVersionAtLeast(refreshedVersion, minVersion)
2867
2956
  ) {
2868
2957
  writeCompatCheckCache(cacheKey, refreshedVersion);
@@ -2891,22 +2980,11 @@ export const CodememPlugin = async ({
2891
2980
  );
2892
2981
  return;
2893
2982
  }
2894
- let installError = null;
2895
- try {
2896
- installError = JSON.parse((updateResult?.stdout || "").trim())?.error || null;
2897
- } catch {
2898
- // Non-JSON output is treated as an installation failure.
2899
- }
2900
- const failureReason = installError === "update_install_locked"
2901
- ? "another update is already running"
2902
- : installError === "update_install_refused"
2903
- ? "not eligible"
2904
- : "installation failed";
2905
2983
  await logLine(
2906
- `compat.auto_update_skipped reason=${installError || "update_install_failed"} exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog((updateResult?.stderr || "").trim())}`
2984
+ `compat.auto_update_skipped reason=update_install_failed exit=${updateResult?.exitCode ?? "unknown"} stderr=${redactLog((updateResult?.stderr || "").trim())}`
2907
2985
  );
2908
2986
  await showToast(
2909
- `${message}. Auto-update skipped (${failureReason}). Suggested action: ${guidance.action}`,
2987
+ `${message}. Auto-update skipped (installation failed). Suggested action: ${guidance.action}`,
2910
2988
  "warning"
2911
2989
  );
2912
2990
  return;
@@ -2916,7 +2994,7 @@ export const CodememPlugin = async ({
2916
2994
  };
2917
2995
 
2918
2996
  const checkForReleaseUpdate = async () => {
2919
- if (backendUpdatePolicy === "off") return;
2997
+ if (backendUpdatePolicy === "off" || compatibilityAutoUpdateAttempted) return;
2920
2998
  const notification = parseReleaseNotification(
2921
2999
  await runCli(["update", "check", "--json"])
2922
3000
  );
@@ -2932,13 +3010,31 @@ export const CodememPlugin = async ({
2932
3010
  && notification.autoUpdateEligible
2933
3011
  && !compatibilityAutoUpdateAttempted
2934
3012
  ) {
2935
- const autoPlan = resolveAutoUpdatePlan({ runner, runnerFrom, runnerFromExplicit });
3013
+ const autoPlan = resolveAutoUpdatePlan({
3014
+ runner,
3015
+ runnerFrom,
3016
+ runnerFromExplicit,
3017
+ targetVersion: notification.latestVersion,
3018
+ });
2936
3019
  if (autoPlan.allowed) {
2937
- const installation = await runCli(
2938
- ["update", "install", "--json"],
2939
- { timeoutMs: 420_000 }
2940
- );
3020
+ await logLine(`release.auto_update_start cmd=${autoPlan.commandText}`);
3021
+ const installation = await runCommand(autoPlan.command, {
3022
+ env: resolveAutoUpdateEnvironment(),
3023
+ timeoutMs: 480_000,
3024
+ });
2941
3025
  if (installation?.exitCode === 0) {
3026
+ const verification = await runCli(["version"]);
3027
+ const installedVersion = (verification?.stdout || "").trim();
3028
+ if (verification?.exitCode !== 0 || installedVersion !== notification.latestVersion) {
3029
+ await logLine(
3030
+ `release.auto_update_verification_failed current=${redactLog(installedVersion || "unknown")} expected=${notification.latestVersion}`
3031
+ );
3032
+ await showToast(
3033
+ `codemem ${notification.latestVersion} was installed, but the active CLI failed verification.`,
3034
+ "warning"
3035
+ );
3036
+ return;
3037
+ }
2942
3038
  const viewerRestart = await restartViewerAfterAutoUpdate();
2943
3039
  await showToast(`Updated codemem to ${notification.latestVersion}.`, "success");
2944
3040
  if (viewerRestart.attempted && !viewerRestart.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemem/opencode-plugin",
3
- "version": "0.43.1",
3
+ "version": "0.44.0-alpha.1",
4
4
  "description": "CodeMem plugin for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -18,7 +18,10 @@
18
18
  ".opencode/lib/"
19
19
  ],
20
20
  "dependencies": {
21
- "@opencode-ai/plugin": "1.18.21"
21
+ "@opencode-ai/plugin": "1.18.25"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.12.4"
22
25
  },
23
26
  "repository": {
24
27
  "type": "git",
@@ -30,7 +33,7 @@
30
33
  "url": "https://github.com/kunickiaj/codemem/issues"
31
34
  },
32
35
  "engines": {
33
- "node": ">=24"
36
+ "node": ">=24.15.0"
34
37
  },
35
38
  "license": "MIT",
36
39
  "publishConfig": {