@cjhyy/code-shell-server 0.9.34 → 0.9.36
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/dist/links/service.d.ts +2 -0
- package/dist/links/service.js +21 -2
- package/dist/mobile-remote/mobile-client-event-validator.js +10 -1
- package/dist/mobile-remote/room-history.d.ts +20 -0
- package/dist/mobile-remote/room-history.js +327 -0
- package/dist/mobile-remote/room-manager.d.ts +11 -0
- package/dist/mobile-remote/room-manager.js +107 -11
- package/dist/panels/runtime-services.js +32 -7
- package/package.json +5 -5
package/dist/links/service.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface LinkServiceOptions {
|
|
|
16
16
|
now?: () => number;
|
|
17
17
|
/** Trusted Host configuration; never accepted from a browser request. */
|
|
18
18
|
remoteLink?: () => RemoteLinkConfiguration | undefined;
|
|
19
|
+
/** Trusted Host opt-in: reuse a uniquely identified user connection after remote OAuth. */
|
|
20
|
+
reuseRemoteConnection?: boolean;
|
|
19
21
|
/** Test seams. Production delegates to Core's fixed provider and CLI executors. */
|
|
20
22
|
validateToken?: typeof validateLocalLinkToken;
|
|
21
23
|
cliStatus?: typeof getCliLinkStatus;
|
package/dist/links/service.js
CHANGED
|
@@ -1164,8 +1164,27 @@ export function createLinkService(options = {}) {
|
|
|
1164
1164
|
};
|
|
1165
1165
|
await change(guarded, () => {
|
|
1166
1166
|
assertPending();
|
|
1167
|
-
|
|
1168
|
-
|
|
1167
|
+
let prepared = job.prepared;
|
|
1168
|
+
if (options.reuseRemoteConnection &&
|
|
1169
|
+
!prepared.expected &&
|
|
1170
|
+
prepared.input.connectionId === undefined) {
|
|
1171
|
+
// Identity comes from authenticated metadata, never a browser-supplied connection ID.
|
|
1172
|
+
const current = records();
|
|
1173
|
+
const matches = current.credentials.filter((entry) => !current.projectIds.has(entry.id) &&
|
|
1174
|
+
isRemoteLinkCredential(entry) &&
|
|
1175
|
+
entry.meta?.linkProvider === credential.meta?.linkProvider &&
|
|
1176
|
+
entry.meta?.linkRemoteIssuer === credential.meta?.linkRemoteIssuer &&
|
|
1177
|
+
entry.meta?.linkRemoteConnectionId === credential.meta?.linkRemoteConnectionId);
|
|
1178
|
+
if (matches.length > 1)
|
|
1179
|
+
throw new LinkServiceError(409, "conflict");
|
|
1180
|
+
if (matches.length === 1) {
|
|
1181
|
+
const expected = matches[0];
|
|
1182
|
+
prepared = { ...prepared, id: expected.id, expected };
|
|
1183
|
+
credential = { ...credential, id: expected.id, label: expected.label };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
swap(prepared, credential, {
|
|
1187
|
+
retireExpected: !!prepared.expected,
|
|
1169
1188
|
adopt: staged,
|
|
1170
1189
|
now: now(),
|
|
1171
1190
|
});
|
|
@@ -135,7 +135,16 @@ export function parseMobileClientEvent(value) {
|
|
|
135
135
|
break;
|
|
136
136
|
case "room.history":
|
|
137
137
|
valid =
|
|
138
|
-
boundedString(event.roomId, MAX_ID_LENGTH) &&
|
|
138
|
+
boundedString(event.roomId, MAX_ID_LENGTH) &&
|
|
139
|
+
optionalNonNegativeInteger(event.sinceSeq) &&
|
|
140
|
+
(event.page === undefined || typeof event.page === "boolean") &&
|
|
141
|
+
optionalBoundedString(event.cursor, 2048) &&
|
|
142
|
+
optionalBoundedString(event.requestId, MAX_ID_LENGTH) &&
|
|
143
|
+
(event.limit === undefined ||
|
|
144
|
+
(Number.isSafeInteger(event.limit) &&
|
|
145
|
+
event.limit >= 1 &&
|
|
146
|
+
event.limit <= 500)) &&
|
|
147
|
+
(event.cursor === undefined || (event.page === true && event.sinceSeq === undefined));
|
|
139
148
|
break;
|
|
140
149
|
case "ccRoom.probe":
|
|
141
150
|
valid =
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Room-local history coordinates. These are never native Session/Run cursors. */
|
|
2
|
+
export interface RoomHistoryPage<T = unknown> {
|
|
3
|
+
snapshotId: string;
|
|
4
|
+
throughSeq: number;
|
|
5
|
+
messages: T[];
|
|
6
|
+
nextCursor?: string;
|
|
7
|
+
complete: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RoomHistoryRequest {
|
|
10
|
+
sinceSeq?: number;
|
|
11
|
+
cursor?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function scanRoomHistory<T extends {
|
|
15
|
+
seq: number;
|
|
16
|
+
}>(file: string, parse: (value: unknown) => T | undefined, visit?: (message: T) => void): number;
|
|
17
|
+
/** Durable immutable copies keep a frozen page boundary valid across Host restarts. */
|
|
18
|
+
export declare function readRoomHistoryPage<T extends {
|
|
19
|
+
seq: number;
|
|
20
|
+
}>(roomDir: string, roomId: string, request: RoomHistoryRequest, parse: (value: unknown) => T | undefined): RoomHistoryPage<T>;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const MAX_BYTES = 256 * 1024 * 1024;
|
|
5
|
+
const MAX_LINE = 4 * 1024 * 1024;
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
7
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
8
|
+
function regular(path) {
|
|
9
|
+
const stat = lstatSync(path);
|
|
10
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
|
|
11
|
+
throw new Error("room history must be a private regular file");
|
|
12
|
+
}
|
|
13
|
+
return stat;
|
|
14
|
+
}
|
|
15
|
+
function stamp(stat) {
|
|
16
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
17
|
+
}
|
|
18
|
+
function openRegular(path) {
|
|
19
|
+
const before = regular(path);
|
|
20
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
21
|
+
if (stamp(before) !== stamp(fstatSync(fd))) {
|
|
22
|
+
closeSync(fd);
|
|
23
|
+
throw new Error("room history changed while opening");
|
|
24
|
+
}
|
|
25
|
+
return fd;
|
|
26
|
+
}
|
|
27
|
+
function syncDirectory(path) {
|
|
28
|
+
if (process.platform === "win32")
|
|
29
|
+
return;
|
|
30
|
+
const fd = openSync(path, "r");
|
|
31
|
+
try {
|
|
32
|
+
fsyncSync(fd);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Bounded memory, strict complete records, exact physical offsets. */
|
|
39
|
+
function* lines(fd, start, end, hashes) {
|
|
40
|
+
let offset = start;
|
|
41
|
+
let carry = Buffer.alloc(0);
|
|
42
|
+
while (offset < end) {
|
|
43
|
+
const blockStart = hashes ? Math.floor(offset / 65536) * 65536 : offset;
|
|
44
|
+
const data = Buffer.allocUnsafe(Math.min(65536, end - blockStart));
|
|
45
|
+
let total = 0;
|
|
46
|
+
while (total < data.length) {
|
|
47
|
+
const count = readSync(fd, data, total, data.length - total, blockStart + total);
|
|
48
|
+
if (!count)
|
|
49
|
+
throw new Error("room history was truncated while reading");
|
|
50
|
+
total += count;
|
|
51
|
+
}
|
|
52
|
+
if (hashes && createHash("sha256").update(data).digest("hex") !== hashes[blockStart / 65536]) {
|
|
53
|
+
throw new Error("room history snapshot checksum mismatch");
|
|
54
|
+
}
|
|
55
|
+
const available = data.subarray(offset - blockStart);
|
|
56
|
+
const count = available.length;
|
|
57
|
+
offset += count;
|
|
58
|
+
const chunk = Buffer.concat([carry, available]);
|
|
59
|
+
let from = 0;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const newline = chunk.indexOf(0x0a, from);
|
|
62
|
+
if (newline < 0)
|
|
63
|
+
break;
|
|
64
|
+
if (newline - from > MAX_LINE)
|
|
65
|
+
throw new Error("room history record exceeds size limit");
|
|
66
|
+
yield {
|
|
67
|
+
text: decoder.decode(chunk.subarray(from, newline)),
|
|
68
|
+
end: offset - chunk.length + newline + 1,
|
|
69
|
+
};
|
|
70
|
+
from = newline + 1;
|
|
71
|
+
}
|
|
72
|
+
carry = Buffer.from(chunk.subarray(from));
|
|
73
|
+
if (carry.length > MAX_LINE)
|
|
74
|
+
throw new Error("room history record exceeds size limit");
|
|
75
|
+
}
|
|
76
|
+
if (carry.length)
|
|
77
|
+
throw new Error("room history has an incomplete final record");
|
|
78
|
+
}
|
|
79
|
+
export function scanRoomHistory(file, parse, visit) {
|
|
80
|
+
const fd = openRegular(file);
|
|
81
|
+
try {
|
|
82
|
+
const before = fstatSync(fd);
|
|
83
|
+
if (before.size > MAX_BYTES)
|
|
84
|
+
throw new Error("room history exceeds 256 MiB; archive it before continuing");
|
|
85
|
+
let seq = 0;
|
|
86
|
+
for (const line of lines(fd, 0, before.size)) {
|
|
87
|
+
const msg = parse(JSON.parse(line.text));
|
|
88
|
+
if (!msg || msg.seq !== seq + 1)
|
|
89
|
+
throw new Error(`room history is corrupt at sequence ${seq + 1}`);
|
|
90
|
+
seq = msg.seq;
|
|
91
|
+
visit?.(msg);
|
|
92
|
+
}
|
|
93
|
+
if (stamp(before) !== stamp(fstatSync(fd)) || stamp(before) !== stamp(regular(file))) {
|
|
94
|
+
throw new Error("room history changed while scanning");
|
|
95
|
+
}
|
|
96
|
+
return seq;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function validateManifest(value, roomId, snapshotId) {
|
|
103
|
+
if (!value ||
|
|
104
|
+
value.version !== 1 ||
|
|
105
|
+
value.roomId !== roomId ||
|
|
106
|
+
value.snapshotId !== snapshotId ||
|
|
107
|
+
!Number.isSafeInteger(value.bytes) ||
|
|
108
|
+
value.bytes < 0 ||
|
|
109
|
+
value.bytes > MAX_BYTES ||
|
|
110
|
+
!Number.isSafeInteger(value.throughSeq) ||
|
|
111
|
+
value.throughSeq < 0 ||
|
|
112
|
+
!Number.isSafeInteger(value.sinceSeq) ||
|
|
113
|
+
value.sinceSeq < 0 ||
|
|
114
|
+
value.sinceSeq > value.throughSeq ||
|
|
115
|
+
typeof value.key !== "string" ||
|
|
116
|
+
!/^[0-9a-f]{64}$/.test(value.key) ||
|
|
117
|
+
typeof value.stamp !== "string" ||
|
|
118
|
+
!/^[0-9:.]+$/.test(value.stamp) ||
|
|
119
|
+
!Array.isArray(value.hashes) ||
|
|
120
|
+
value.hashes.length !== Math.ceil(value.bytes / 65536) ||
|
|
121
|
+
value.hashes.some((hash) => typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash)))
|
|
122
|
+
throw new Error("invalid history manifest");
|
|
123
|
+
}
|
|
124
|
+
/** Durable immutable copies keep a frozen page boundary valid across Host restarts. */
|
|
125
|
+
export function readRoomHistoryPage(roomDir, roomId, request, parse) {
|
|
126
|
+
const limit = request.limit ?? 200;
|
|
127
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
128
|
+
throw new Error("invalid history page limit");
|
|
129
|
+
if (request.sinceSeq !== undefined &&
|
|
130
|
+
(!Number.isSafeInteger(request.sinceSeq) || request.sinceSeq < 0)) {
|
|
131
|
+
throw new Error("invalid room sequence");
|
|
132
|
+
}
|
|
133
|
+
const directory = join(roomDir, "history-snapshots");
|
|
134
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
135
|
+
const directoryStat = lstatSync(directory);
|
|
136
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
|
|
137
|
+
throw new Error("invalid history snapshot directory");
|
|
138
|
+
const assertDirectory = () => {
|
|
139
|
+
const current = lstatSync(directory);
|
|
140
|
+
if (!current.isDirectory() ||
|
|
141
|
+
current.isSymbolicLink() ||
|
|
142
|
+
current.dev !== directoryStat.dev ||
|
|
143
|
+
current.ino !== directoryStat.ino)
|
|
144
|
+
throw new Error("room history snapshot directory changed");
|
|
145
|
+
};
|
|
146
|
+
let manifest;
|
|
147
|
+
let cursor;
|
|
148
|
+
if (request.cursor !== undefined) {
|
|
149
|
+
if (request.sinceSeq !== undefined || request.cursor.length > 2048)
|
|
150
|
+
throw new Error("invalid history cursor");
|
|
151
|
+
const [body, signature, extra] = request.cursor.split(".");
|
|
152
|
+
if (!body || !signature || extra)
|
|
153
|
+
throw new Error("invalid history cursor");
|
|
154
|
+
cursor = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
155
|
+
if (!UUID.test(cursor.snapshotId))
|
|
156
|
+
throw new Error("invalid history snapshot id");
|
|
157
|
+
const path = join(directory, `${cursor.snapshotId}.json`);
|
|
158
|
+
const metaFd = openRegular(path);
|
|
159
|
+
try {
|
|
160
|
+
const before = fstatSync(metaFd);
|
|
161
|
+
if (before.size > 512 * 1024)
|
|
162
|
+
throw new Error("invalid history manifest");
|
|
163
|
+
manifest = JSON.parse(readFileSync(metaFd, "utf8"));
|
|
164
|
+
if (stamp(before) !== stamp(fstatSync(metaFd)) || stamp(before) !== stamp(regular(path)))
|
|
165
|
+
throw new Error("room history manifest changed while reading");
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
closeSync(metaFd);
|
|
169
|
+
}
|
|
170
|
+
validateManifest(manifest, roomId, cursor.snapshotId);
|
|
171
|
+
const expected = createHmac("sha256", manifest.key).update(body).digest();
|
|
172
|
+
const received = Buffer.from(signature, "base64url");
|
|
173
|
+
if (received.length !== expected.length || !timingSafeEqual(expected, received))
|
|
174
|
+
throw new Error("invalid history cursor signature");
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const snapshotId = randomUUID();
|
|
178
|
+
const source = join(roomDir, "messages.jsonl");
|
|
179
|
+
const file = join(directory, `${snapshotId}.jsonl`);
|
|
180
|
+
const input = openRegular(source);
|
|
181
|
+
let output;
|
|
182
|
+
try {
|
|
183
|
+
output = openSync(file, "wx", 0o600);
|
|
184
|
+
const before = fstatSync(input);
|
|
185
|
+
if (before.size > MAX_BYTES)
|
|
186
|
+
throw new Error("room history exceeds 256 MiB; archive it before continuing");
|
|
187
|
+
const hashes = [];
|
|
188
|
+
let offset = 0;
|
|
189
|
+
while (offset < before.size) {
|
|
190
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
191
|
+
let count = 0;
|
|
192
|
+
while (count < chunk.length) {
|
|
193
|
+
const read = readSync(input, chunk, count, chunk.length - count, offset + count);
|
|
194
|
+
if (!read)
|
|
195
|
+
throw new Error("room history was truncated while freezing");
|
|
196
|
+
count += read;
|
|
197
|
+
}
|
|
198
|
+
hashes.push(createHash("sha256").update(chunk).digest("hex"));
|
|
199
|
+
let written = 0;
|
|
200
|
+
while (written < count) {
|
|
201
|
+
const progress = writeSync(output, chunk, written, count - written);
|
|
202
|
+
if (!progress)
|
|
203
|
+
throw new Error("room history freeze write made no progress");
|
|
204
|
+
written += progress;
|
|
205
|
+
}
|
|
206
|
+
offset += count;
|
|
207
|
+
}
|
|
208
|
+
fsyncSync(output);
|
|
209
|
+
if (stamp(before) !== stamp(fstatSync(input)) || stamp(before) !== stamp(regular(source))) {
|
|
210
|
+
throw new Error("room history changed while freezing");
|
|
211
|
+
}
|
|
212
|
+
const throughSeq = scanRoomHistory(file, parse);
|
|
213
|
+
manifest = {
|
|
214
|
+
version: 1,
|
|
215
|
+
roomId,
|
|
216
|
+
snapshotId,
|
|
217
|
+
bytes: before.size,
|
|
218
|
+
throughSeq,
|
|
219
|
+
sinceSeq: request.sinceSeq ?? 0,
|
|
220
|
+
key: randomBytes(32).toString("hex"),
|
|
221
|
+
stamp: stamp(fstatSync(output)),
|
|
222
|
+
hashes,
|
|
223
|
+
};
|
|
224
|
+
const temporary = join(directory, `${snapshotId}.tmp`);
|
|
225
|
+
writeFileSync(temporary, JSON.stringify(manifest), { flag: "wx", mode: 0o600 });
|
|
226
|
+
const metaFd = openRegular(temporary);
|
|
227
|
+
try {
|
|
228
|
+
fsyncSync(metaFd);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
closeSync(metaFd);
|
|
232
|
+
}
|
|
233
|
+
assertDirectory();
|
|
234
|
+
renameSync(temporary, join(directory, `${snapshotId}.json`));
|
|
235
|
+
syncDirectory(directory);
|
|
236
|
+
cursor = { snapshotId, offset: 0, afterSeq: 0 };
|
|
237
|
+
// Old cursors fail explicitly when their retained copy expires. Never
|
|
238
|
+
// remove another in-progress copy or an unknown directory entry.
|
|
239
|
+
try {
|
|
240
|
+
const old = readdirSync(directory)
|
|
241
|
+
.filter((name) => UUID.test(name.replace(/\.json$/, "")) && name.endsWith(".json"))
|
|
242
|
+
.map((name) => ({ name, time: regular(join(directory, name)).mtimeMs }))
|
|
243
|
+
.sort((a, b) => b.time - a.time)
|
|
244
|
+
.slice(8);
|
|
245
|
+
for (const item of old) {
|
|
246
|
+
if (item.name === `${snapshotId}.json`)
|
|
247
|
+
continue;
|
|
248
|
+
rmSync(join(directory, item.name));
|
|
249
|
+
rmSync(join(directory, item.name.replace(/\.json$/, ".jsonl")), { force: true });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* Retention failure must not invalidate the successfully published snapshot. */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
// Remove the publication marker before its data on an unpublished/failed
|
|
258
|
+
// freeze. Retention failures are isolated after successful publication.
|
|
259
|
+
rmSync(join(directory, `${snapshotId}.json`), { force: true });
|
|
260
|
+
rmSync(join(directory, `${snapshotId}.tmp`), { force: true });
|
|
261
|
+
rmSync(file, { force: true });
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
closeSync(input);
|
|
266
|
+
if (output !== undefined)
|
|
267
|
+
closeSync(output);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (manifest.version !== 1 ||
|
|
271
|
+
manifest.roomId !== roomId ||
|
|
272
|
+
manifest.snapshotId !== cursor.snapshotId ||
|
|
273
|
+
!Number.isSafeInteger(manifest.bytes) ||
|
|
274
|
+
manifest.bytes < 0 ||
|
|
275
|
+
manifest.bytes > MAX_BYTES ||
|
|
276
|
+
!Number.isSafeInteger(manifest.throughSeq) ||
|
|
277
|
+
manifest.throughSeq < 0 ||
|
|
278
|
+
!Number.isSafeInteger(cursor.offset) ||
|
|
279
|
+
cursor.offset < 0 ||
|
|
280
|
+
cursor.offset > manifest.bytes ||
|
|
281
|
+
!Number.isSafeInteger(cursor.afterSeq) ||
|
|
282
|
+
cursor.afterSeq < 0 ||
|
|
283
|
+
cursor.afterSeq > manifest.throughSeq) {
|
|
284
|
+
throw new Error("invalid room history snapshot");
|
|
285
|
+
}
|
|
286
|
+
assertDirectory();
|
|
287
|
+
validateManifest(manifest, roomId, cursor.snapshotId);
|
|
288
|
+
const file = join(directory, `${cursor.snapshotId}.jsonl`);
|
|
289
|
+
const fd = openRegular(file);
|
|
290
|
+
const messages = [];
|
|
291
|
+
try {
|
|
292
|
+
if (stamp(fstatSync(fd)) !== manifest.stamp)
|
|
293
|
+
throw new Error("room history snapshot was modified");
|
|
294
|
+
for (const line of lines(fd, cursor.offset, manifest.bytes, manifest.hashes)) {
|
|
295
|
+
const msg = parse(JSON.parse(line.text));
|
|
296
|
+
if (!msg || msg.seq !== cursor.afterSeq + 1)
|
|
297
|
+
throw new Error("room history snapshot is corrupt");
|
|
298
|
+
cursor.offset = line.end;
|
|
299
|
+
cursor.afterSeq = msg.seq;
|
|
300
|
+
if (msg.seq > manifest.sinceSeq)
|
|
301
|
+
messages.push(msg);
|
|
302
|
+
if (messages.length === limit)
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (stamp(fstatSync(fd)) !== manifest.stamp || stamp(regular(file)) !== manifest.stamp)
|
|
306
|
+
throw new Error("room history snapshot changed while paging");
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
closeSync(fd);
|
|
310
|
+
}
|
|
311
|
+
assertDirectory();
|
|
312
|
+
const complete = cursor.offset === manifest.bytes;
|
|
313
|
+
if (complete && cursor.afterSeq !== manifest.throughSeq)
|
|
314
|
+
throw new Error("incomplete room history snapshot");
|
|
315
|
+
const body = Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
316
|
+
return {
|
|
317
|
+
snapshotId: manifest.snapshotId,
|
|
318
|
+
throughSeq: manifest.throughSeq,
|
|
319
|
+
messages,
|
|
320
|
+
complete,
|
|
321
|
+
...(!complete
|
|
322
|
+
? {
|
|
323
|
+
nextCursor: `${body}.${createHmac("sha256", manifest.key).update(body).digest("base64url")}`,
|
|
324
|
+
}
|
|
325
|
+
: {}),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type RoomHistoryRequest, type RoomHistoryPage } from "./room-history.js";
|
|
2
|
+
export type { RoomHistoryPage, RoomHistoryRequest } from "./room-history.js";
|
|
1
3
|
import type { ResidentAgentEvent } from "./resident-agent.js";
|
|
2
4
|
import type { InputAttachmentMeta } from "../attachment-service.js";
|
|
3
5
|
export declare function isValidRoomId(id: unknown): id is string;
|
|
@@ -73,6 +75,8 @@ export interface RoomMessage {
|
|
|
73
75
|
* card expands to so the real parameters are visible. Absent on legacy
|
|
74
76
|
* messages that predate args persistence. */
|
|
75
77
|
args?: Record<string, unknown>;
|
|
78
|
+
/** Durable external transcript provenance, scoped to its actual file incarnation. */
|
|
79
|
+
transcriptSource?: string;
|
|
76
80
|
attachments?: Array<{
|
|
77
81
|
name: string;
|
|
78
82
|
mime?: string;
|
|
@@ -156,6 +160,7 @@ export declare class RoomManager {
|
|
|
156
160
|
/** User turns are echoed immediately for responsive room UX. When a
|
|
157
161
|
* transcript follower later sees the same CLI-written user line, this queue
|
|
158
162
|
* suppresses that duplicate. */
|
|
163
|
+
private readonly transcriptSources;
|
|
159
164
|
private pendingTranscriptUserEchoes;
|
|
160
165
|
/**
|
|
161
166
|
* Rooms whose agent events must be buffered instead of handled immediately.
|
|
@@ -172,12 +177,16 @@ export declare class RoomManager {
|
|
|
172
177
|
* JSONL. Seeded lazily from disk on the first append after startup; dropped
|
|
173
178
|
* when the room's messages are deleted so the next append re-derives it.
|
|
174
179
|
*/
|
|
180
|
+
private readonly messageStampByRoom;
|
|
181
|
+
private messageStamp;
|
|
175
182
|
private lastSeqByRoom;
|
|
176
183
|
/** Throttled lastActiveAt bookkeeping — see touchLastActive. */
|
|
177
184
|
private pendingLastActive;
|
|
178
185
|
private lastActiveWrittenAt;
|
|
179
186
|
private now;
|
|
180
187
|
private readonly rootRealPath;
|
|
188
|
+
private readonly rootIdentity;
|
|
189
|
+
private readonly failedWrites;
|
|
181
190
|
/** Drop all pending AskUser entries for a room. Called when its agent goes
|
|
182
191
|
* away (exit/close) so a request that can no longer be answered doesn't leak
|
|
183
192
|
* in the map (small memory leak) and its approval card doesn't hang until the
|
|
@@ -222,7 +231,9 @@ export declare class RoomManager {
|
|
|
222
231
|
* next open() can continue the same conversation. No-op if unchanged. */
|
|
223
232
|
setRoomSessionId(id: string, sessionId: string): void;
|
|
224
233
|
getMessages(id: string, sinceSeq?: number): RoomMessage[];
|
|
234
|
+
getHistoryPage(id: string, request?: RoomHistoryRequest): RoomHistoryPage<RoomMessage>;
|
|
225
235
|
latestSeq(id: string): number;
|
|
236
|
+
hasTranscriptHistory(id: string): boolean;
|
|
226
237
|
beginTranscriptFollow(id: string): void;
|
|
227
238
|
endTranscriptFollow(id: string): void;
|
|
228
239
|
/** Persist normalized transcript-tail messages through the same seq/history/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { chmodSync, closeSync, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, readdirSync, renameSync, writeFileSync, appendFileSync, rmSync, } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, existsSync, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, readdirSync, renameSync, writeFileSync, appendFileSync, rmSync, } from "node:fs";
|
|
2
|
+
import { readRoomHistoryPage, scanRoomHistory, } from "./room-history.js";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
5
|
/**
|
|
@@ -24,6 +25,17 @@ function isContained(root, target) {
|
|
|
24
25
|
const rel = relative(root, target);
|
|
25
26
|
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
26
27
|
}
|
|
28
|
+
function syncRoomDirectory(path) {
|
|
29
|
+
if (process.platform === "win32")
|
|
30
|
+
return;
|
|
31
|
+
const fd = openSync(path, "r");
|
|
32
|
+
try {
|
|
33
|
+
fsyncSync(fd);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
closeSync(fd);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
27
39
|
function readBoundedUtf8File(path, maxBytes) {
|
|
28
40
|
const fd = openSync(path, "r");
|
|
29
41
|
try {
|
|
@@ -105,7 +117,14 @@ function parseRoomMessage(value) {
|
|
|
105
117
|
raw.type.length > 128) {
|
|
106
118
|
return undefined;
|
|
107
119
|
}
|
|
108
|
-
for (const field of [
|
|
120
|
+
for (const field of [
|
|
121
|
+
"text",
|
|
122
|
+
"tool",
|
|
123
|
+
"summary",
|
|
124
|
+
"reason",
|
|
125
|
+
"toolId",
|
|
126
|
+
"transcriptSource",
|
|
127
|
+
]) {
|
|
109
128
|
const item = raw[field];
|
|
110
129
|
if (item !== undefined && (typeof item !== "string" || item.includes("\0")))
|
|
111
130
|
return undefined;
|
|
@@ -235,6 +254,7 @@ export class RoomManager {
|
|
|
235
254
|
/** User turns are echoed immediately for responsive room UX. When a
|
|
236
255
|
* transcript follower later sees the same CLI-written user line, this queue
|
|
237
256
|
* suppresses that duplicate. */
|
|
257
|
+
transcriptSources = new Map();
|
|
238
258
|
pendingTranscriptUserEchoes = new Map();
|
|
239
259
|
/**
|
|
240
260
|
* Rooms whose agent events must be buffered instead of handled immediately.
|
|
@@ -251,12 +271,19 @@ export class RoomManager {
|
|
|
251
271
|
* JSONL. Seeded lazily from disk on the first append after startup; dropped
|
|
252
272
|
* when the room's messages are deleted so the next append re-derives it.
|
|
253
273
|
*/
|
|
274
|
+
messageStampByRoom = new Map();
|
|
275
|
+
messageStamp(id) {
|
|
276
|
+
const stat = lstatSync(this.msgPath(id));
|
|
277
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
278
|
+
}
|
|
254
279
|
lastSeqByRoom = new Map();
|
|
255
280
|
/** Throttled lastActiveAt bookkeeping — see touchLastActive. */
|
|
256
281
|
pendingLastActive = new Map();
|
|
257
282
|
lastActiveWrittenAt = new Map();
|
|
258
283
|
now;
|
|
259
284
|
rootRealPath;
|
|
285
|
+
rootIdentity;
|
|
286
|
+
failedWrites = new Set();
|
|
260
287
|
/** Drop all pending AskUser entries for a room. Called when its agent goes
|
|
261
288
|
* away (exit/close) so a request that can no longer be answered doesn't leak
|
|
262
289
|
* in the map (small memory leak) and its approval card doesn't hang until the
|
|
@@ -281,6 +308,7 @@ export class RoomManager {
|
|
|
281
308
|
throw new Error(`invalid room storage root: ${opts.rootDir}`);
|
|
282
309
|
}
|
|
283
310
|
this.rootRealPath = realpathSync(opts.rootDir);
|
|
311
|
+
this.rootIdentity = `${rootInfo.dev}:${rootInfo.ino}`;
|
|
284
312
|
if (process.platform !== "win32")
|
|
285
313
|
chmodSync(opts.rootDir, 0o700);
|
|
286
314
|
}
|
|
@@ -288,7 +316,8 @@ export class RoomManager {
|
|
|
288
316
|
const info = lstatSync(this.opts.rootDir);
|
|
289
317
|
if (info.isSymbolicLink() ||
|
|
290
318
|
!info.isDirectory() ||
|
|
291
|
-
realpathSync(this.opts.rootDir) !== this.rootRealPath
|
|
319
|
+
realpathSync(this.opts.rootDir) !== this.rootRealPath ||
|
|
320
|
+
`${info.dev}:${info.ino}` !== this.rootIdentity) {
|
|
292
321
|
throw new Error(`invalid room storage root: ${this.opts.rootDir}`);
|
|
293
322
|
}
|
|
294
323
|
}
|
|
@@ -318,7 +347,7 @@ export class RoomManager {
|
|
|
318
347
|
const path = join(this.roomDir(id), name);
|
|
319
348
|
if (existsSync(path)) {
|
|
320
349
|
const info = lstatSync(path);
|
|
321
|
-
if (info.isSymbolicLink() || !info.isFile())
|
|
350
|
+
if (info.isSymbolicLink() || !info.isFile() || info.nlink !== 1)
|
|
322
351
|
throw new Error(`invalid room file: ${path}`);
|
|
323
352
|
}
|
|
324
353
|
return path;
|
|
@@ -337,7 +366,15 @@ export class RoomManager {
|
|
|
337
366
|
encoding: "utf-8",
|
|
338
367
|
mode: 0o600,
|
|
339
368
|
});
|
|
369
|
+
const fd = openSync(temporary, "r");
|
|
370
|
+
try {
|
|
371
|
+
fsyncSync(fd);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
closeSync(fd);
|
|
375
|
+
}
|
|
340
376
|
renameSync(temporary, target);
|
|
377
|
+
syncRoomDirectory(this.roomDir(id));
|
|
341
378
|
}
|
|
342
379
|
finally {
|
|
343
380
|
try {
|
|
@@ -354,6 +391,7 @@ export class RoomManager {
|
|
|
354
391
|
if (Buffer.byteLength(serialized, "utf8") > MAX_ROOM_MESSAGE_BYTES) {
|
|
355
392
|
throw new Error("room message exceeds the size limit");
|
|
356
393
|
}
|
|
394
|
+
const created = !existsSync(target);
|
|
357
395
|
const fd = openSync(target, "a+", 0o600);
|
|
358
396
|
try {
|
|
359
397
|
if (process.platform !== "win32")
|
|
@@ -367,6 +405,13 @@ export class RoomManager {
|
|
|
367
405
|
prefix = "\n";
|
|
368
406
|
}
|
|
369
407
|
appendFileSync(fd, `${prefix}${serialized}\n`, "utf-8");
|
|
408
|
+
fsyncSync(fd);
|
|
409
|
+
if (created)
|
|
410
|
+
syncRoomDirectory(this.roomDir(id));
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
this.failedWrites.add(id);
|
|
414
|
+
throw error;
|
|
370
415
|
}
|
|
371
416
|
finally {
|
|
372
417
|
closeSync(fd);
|
|
@@ -408,6 +453,7 @@ export class RoomManager {
|
|
|
408
453
|
linkedSessionMode: input.linkedSessionMode,
|
|
409
454
|
};
|
|
410
455
|
mkdirSync(this.roomDir(id), { recursive: true, mode: 0o700 });
|
|
456
|
+
syncRoomDirectory(this.opts.rootDir);
|
|
411
457
|
this.writeRoomMeta(id, meta);
|
|
412
458
|
// Audit anchor: the first line records how the room was opened (cwd +
|
|
413
459
|
// permission mode), so messages.jsonl is self-describing for "what could
|
|
@@ -466,14 +512,20 @@ export class RoomManager {
|
|
|
466
512
|
// host's event loop: a long-lived room progressively slowed down new
|
|
467
513
|
// messages, heartbeats and every other WebSocket client. (Measured: ~32s to
|
|
468
514
|
// write 2,000 short messages.)
|
|
515
|
+
if (this.failedWrites.has(id))
|
|
516
|
+
throw new Error("room persistence failed; reopen after inspecting its history");
|
|
469
517
|
const cached = this.lastSeqByRoom.get(id);
|
|
470
518
|
if (cached !== undefined) {
|
|
519
|
+
if (this.messageStampByRoom.get(id) !== this.messageStamp(id)) {
|
|
520
|
+
this.failedWrites.add(id);
|
|
521
|
+
throw new Error("room history changed outside this writer; inspect it before reopening");
|
|
522
|
+
}
|
|
471
523
|
const next = cached + 1;
|
|
472
524
|
this.lastSeqByRoom.set(id, next);
|
|
473
525
|
return next;
|
|
474
526
|
}
|
|
475
|
-
const
|
|
476
|
-
const next =
|
|
527
|
+
const path = this.msgPath(id);
|
|
528
|
+
const next = existsSync(path) ? scanRoomHistory(path, parseRoomMessage) + 1 : 1;
|
|
477
529
|
this.lastSeqByRoom.set(id, next);
|
|
478
530
|
return next;
|
|
479
531
|
}
|
|
@@ -484,6 +536,7 @@ export class RoomManager {
|
|
|
484
536
|
const msg = { seq, ts, ...partial };
|
|
485
537
|
try {
|
|
486
538
|
this.appendRoomMessage(id, msg);
|
|
539
|
+
this.messageStampByRoom.set(id, this.messageStamp(id));
|
|
487
540
|
}
|
|
488
541
|
catch (error) {
|
|
489
542
|
// Sequence assignment is a commit effect, not an attempt counter. A
|
|
@@ -601,8 +654,29 @@ export class RoomManager {
|
|
|
601
654
|
closeSync(fd);
|
|
602
655
|
}
|
|
603
656
|
}
|
|
657
|
+
getHistoryPage(id, request = {}) {
|
|
658
|
+
if (!this.getRoom(id))
|
|
659
|
+
throw new Error("room is missing or its metadata is corrupt");
|
|
660
|
+
return readRoomHistoryPage(this.roomDir(id), id, request, parseRoomMessage);
|
|
661
|
+
}
|
|
604
662
|
latestSeq(id) {
|
|
605
|
-
|
|
663
|
+
if (this.failedWrites.has(id))
|
|
664
|
+
throw new Error("room persistence failed");
|
|
665
|
+
const cached = this.lastSeqByRoom.get(id);
|
|
666
|
+
if (cached !== undefined)
|
|
667
|
+
return cached;
|
|
668
|
+
const path = this.msgPath(id);
|
|
669
|
+
return existsSync(path) ? scanRoomHistory(path, parseRoomMessage) : 0;
|
|
670
|
+
}
|
|
671
|
+
hasTranscriptHistory(id) {
|
|
672
|
+
let found = false;
|
|
673
|
+
const file = this.msgPath(id);
|
|
674
|
+
if (existsSync(file))
|
|
675
|
+
scanRoomHistory(file, parseRoomMessage, (message) => {
|
|
676
|
+
if (message.transcriptSource)
|
|
677
|
+
found = true;
|
|
678
|
+
});
|
|
679
|
+
return found;
|
|
606
680
|
}
|
|
607
681
|
beginTranscriptFollow(id) {
|
|
608
682
|
if (this.getRoom(id))
|
|
@@ -614,9 +688,30 @@ export class RoomManager {
|
|
|
614
688
|
/** Persist normalized transcript-tail messages through the same seq/history/
|
|
615
689
|
* broadcast path as resident-agent output. */
|
|
616
690
|
ingestTranscriptMessages(id, messages) {
|
|
617
|
-
if (!
|
|
691
|
+
if (!messages.length)
|
|
618
692
|
return;
|
|
693
|
+
if (!this.transcriptFollowedRooms.has(id) || !this.getRoom(id))
|
|
694
|
+
throw new Error("transcript room is not active");
|
|
695
|
+
let sources = this.transcriptSources.get(id);
|
|
696
|
+
if (!sources) {
|
|
697
|
+
sources = new Set();
|
|
698
|
+
const path = this.msgPath(id);
|
|
699
|
+
if (existsSync(path))
|
|
700
|
+
scanRoomHistory(path, parseRoomMessage, (message) => {
|
|
701
|
+
if (message.transcriptSource)
|
|
702
|
+
sources.add(message.transcriptSource);
|
|
703
|
+
});
|
|
704
|
+
this.transcriptSources.set(id, sources);
|
|
705
|
+
}
|
|
619
706
|
for (const message of messages) {
|
|
707
|
+
const source = message.transcriptSource;
|
|
708
|
+
if (source !== undefined &&
|
|
709
|
+
(typeof source !== "string" ||
|
|
710
|
+
source.length > 200 ||
|
|
711
|
+
!/^[0-9a-f-]{36}:[0-9]+:[0-9]+$/.test(source)))
|
|
712
|
+
throw new Error("invalid transcript source coordinate");
|
|
713
|
+
if (source && sources.has(source))
|
|
714
|
+
continue;
|
|
620
715
|
if (message.from === "user" && typeof message.text === "string") {
|
|
621
716
|
const text = stripRoomAttachmentBlock(message.text);
|
|
622
717
|
const pending = this.pendingTranscriptUserEchoes.get(id);
|
|
@@ -628,15 +723,16 @@ export class RoomManager {
|
|
|
628
723
|
pending.shift();
|
|
629
724
|
if (pending?.length === 0)
|
|
630
725
|
this.pendingTranscriptUserEchoes.delete(id);
|
|
631
|
-
// `send` already persisted and broadcast this exact user turn. The
|
|
632
|
-
// transcript remains authoritative for agent/tool output, but must not
|
|
633
|
-
// render a second copy of the prompt.
|
|
634
726
|
if (!matched)
|
|
635
727
|
this.append(id, { ...message, text });
|
|
728
|
+
else if (source)
|
|
729
|
+
this.append(id, { from: "system", type: "transcript_echo", transcriptSource: source });
|
|
636
730
|
}
|
|
637
731
|
else {
|
|
638
732
|
this.append(id, message);
|
|
639
733
|
}
|
|
734
|
+
if (source)
|
|
735
|
+
sources.add(source);
|
|
640
736
|
}
|
|
641
737
|
}
|
|
642
738
|
/**
|
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { chmod, link, lstat, mkdir, open, opendir, realpath, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
-
import { acquireLockOnPath } from "@cjhyy/code-shell-core/internal";
|
|
5
|
+
import { acquireLockOnPath, acquireWorkspaceWriteLock } from "@cjhyy/code-shell-core/internal";
|
|
6
6
|
import { DEFAULT_PANEL_APP_STORAGE_QUOTA_BYTES, panelAppStorageSnapshot, panelAppStorageChange, applyPanelAppStorageChange, panelAppStorageKey, panelAppStoragePath, preparePanelAppStorage, readPanelAppStorage, writePanelAppStorage, } from "./storage-store.js";
|
|
7
7
|
const MAX_READ_BYTES = 480 * 1024;
|
|
8
8
|
const MAX_WRITE_BYTES = 384 * 1024;
|
|
@@ -390,7 +390,15 @@ export class PanelRuntimeServices {
|
|
|
390
390
|
const parent = await this.openPath(root, dirname(path), true);
|
|
391
391
|
const stableTarget = join(parent.path, basename(path));
|
|
392
392
|
const temporary = join(parent.path, `.${basename(path)}.${randomUUID()}.tmp`);
|
|
393
|
+
let releaseWorkspaceLock;
|
|
394
|
+
let outcome;
|
|
393
395
|
try {
|
|
396
|
+
await parent.verify();
|
|
397
|
+
releaseWorkspaceLock = await acquireWorkspaceWriteLock(parent.path, basename(path));
|
|
398
|
+
await parent.verify();
|
|
399
|
+
await this.root(scope);
|
|
400
|
+
await authorized(scope);
|
|
401
|
+
await parent.verify();
|
|
394
402
|
let modifiedAt = null, currentRevision = null, mode = 0o644;
|
|
395
403
|
let existing;
|
|
396
404
|
try {
|
|
@@ -459,17 +467,34 @@ export class PanelRuntimeServices {
|
|
|
459
467
|
await rename(temporary, stableTarget);
|
|
460
468
|
await chmod(stableTarget, mode).catch(() => { });
|
|
461
469
|
const metadata = await lstat(stableTarget);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
470
|
+
outcome = {
|
|
471
|
+
ok: true,
|
|
472
|
+
value: {
|
|
473
|
+
path,
|
|
474
|
+
size: bytes.length,
|
|
475
|
+
modifiedAt: metadata.mtimeMs,
|
|
476
|
+
revision: revision(bytes),
|
|
477
|
+
},
|
|
467
478
|
};
|
|
468
479
|
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
outcome = { ok: false, error };
|
|
482
|
+
}
|
|
483
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
484
|
+
try {
|
|
485
|
+
await releaseWorkspaceLock?.();
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
if (!outcome.ok)
|
|
489
|
+
throw new AggregateError([outcome.error, error], `${String(outcome.error)}; ${String(error)}`, { cause: error });
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
469
492
|
finally {
|
|
470
|
-
await rm(temporary, { force: true }).catch(() => { });
|
|
471
493
|
await parent.close();
|
|
472
494
|
}
|
|
495
|
+
if (!outcome.ok)
|
|
496
|
+
throw outcome.error;
|
|
497
|
+
return outcome.value;
|
|
473
498
|
});
|
|
474
499
|
}
|
|
475
500
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.36",
|
|
4
4
|
"description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@cjhyy/code-shell-capability-coding": "0.9.
|
|
64
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
65
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
63
|
+
"@cjhyy/code-shell-capability-coding": "0.9.36",
|
|
64
|
+
"@cjhyy/code-shell-core": "0.9.36",
|
|
65
|
+
"@cjhyy/code-shell-web": "0.9.36",
|
|
66
66
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
67
67
|
"@types/ws": "^8.18.1",
|
|
68
68
|
"ws": "^8.21.0",
|
|
69
|
-
"@cjhyy/code-shell-link": "0.9.
|
|
69
|
+
"@cjhyy/code-shell-link": "0.9.36"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=20.10"
|