@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.
- package/.opencode/lib/compat.js +48 -14
- package/.opencode/lib/raw-event-spool.js +197 -0
- package/.opencode/plugins/codemem.js +534 -241
- package/package.json +5 -2
package/.opencode/lib/compat.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
167
|
-
allowed:
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
};
|