@codemem/opencode-plugin 0.43.2 → 0.44.0-beta.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` to update the CLI and its 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` to update the CLI and its 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,37 @@ 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
+ const UPDATE_TARGET_VERSION =
121
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:alpha|beta|rc)(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
122
+
123
+ const createNpmUpdatePlan = (targetVersion) => {
124
+ const command = [
125
+ "npm",
126
+ "install",
127
+ "-g",
128
+ "--registry",
129
+ PUBLIC_NPM_REGISTRY,
130
+ `--@codemem:registry=${PUBLIC_NPM_REGISTRY}`,
131
+ `codemem@${targetVersion}`,
132
+ `@codemem/embeddings@${targetVersion}`,
133
+ ];
134
+ return { allowed: true, reason: null, command, commandText: command.join(" ") };
135
+ };
136
+
137
+ export const resolveAutoUpdatePlan = ({
138
+ runner,
139
+ runnerFrom,
140
+ runnerFromExplicit = false,
141
+ targetVersion,
142
+ platform = process.platform,
143
+ }) => {
110
144
  const normalizedRunner = String(runner || "").trim();
111
145
  const source = String(runnerFrom || "").trim();
146
+ const normalizedTargetVersion = String(targetVersion || "").trim();
147
+ if (platform === "win32") {
148
+ return { allowed: false, reason: "unsupported-platform", command: null, commandText: null };
149
+ }
112
150
  if (isPinnedGitSource(source) || /^codemem@(?!latest$|next$|\*$)[^\s]+$/i.test(source)) {
113
151
  return {
114
152
  allowed: false,
@@ -136,12 +174,10 @@ export const resolveAutoUpdatePlan = ({ runner, runnerFrom, runnerFromExplicit =
136
174
  commandText: null,
137
175
  };
138
176
  }
139
- return {
140
- allowed: true,
141
- reason: null,
142
- command: ["npm", "install", "-g", "codemem@latest"],
143
- commandText: "npm install -g codemem@latest",
144
- };
177
+ if (!UPDATE_TARGET_VERSION.test(normalizedTargetVersion)) {
178
+ return { allowed: false, reason: "invalid-version", command: null, commandText: null };
179
+ }
180
+ return createNpmUpdatePlan(normalizedTargetVersion);
145
181
  }
146
182
 
147
183
  if (normalizedRunner === "uv") {
@@ -163,12 +199,10 @@ export const resolveAutoUpdatePlan = ({ runner, runnerFrom, runnerFromExplicit =
163
199
  }
164
200
 
165
201
  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
- };
202
+ if (!UPDATE_TARGET_VERSION.test(normalizedTargetVersion)) {
203
+ return { allowed: false, reason: "invalid-version", command: null, commandText: null };
204
+ }
205
+ return createNpmUpdatePlan(normalizedTargetVersion);
172
206
  }
173
207
 
174
208
  return { allowed: false, reason: "unknown-runner", command: null, commandText: null };
@@ -0,0 +1,197 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmod,
4
+ link,
5
+ mkdir,
6
+ readFile,
7
+ readdir,
8
+ rm,
9
+ stat,
10
+ writeFile,
11
+ } from "node:fs/promises";
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ const DEFAULT_DRAIN_LIMIT = 20;
16
+ const DEFAULT_MAX_ENTRIES = 2000;
17
+ const RAW_EVENT_SPOOL_FULL_CODE = "raw_event_spool_full";
18
+ const SPOOL_DIRECTORY_NAME = "opencode-raw-event-spool";
19
+ const spoolWritesInFlight = new Map();
20
+
21
+ const resolveSpoolDirectory = (homeDir = homedir()) =>
22
+ join(homeDir, ".codemem", SPOOL_DIRECTORY_NAME);
23
+
24
+ const spoolFilename = (eventId) => {
25
+ const digest = createHash("sha256").update(String(eventId)).digest("hex");
26
+ return `${digest}.json`;
27
+ };
28
+
29
+ const requireEventId = (envelope) => {
30
+ const eventId = typeof envelope?.event_id === "string"
31
+ ? envelope.event_id.trim()
32
+ : "";
33
+ if (!eventId) {
34
+ throw new Error("raw event spool requires event_id");
35
+ }
36
+ return eventId;
37
+ };
38
+
39
+ const ensurePrivateDirectory = async (directory) => {
40
+ await mkdir(directory, { recursive: true, mode: 0o700 });
41
+ await chmod(directory, 0o700);
42
+ };
43
+
44
+ const writeRawEventSpoolEntryUnlocked = async ({
45
+ envelope,
46
+ serialized,
47
+ homeDir = homedir(),
48
+ maxEntries = DEFAULT_MAX_ENTRIES,
49
+ }) => {
50
+ const eventId = requireEventId(envelope);
51
+ const directory = resolveSpoolDirectory(homeDir);
52
+ const bytes = typeof serialized === "string" ? serialized : JSON.stringify(envelope);
53
+ const normalizedMaxEntries = Number.isFinite(maxEntries) && maxEntries > 0
54
+ ? Math.trunc(maxEntries)
55
+ : DEFAULT_MAX_ENTRIES;
56
+ const destination = join(directory, spoolFilename(eventId));
57
+ const temporary = join(
58
+ directory,
59
+ `.${spoolFilename(eventId)}.${process.pid}.${randomUUID()}.tmp`,
60
+ );
61
+
62
+ await ensurePrivateDirectory(directory);
63
+ try {
64
+ const existing = await readFile(destination, "utf8");
65
+ if (existing !== bytes) {
66
+ throw new Error("raw event spool entry conflicts with existing event_id");
67
+ }
68
+ await chmod(destination, 0o600);
69
+ return { eventId, serialized: existing };
70
+ } catch (error) {
71
+ if (error?.code !== "ENOENT") {
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ const directoryEntries = await readdir(directory, { withFileTypes: true });
77
+ const entryCount = directoryEntries.filter(
78
+ (entry) => entry.isFile() && entry.name.endsWith(".json"),
79
+ ).length;
80
+ if (entryCount >= normalizedMaxEntries) {
81
+ const error = new Error("raw event spool is full");
82
+ error.code = RAW_EVENT_SPOOL_FULL_CODE;
83
+ throw error;
84
+ }
85
+
86
+ try {
87
+ await writeFile(temporary, bytes, { encoding: "utf8", mode: 0o600, flag: "wx" });
88
+ try {
89
+ await link(temporary, destination);
90
+ } catch (error) {
91
+ if (error?.code !== "EEXIST") {
92
+ throw error;
93
+ }
94
+ const existing = await readFile(destination, "utf8");
95
+ if (existing !== bytes) {
96
+ throw new Error("raw event spool entry conflicts with existing event_id");
97
+ }
98
+ await chmod(destination, 0o600);
99
+ return { eventId, serialized: existing };
100
+ }
101
+ await chmod(destination, 0o600);
102
+ } finally {
103
+ await rm(temporary, { force: true }).catch(() => {});
104
+ }
105
+
106
+ return { eventId, serialized: bytes };
107
+ };
108
+
109
+ const writeRawEventSpoolEntry = async (options) => {
110
+ const directory = resolveSpoolDirectory(options?.homeDir);
111
+ const previousWrite = spoolWritesInFlight.get(directory);
112
+ const writePromise = (previousWrite ? previousWrite.catch(() => {}) : Promise.resolve())
113
+ .then(() => writeRawEventSpoolEntryUnlocked(options));
114
+ const trackedWrite = writePromise.finally(() => {
115
+ if (spoolWritesInFlight.get(directory) === trackedWrite) {
116
+ spoolWritesInFlight.delete(directory);
117
+ }
118
+ });
119
+ spoolWritesInFlight.set(directory, trackedWrite);
120
+ return trackedWrite;
121
+ };
122
+
123
+ const removeRawEventSpoolEntry = async ({ eventId, homeDir = homedir() }) => {
124
+ const normalizedEventId = requireEventId({ event_id: eventId });
125
+ await rm(
126
+ join(resolveSpoolDirectory(homeDir), spoolFilename(normalizedEventId)),
127
+ { force: true },
128
+ );
129
+ };
130
+
131
+ const loadRawEventSpoolEntries = async ({
132
+ homeDir = homedir(),
133
+ limit = DEFAULT_DRAIN_LIMIT,
134
+ } = {}) => {
135
+ const directory = resolveSpoolDirectory(homeDir);
136
+ let directoryEntries;
137
+ try {
138
+ directoryEntries = await readdir(directory, { withFileTypes: true });
139
+ } catch (error) {
140
+ if (error?.code === "ENOENT") {
141
+ return { entries: [], corruptCount: 0 };
142
+ }
143
+ throw error;
144
+ }
145
+
146
+ const candidates = [];
147
+ for (const directoryEntry of directoryEntries) {
148
+ if (!directoryEntry.isFile() || !directoryEntry.name.endsWith(".json")) {
149
+ continue;
150
+ }
151
+ const path = join(directory, directoryEntry.name);
152
+ try {
153
+ const details = await stat(path);
154
+ candidates.push({ path, mtimeMs: details.mtimeMs, name: directoryEntry.name });
155
+ } catch {
156
+ // A concurrently removed entry needs no recovery attempt.
157
+ }
158
+ }
159
+ candidates.sort(
160
+ (left, right) => left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name),
161
+ );
162
+
163
+ const entries = [];
164
+ let corruptCount = 0;
165
+ const normalizedLimit = Number.isFinite(limit) && limit > 0
166
+ ? Math.trunc(limit)
167
+ : 0;
168
+ for (const candidate of candidates) {
169
+ if (entries.length >= normalizedLimit) {
170
+ break;
171
+ }
172
+ try {
173
+ const serialized = await readFile(candidate.path, "utf8");
174
+ const envelope = JSON.parse(serialized);
175
+ const eventId = requireEventId(envelope);
176
+ if (candidate.name !== spoolFilename(eventId)) {
177
+ corruptCount += 1;
178
+ continue;
179
+ }
180
+ entries.push({ eventId, envelope, serialized });
181
+ } catch {
182
+ corruptCount += 1;
183
+ }
184
+ }
185
+
186
+ return { entries, corruptCount };
187
+ };
188
+
189
+ export {
190
+ DEFAULT_DRAIN_LIMIT,
191
+ DEFAULT_MAX_ENTRIES,
192
+ RAW_EVENT_SPOOL_FULL_CODE,
193
+ loadRawEventSpoolEntries,
194
+ removeRawEventSpoolEntry,
195
+ resolveSpoolDirectory,
196
+ writeRawEventSpoolEntry,
197
+ };