deepspace 0.20.0 → 0.21.0
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/CHANGELOG.md +18 -0
- package/dist/{chunk-A7UFIOWO.js → chunk-KDCDRHBA.js} +7 -2
- package/dist/chunk-KDCDRHBA.js.map +1 -0
- package/dist/cli.js +193 -62
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.d.ts +3 -2
- package/dist/documentation-client-core.js +1 -1
- package/dist/documentation-default-renderer.cjs +48 -48
- package/dist/documentation-react.d.ts +2 -1
- package/dist/documentation-react.js +1 -1
- package/dist/documentation-runtime.js +7 -7
- package/dist/documentation-server-core.d.ts +3 -2
- package/dist/documentation-server-core.js +1 -1
- package/dist/documentation.d.ts +40 -12
- package/dist/documentation.js +40 -7
- package/dist/documentation.js.map +1 -1
- package/dist/index.d.ts +225 -21
- package/dist/index.js +239 -100
- package/dist/index.js.map +1 -1
- package/dist/{public-CFAj1hL2.d.ts → public-DenQ1TsZ.d.ts} +36 -11
- package/dist/server.d.ts +21 -0
- package/dist/server.js +148 -39
- package/dist/server.js.map +1 -1
- package/dist/testing-mcp.d.ts +69 -0
- package/dist/testing-mcp.js +99 -0
- package/dist/testing-mcp.js.map +1 -0
- package/dist/testing.d.ts +17 -20
- package/dist/testing.js +47 -25
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +48 -3
- package/dist/worker.js +234 -62
- package/dist/worker.js.map +1 -1
- package/features/ai-chat/feature.json +2 -2
- package/features/ai-chat/src/AiChatPage.tsx +56 -21
- package/features/ai-chat/src/ChatPanel.tsx +33 -10
- package/features/ai-chat/src/__tests__/ChatPanel.lifecycle.test.tsx +156 -0
- package/features/cron/src/CronLogPage.tsx +107 -69
- package/features/file-attachments/feature.json +2 -1
- package/features/file-manager/feature.json +2 -1
- package/features/file-manager/src/FileManagerPage.tsx +227 -194
- package/features/integration-test/src/IntegrationTestPage.tsx +54 -34
- package/package.json +5 -1
- package/dist/chunk-A7UFIOWO.js.map +0 -1
package/dist/worker.js
CHANGED
|
@@ -351,6 +351,9 @@ var MSG = {
|
|
|
351
351
|
CRON_PAUSE: "cron.pause",
|
|
352
352
|
CRON_RESUME: "cron.resume",
|
|
353
353
|
CRON_STATUS: "cron.status",
|
|
354
|
+
// Per-mutation receipt, sent only to the socket whose trigger/pause/
|
|
355
|
+
// resume carried a `requestId` — frames without one stay fire-and-forget.
|
|
356
|
+
CRON_ACK: "cron.ack",
|
|
354
357
|
// ---- JobRoom ---------------------------------------------------------
|
|
355
358
|
// Background-job queue with durable, alarm-driven execution. See
|
|
356
359
|
// server/rooms/job-room.ts. Clients enqueue and subscribe to updates;
|
|
@@ -1063,9 +1066,9 @@ var clientBuild = {
|
|
|
1063
1066
|
canvasUndo: () => ({ type: MSG.CANVAS_UNDO, payload: EMPTY }),
|
|
1064
1067
|
canvasRedo: () => ({ type: MSG.CANVAS_REDO, payload: EMPTY }),
|
|
1065
1068
|
// Cron
|
|
1066
|
-
cronTrigger: (taskName) => ({ type: MSG.CRON_TRIGGER, payload: { taskName } }),
|
|
1067
|
-
cronPause: (taskName) => ({ type: MSG.CRON_PAUSE, payload: { taskName } }),
|
|
1068
|
-
cronResume: (taskName) => ({ type: MSG.CRON_RESUME, payload: { taskName } }),
|
|
1069
|
+
cronTrigger: (taskName, requestId) => ({ type: MSG.CRON_TRIGGER, payload: { taskName, requestId } }),
|
|
1070
|
+
cronPause: (taskName, requestId) => ({ type: MSG.CRON_PAUSE, payload: { taskName, requestId } }),
|
|
1071
|
+
cronResume: (taskName, requestId) => ({ type: MSG.CRON_RESUME, payload: { taskName, requestId } }),
|
|
1069
1072
|
// Jobs
|
|
1070
1073
|
jobEnqueue: (requestId, type, payload, maxAttempts) => ({
|
|
1071
1074
|
type: MSG.JOB_ENQUEUE,
|
|
@@ -1119,6 +1122,14 @@ var serverBuild = {
|
|
|
1119
1122
|
cronTasks: (tasks) => ({ type: MSG.CRON_TASKS, payload: { tasks } }),
|
|
1120
1123
|
cronHistory: (history) => ({ type: MSG.CRON_HISTORY, payload: { history } }),
|
|
1121
1124
|
cronStatus: (tasks, recentHistory) => ({ type: MSG.CRON_STATUS, payload: { tasks, recentHistory } }),
|
|
1125
|
+
cronAckSuccess: (requestId, taskName) => ({
|
|
1126
|
+
type: MSG.CRON_ACK,
|
|
1127
|
+
payload: { requestId, taskName, ok: true }
|
|
1128
|
+
}),
|
|
1129
|
+
cronAckFailure: (requestId, reason, error, taskName) => ({
|
|
1130
|
+
type: MSG.CRON_ACK,
|
|
1131
|
+
payload: { requestId, taskName, ok: false, reason, error }
|
|
1132
|
+
}),
|
|
1122
1133
|
// Jobs
|
|
1123
1134
|
jobSnapshot: (jobs) => ({ type: MSG.JOB_UPDATE, payload: { kind: "snapshot", jobs } }),
|
|
1124
1135
|
jobEnqueued: (job, requestId) => ({
|
|
@@ -3992,40 +4003,88 @@ var CronRoom = class extends BaseRoom {
|
|
|
3992
4003
|
}
|
|
3993
4004
|
async onMessage(ws, user, message) {
|
|
3994
4005
|
this.ensureInitialized();
|
|
3995
|
-
const { type, payload } = message;
|
|
4006
|
+
const { type, payload = {} } = message;
|
|
3996
4007
|
if (CRON_WRITE_TYPES.has(type) && !user.canWrite) {
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4008
|
+
const error = "Write access denied: viewer role cannot modify cron tasks";
|
|
4009
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
4010
|
+
this.ackMutation(ws, payload.requestId, {
|
|
4011
|
+
ok: false,
|
|
4012
|
+
taskName: payload.taskName,
|
|
4013
|
+
reason: "read_only",
|
|
4014
|
+
error
|
|
4000
4015
|
});
|
|
4001
4016
|
return;
|
|
4002
4017
|
}
|
|
4018
|
+
try {
|
|
4019
|
+
await this.dispatchMessage(ws, type, payload);
|
|
4020
|
+
} catch (error) {
|
|
4021
|
+
try {
|
|
4022
|
+
this.ackMutation(ws, payload.requestId, {
|
|
4023
|
+
ok: false,
|
|
4024
|
+
taskName: payload.taskName,
|
|
4025
|
+
reason: "failed",
|
|
4026
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4027
|
+
});
|
|
4028
|
+
} catch {
|
|
4029
|
+
}
|
|
4030
|
+
throw error;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
async dispatchMessage(ws, type, payload) {
|
|
4003
4034
|
switch (type) {
|
|
4004
4035
|
case MSG.CRON_TRIGGER: {
|
|
4005
4036
|
const taskName = payload.taskName;
|
|
4006
4037
|
if (!taskName) {
|
|
4007
|
-
|
|
4038
|
+
const error = "Missing taskName";
|
|
4039
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
4040
|
+
this.ackMutation(ws, payload.requestId, { ok: false, reason: "failed", error });
|
|
4008
4041
|
return;
|
|
4009
4042
|
}
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4043
|
+
const execution = await this.executeTask(taskName);
|
|
4044
|
+
if (!execution) {
|
|
4045
|
+
const error = `Unknown cron task: ${taskName}`;
|
|
4046
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
4047
|
+
this.ackMutation(ws, payload.requestId, {
|
|
4048
|
+
ok: false,
|
|
4049
|
+
taskName,
|
|
4050
|
+
reason: "unknown_task",
|
|
4051
|
+
error
|
|
4014
4052
|
});
|
|
4053
|
+
break;
|
|
4015
4054
|
}
|
|
4055
|
+
this.ackMutation(
|
|
4056
|
+
ws,
|
|
4057
|
+
payload.requestId,
|
|
4058
|
+
execution.success ? { ok: true, taskName } : { ok: false, taskName, reason: "failed", error: execution.error }
|
|
4059
|
+
);
|
|
4016
4060
|
break;
|
|
4017
4061
|
}
|
|
4018
|
-
case MSG.CRON_PAUSE:
|
|
4019
|
-
const taskName = payload.taskName;
|
|
4020
|
-
this.sql.exec(`UPDATE cron_tasks SET paused = 1 WHERE name = ?`, taskName);
|
|
4021
|
-
this.broadcastStatus();
|
|
4022
|
-
break;
|
|
4023
|
-
}
|
|
4062
|
+
case MSG.CRON_PAUSE:
|
|
4024
4063
|
case MSG.CRON_RESUME: {
|
|
4025
4064
|
const taskName = payload.taskName;
|
|
4026
|
-
|
|
4027
|
-
|
|
4065
|
+
if (!taskName) {
|
|
4066
|
+
const error = "Missing taskName";
|
|
4067
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
4068
|
+
this.ackMutation(ws, payload.requestId, { ok: false, reason: "failed", error });
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
const known = this.sql.exec(`SELECT name FROM cron_tasks WHERE name = ?`, taskName).toArray().length > 0;
|
|
4072
|
+
if (!known) {
|
|
4073
|
+
const error = `Unknown cron task: ${taskName}`;
|
|
4074
|
+
this.sendTo(ws, { type: MSG.ERROR, payload: { error } });
|
|
4075
|
+
this.ackMutation(ws, payload.requestId, {
|
|
4076
|
+
ok: false,
|
|
4077
|
+
taskName,
|
|
4078
|
+
reason: "unknown_task",
|
|
4079
|
+
error
|
|
4080
|
+
});
|
|
4081
|
+
break;
|
|
4082
|
+
}
|
|
4083
|
+
const paused = type === MSG.CRON_PAUSE;
|
|
4084
|
+
this.sql.exec(`UPDATE cron_tasks SET paused = ? WHERE name = ?`, paused ? 1 : 0, taskName);
|
|
4085
|
+
if (!paused) this.scheduleNextAlarm();
|
|
4028
4086
|
this.broadcastStatus();
|
|
4087
|
+
this.ackMutation(ws, payload.requestId, { ok: true, taskName });
|
|
4029
4088
|
break;
|
|
4030
4089
|
}
|
|
4031
4090
|
case MSG.CRON_TASKS: {
|
|
@@ -4068,7 +4127,7 @@ var CronRoom = class extends BaseRoom {
|
|
|
4068
4127
|
// ==========================================================================
|
|
4069
4128
|
async executeTask(taskName) {
|
|
4070
4129
|
const taskRow = this.sql.exec(`SELECT interval_minutes, schedule, timezone FROM cron_tasks WHERE name = ?`, taskName).toArray()[0];
|
|
4071
|
-
if (!taskRow) return
|
|
4130
|
+
if (!taskRow) return null;
|
|
4072
4131
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4073
4132
|
const start = Date.now();
|
|
4074
4133
|
let success = true;
|
|
@@ -4104,7 +4163,7 @@ var CronRoom = class extends BaseRoom {
|
|
|
4104
4163
|
`DELETE FROM cron_history WHERE id NOT IN (SELECT id FROM cron_history ORDER BY id DESC LIMIT 500)`
|
|
4105
4164
|
);
|
|
4106
4165
|
this.broadcastStatus();
|
|
4107
|
-
return
|
|
4166
|
+
return { taskName, startedAt, completedAt, success, durationMs, error };
|
|
4108
4167
|
}
|
|
4109
4168
|
// ==========================================================================
|
|
4110
4169
|
// Scheduling
|
|
@@ -4174,6 +4233,21 @@ var CronRoom = class extends BaseRoom {
|
|
|
4174
4233
|
}
|
|
4175
4234
|
});
|
|
4176
4235
|
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Receipt for a mutation frame, addressed only to its sender. Receipts
|
|
4238
|
+
* are opt-in by correlation id: frames without a `requestId` keep the
|
|
4239
|
+
* fire-and-forget contract, so the (untyped) `requestId` from the wire
|
|
4240
|
+
* is the single gate here rather than a check at every call site.
|
|
4241
|
+
* Frames go through the shared `serverBuild` constructors so the wire
|
|
4242
|
+
* shape is enforced by the protocol types, not re-spelled here.
|
|
4243
|
+
*/
|
|
4244
|
+
ackMutation(ws, requestId, result) {
|
|
4245
|
+
if (typeof requestId !== "string") return;
|
|
4246
|
+
this.sendTo(
|
|
4247
|
+
ws,
|
|
4248
|
+
result.ok ? serverBuild.cronAckSuccess(requestId, result.taskName) : serverBuild.cronAckFailure(requestId, result.reason, result.error, result.taskName)
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4177
4251
|
};
|
|
4178
4252
|
function computeNextRunAt(task, from) {
|
|
4179
4253
|
if (task.interval_minutes) {
|
|
@@ -6622,6 +6696,9 @@ function declaredLength(request, what) {
|
|
|
6622
6696
|
function isKeyWithinScope(key, prefix, excludedPrefixes) {
|
|
6623
6697
|
return key.startsWith(prefix) && !excludedPrefixes.some((excluded) => key.startsWith(excluded));
|
|
6624
6698
|
}
|
|
6699
|
+
function scopeRelativeKey(key, prefix) {
|
|
6700
|
+
return key.slice(prefix.length);
|
|
6701
|
+
}
|
|
6625
6702
|
function anchorKey(raw, prefix, excludedPrefixes) {
|
|
6626
6703
|
const safe = sanitizeSubpath(raw);
|
|
6627
6704
|
if (safe === null) return fail2(400, "Invalid key: traversal not allowed", "invalid_key");
|
|
@@ -6821,7 +6898,7 @@ async function handleUpload(request, url, bucket, prefix, excludedPrefixes, auth
|
|
|
6821
6898
|
admission.markerKey,
|
|
6822
6899
|
admission.markerKey !== null || fileData.byteLength < replacedBytes
|
|
6823
6900
|
);
|
|
6824
|
-
return storedFileJson(located.key, fileName, url, scope);
|
|
6901
|
+
return storedFileJson(located.key, prefix, fileName, url, scope);
|
|
6825
6902
|
} catch (err) {
|
|
6826
6903
|
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
6827
6904
|
console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
|
|
@@ -6843,11 +6920,17 @@ function fileMetadata(fileName, userId, reservationId, declaredBytes) {
|
|
|
6843
6920
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6844
6921
|
};
|
|
6845
6922
|
}
|
|
6846
|
-
function storedFileJson(key, name, url, scope) {
|
|
6923
|
+
function storedFileJson(key, prefix, name, url, scope) {
|
|
6847
6924
|
const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
|
|
6848
6925
|
fileUrl.searchParams.set("scope", scope);
|
|
6849
6926
|
return Response.json(
|
|
6850
|
-
{
|
|
6927
|
+
{
|
|
6928
|
+
success: true,
|
|
6929
|
+
key,
|
|
6930
|
+
relativeKey: scopeRelativeKey(key, prefix),
|
|
6931
|
+
url: fileUrl.toString(),
|
|
6932
|
+
name
|
|
6933
|
+
},
|
|
6851
6934
|
{ headers: CORS_HEADERS4 }
|
|
6852
6935
|
);
|
|
6853
6936
|
}
|
|
@@ -7127,7 +7210,7 @@ async function completedReservationObject(bucket, session) {
|
|
|
7127
7210
|
const object = await bucket.head(session.key);
|
|
7128
7211
|
return object?.customMetadata?.reservationId === session.reservationId ? object : null;
|
|
7129
7212
|
}
|
|
7130
|
-
async function finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope) {
|
|
7213
|
+
async function finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope, prefix) {
|
|
7131
7214
|
const declaredMetadata = Number(object.customMetadata?.declaredBytes);
|
|
7132
7215
|
const declaredBytes = reservation?.declaredBytes ?? (Number.isSafeInteger(declaredMetadata) && declaredMetadata >= 0 ? declaredMetadata : MAX_APP_FILE_BYTES);
|
|
7133
7216
|
const refusal = overCeiling(object.size) ?? (object.size > declaredBytes ? fail2(
|
|
@@ -7161,7 +7244,7 @@ async function finishCompletedMultipart(bucket, session, reservation, object, au
|
|
|
7161
7244
|
}
|
|
7162
7245
|
}
|
|
7163
7246
|
const name = object.customMetadata?.originalName ?? session.uploadKey.split("/").pop() ?? "file";
|
|
7164
|
-
return storedFileJson(session.key, name, url, scope);
|
|
7247
|
+
return storedFileJson(session.key, prefix, name, url, scope);
|
|
7165
7248
|
}
|
|
7166
7249
|
async function multipartComplete(request, url, bucket, prefix, excludedPrefixes, auth, scope) {
|
|
7167
7250
|
const body = await readControlJson(request);
|
|
@@ -7182,7 +7265,7 @@ async function multipartComplete(request, url, bucket, prefix, excludedPrefixes,
|
|
|
7182
7265
|
if (auth.storage && !reservation) {
|
|
7183
7266
|
const completed = await completedReservationObject(bucket, session);
|
|
7184
7267
|
if (completed) {
|
|
7185
|
-
return finishCompletedMultipart(bucket, session, null, completed, auth, url, scope);
|
|
7268
|
+
return finishCompletedMultipart(bucket, session, null, completed, auth, url, scope, prefix);
|
|
7186
7269
|
}
|
|
7187
7270
|
await abandonMultipart(bucket, session, auth, null);
|
|
7188
7271
|
return quotaUnavailable("This upload reservation is missing or does not match.");
|
|
@@ -7221,14 +7304,23 @@ async function multipartComplete(request, url, bucket, prefix, excludedPrefixes,
|
|
|
7221
7304
|
} catch (error) {
|
|
7222
7305
|
const completed = await completedReservationObject(bucket, session);
|
|
7223
7306
|
if (completed) {
|
|
7224
|
-
return finishCompletedMultipart(
|
|
7307
|
+
return finishCompletedMultipart(
|
|
7308
|
+
bucket,
|
|
7309
|
+
session,
|
|
7310
|
+
reservation,
|
|
7311
|
+
completed,
|
|
7312
|
+
auth,
|
|
7313
|
+
url,
|
|
7314
|
+
scope,
|
|
7315
|
+
prefix
|
|
7316
|
+
);
|
|
7225
7317
|
}
|
|
7226
7318
|
if (r2ErrorCode(error) === 10024 && reservation) {
|
|
7227
7319
|
await releaseQuotaMarker(bucket, auth.storage, reservation.key, true);
|
|
7228
7320
|
}
|
|
7229
7321
|
return multipartFailure(error);
|
|
7230
7322
|
}
|
|
7231
|
-
return finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope);
|
|
7323
|
+
return finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope, prefix);
|
|
7232
7324
|
}
|
|
7233
7325
|
async function multipartAbort(url, bucket, prefix, excludedPrefixes, auth) {
|
|
7234
7326
|
const session = resolveSession(url, prefix, excludedPrefixes);
|
|
@@ -7248,37 +7340,49 @@ async function handleList(bucket, prefix, excludedPrefixes, url, scope, auth) {
|
|
|
7248
7340
|
const listPrefix = `${prefix}${userPrefix}`;
|
|
7249
7341
|
const requestedLimit = Number(url.searchParams.get("limit") ?? 100);
|
|
7250
7342
|
const limit = Number.isSafeInteger(requestedLimit) ? Math.min(1e3, Math.max(1, requestedLimit)) : 100;
|
|
7343
|
+
const requestCursor = url.searchParams.get("cursor");
|
|
7251
7344
|
if (excludedPrefixes.some((excluded) => listPrefix.startsWith(excluded))) {
|
|
7252
7345
|
return Response.json({ files: [], truncated: false }, { headers: CORS_HEADERS4 });
|
|
7253
7346
|
}
|
|
7254
|
-
const
|
|
7347
|
+
const withMetadata = (options) => ({ ...options, include: ["customMetadata"] });
|
|
7348
|
+
const listed = await bucket.list(
|
|
7349
|
+
withMetadata({
|
|
7350
|
+
prefix: listPrefix,
|
|
7351
|
+
limit,
|
|
7352
|
+
...requestCursor ? { cursor: requestCursor } : {}
|
|
7353
|
+
})
|
|
7354
|
+
);
|
|
7255
7355
|
const visible = listed.objects.filter(
|
|
7256
7356
|
(object) => isKeyWithinScope(object.key, prefix, excludedPrefixes)
|
|
7257
7357
|
);
|
|
7258
7358
|
const seen = new Set(visible.map((object) => object.key));
|
|
7259
|
-
let
|
|
7260
|
-
|
|
7261
|
-
|
|
7359
|
+
let frontier = listed;
|
|
7360
|
+
let frontierKey = listed.objects.at(-1)?.key;
|
|
7361
|
+
for (const excluded of excludedPrefixes.filter((candidate) => candidate.startsWith(listPrefix)).sort()) {
|
|
7362
|
+
if (!frontier.truncated || visible.length >= limit) break;
|
|
7262
7363
|
const excludedEnd = `${excluded}\u{10FFFF}`;
|
|
7263
|
-
const
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7364
|
+
const after = await bucket.list(
|
|
7365
|
+
withMetadata({
|
|
7366
|
+
prefix: listPrefix,
|
|
7367
|
+
startAfter: frontierKey && frontierKey > excludedEnd ? frontierKey : excludedEnd,
|
|
7368
|
+
limit: limit - visible.length
|
|
7369
|
+
})
|
|
7370
|
+
);
|
|
7269
7371
|
for (const object of after.objects) {
|
|
7270
7372
|
if (!seen.has(object.key) && isKeyWithinScope(object.key, prefix, excludedPrefixes)) {
|
|
7271
7373
|
seen.add(object.key);
|
|
7272
7374
|
visible.push(object);
|
|
7273
7375
|
}
|
|
7274
7376
|
}
|
|
7275
|
-
|
|
7377
|
+
frontierKey = after.objects.at(-1)?.key ?? frontierKey;
|
|
7378
|
+
frontier = after;
|
|
7276
7379
|
}
|
|
7277
7380
|
const files = visible.slice(0, limit).map((obj) => {
|
|
7278
7381
|
const fileUrl = new URL(`/api/files/${encodeKeyPath(obj.key)}`, url.origin);
|
|
7279
7382
|
fileUrl.searchParams.set("scope", scope);
|
|
7280
7383
|
return {
|
|
7281
7384
|
key: obj.key,
|
|
7385
|
+
relativeKey: scopeRelativeKey(obj.key, prefix),
|
|
7282
7386
|
size: obj.size,
|
|
7283
7387
|
uploaded: obj.uploaded.toISOString(),
|
|
7284
7388
|
url: fileUrl.toString(),
|
|
@@ -7293,7 +7397,12 @@ async function handleList(bucket, prefix, excludedPrefixes, url, scope, auth) {
|
|
|
7293
7397
|
if (storage && limitBytes !== null) storage.limitBytes = limitBytes;
|
|
7294
7398
|
}
|
|
7295
7399
|
return Response.json(
|
|
7296
|
-
{
|
|
7400
|
+
{
|
|
7401
|
+
files,
|
|
7402
|
+
truncated: frontier.truncated,
|
|
7403
|
+
...frontier.truncated ? { cursor: frontier.cursor } : {},
|
|
7404
|
+
...storage ? { storage } : {}
|
|
7405
|
+
},
|
|
7297
7406
|
{ headers: CORS_HEADERS4 }
|
|
7298
7407
|
);
|
|
7299
7408
|
}
|
|
@@ -8593,7 +8702,8 @@ function searchDocumentationCorpus(corpus, query, limit = 6) {
|
|
|
8593
8702
|
title: chunk.title,
|
|
8594
8703
|
...chunk.heading ? { heading: chunk.heading } : {},
|
|
8595
8704
|
excerpt: excerpt(chunk.text, terms),
|
|
8596
|
-
score
|
|
8705
|
+
score,
|
|
8706
|
+
termCoverage: matchedTerms / terms.length
|
|
8597
8707
|
};
|
|
8598
8708
|
}).filter((result) => result.score > 0).sort((left, right) => right.score - left.score || left.route.localeCompare(right.route)).slice(0, Math.max(1, Math.min(limit, 10)));
|
|
8599
8709
|
}
|
|
@@ -8713,6 +8823,25 @@ function excerpt(text, terms) {
|
|
|
8713
8823
|
return `${start > 0 ? "\u2026" : ""}${text.slice(start, end).trim()}${end < text.length ? "\u2026" : ""}`;
|
|
8714
8824
|
}
|
|
8715
8825
|
|
|
8826
|
+
// src/documentation/text.ts
|
|
8827
|
+
function slugify(value) {
|
|
8828
|
+
return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8829
|
+
}
|
|
8830
|
+
function documentationSubject(name) {
|
|
8831
|
+
const normalized = name.trim();
|
|
8832
|
+
if (!normalized || /^documentation$/i.test(normalized)) return "documentation";
|
|
8833
|
+
return /(?:\bdocs?|documentation)$/i.test(normalized) ? normalized : `${normalized} documentation`;
|
|
8834
|
+
}
|
|
8835
|
+
function createSlugger(fallback = "section") {
|
|
8836
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8837
|
+
return (value) => {
|
|
8838
|
+
const base = slugify(value) || fallback;
|
|
8839
|
+
const count = seen.get(base) ?? 0;
|
|
8840
|
+
seen.set(base, count + 1);
|
|
8841
|
+
return count === 0 ? base : `${base}-${count + 1}`;
|
|
8842
|
+
};
|
|
8843
|
+
}
|
|
8844
|
+
|
|
8716
8845
|
// src/documentation/worker/published-corpus.ts
|
|
8717
8846
|
var cachedCorpus;
|
|
8718
8847
|
async function loadDocumentationPublishedManifest(env, requestUrl = "https://assets.local/", assetBasePath = "/_documentation") {
|
|
@@ -8770,7 +8899,7 @@ async function resolveDocumentationPublishedCorpus(env, sourceHash, requestUrl =
|
|
|
8770
8899
|
}
|
|
8771
8900
|
async function readDocumentationPublishedPage(env, corpus, requestedRoute, requestUrl = "https://assets.local/", assetBasePath = "/_documentation") {
|
|
8772
8901
|
const route = collapseRoute(requestedRoute.split("#", 1)[0] ?? "/");
|
|
8773
|
-
const firstChunk = corpus.find((chunk) => chunk.route === route);
|
|
8902
|
+
const firstChunk = corpus.find((chunk) => (chunk.route.split("#", 1)[0] ?? "") === route);
|
|
8774
8903
|
if (!firstChunk) return null;
|
|
8775
8904
|
const pathname = route === "/" ? `${assetBasePath}/index.md` : `${assetBasePath}${route}.md`;
|
|
8776
8905
|
const response = await env.ASSETS.fetch(new Request(new URL(pathname, requestUrl)));
|
|
@@ -8779,6 +8908,36 @@ async function readDocumentationPublishedPage(env, corpus, requestedRoute, reque
|
|
|
8779
8908
|
if (markdown.length > 5e5) throw new Error("Documentation page exceeds 500000 characters");
|
|
8780
8909
|
return { route, title: firstChunk.title, markdown };
|
|
8781
8910
|
}
|
|
8911
|
+
function documentationPageSection(markdown, fragment) {
|
|
8912
|
+
const target = slugify(fragment);
|
|
8913
|
+
if (!target) return null;
|
|
8914
|
+
const slug = createSlugger();
|
|
8915
|
+
const lines = markdown.split("\n");
|
|
8916
|
+
let fence = null;
|
|
8917
|
+
let start = -1;
|
|
8918
|
+
let startDepth = 0;
|
|
8919
|
+
for (let index = 0; index < lines.length; index++) {
|
|
8920
|
+
const line = lines[index] ?? "";
|
|
8921
|
+
const fenceMarker = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
|
|
8922
|
+
if (fenceMarker && !fence) {
|
|
8923
|
+
fence = fenceMarker;
|
|
8924
|
+
continue;
|
|
8925
|
+
}
|
|
8926
|
+
if (fence) {
|
|
8927
|
+
if (fenceMarker?.startsWith(fence)) fence = null;
|
|
8928
|
+
continue;
|
|
8929
|
+
}
|
|
8930
|
+
const heading = line.match(/^ {0,3}(#{1,6})\s+(.+)$/);
|
|
8931
|
+
if (!heading) continue;
|
|
8932
|
+
const depth = (heading[1] ?? "").length;
|
|
8933
|
+
if (start !== -1 && depth <= startDepth) return lines.slice(start, index).join("\n").trim();
|
|
8934
|
+
if (start === -1 && slug(heading[2] ?? "") === target) {
|
|
8935
|
+
start = index;
|
|
8936
|
+
startDepth = depth;
|
|
8937
|
+
}
|
|
8938
|
+
}
|
|
8939
|
+
return start === -1 ? null : lines.slice(start).join("\n").trim();
|
|
8940
|
+
}
|
|
8782
8941
|
function parseAssistantPolicy(value) {
|
|
8783
8942
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
8784
8943
|
const policy = value;
|
|
@@ -9028,6 +9187,17 @@ function registerDocumentationStaticRoutes(app, publicBasePath = "/docs", assetB
|
|
|
9028
9187
|
status = 404;
|
|
9029
9188
|
}
|
|
9030
9189
|
if (status === 404) {
|
|
9190
|
+
if (pathname.endsWith(".md")) {
|
|
9191
|
+
return secureDocumentationResponse(
|
|
9192
|
+
new Response(
|
|
9193
|
+
`No documentation page at ${pathname}. See ${publicBasePath}/llms.txt for every published page.
|
|
9194
|
+
`,
|
|
9195
|
+
{ status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" } }
|
|
9196
|
+
),
|
|
9197
|
+
404,
|
|
9198
|
+
pathname
|
|
9199
|
+
);
|
|
9200
|
+
}
|
|
9031
9201
|
const url = new URL(c.req.url);
|
|
9032
9202
|
url.pathname = `${assetBasePath}/404.html`;
|
|
9033
9203
|
response = await fetchDocumentationAsset(c.env, c.req.raw, url, assetBasePath);
|
|
@@ -9085,13 +9255,6 @@ function secureDocumentationResponse(response, status, pathname) {
|
|
|
9085
9255
|
return new Response(response.body, { status, headers });
|
|
9086
9256
|
}
|
|
9087
9257
|
|
|
9088
|
-
// src/documentation/text.ts
|
|
9089
|
-
function documentationSubject(name) {
|
|
9090
|
-
const normalized = name.trim();
|
|
9091
|
-
if (!normalized || /^documentation$/i.test(normalized)) return "documentation";
|
|
9092
|
-
return /(?:\bdocs?|documentation)$/i.test(normalized) ? normalized : `${normalized} documentation`;
|
|
9093
|
-
}
|
|
9094
|
-
|
|
9095
9258
|
// src/documentation/worker/mcp/protocol.ts
|
|
9096
9259
|
var MCP_SERVER_VERSION = "1.0.0";
|
|
9097
9260
|
var CURRENT_PROTOCOL_VERSION = "2025-11-25";
|
|
@@ -9203,7 +9366,7 @@ var DOCUMENTATION_TOOLS = [
|
|
|
9203
9366
|
},
|
|
9204
9367
|
outputSchema: {
|
|
9205
9368
|
type: "object",
|
|
9206
|
-
properties: { results: { type: "array" } },
|
|
9369
|
+
properties: { notice: { type: "string" }, results: { type: "array" } },
|
|
9207
9370
|
required: ["results"]
|
|
9208
9371
|
},
|
|
9209
9372
|
annotations: {
|
|
@@ -9216,11 +9379,11 @@ var DOCUMENTATION_TOOLS = [
|
|
|
9216
9379
|
{
|
|
9217
9380
|
name: "documentation_read",
|
|
9218
9381
|
title: "Read documentation page",
|
|
9219
|
-
description: "Read one
|
|
9382
|
+
description: "Read one published documentation page as Markdown. The route may omit the leading slash, and a #fragment (as returned by documentation_search) narrows the result to that heading section; an unknown fragment returns the whole page with a notice.",
|
|
9220
9383
|
inputSchema: {
|
|
9221
9384
|
type: "object",
|
|
9222
9385
|
properties: {
|
|
9223
|
-
route: { type: "string", minLength: 1, maxLength: 500
|
|
9386
|
+
route: { type: "string", minLength: 1, maxLength: 500 }
|
|
9224
9387
|
},
|
|
9225
9388
|
required: ["route"],
|
|
9226
9389
|
additionalProperties: false
|
|
@@ -9228,6 +9391,7 @@ var DOCUMENTATION_TOOLS = [
|
|
|
9228
9391
|
outputSchema: {
|
|
9229
9392
|
type: "object",
|
|
9230
9393
|
properties: {
|
|
9394
|
+
notice: { type: "string" },
|
|
9231
9395
|
route: { type: "string" },
|
|
9232
9396
|
title: { type: "string" },
|
|
9233
9397
|
url: { type: "string" },
|
|
@@ -9254,19 +9418,24 @@ async function callDocumentationTool(rawRequest, request, corpus, env, basePath
|
|
|
9254
9418
|
if (query.length < 2 || query.length > 300 || !Number.isInteger(limit) || Number(limit) < 1 || Number(limit) > 10) {
|
|
9255
9419
|
return mcpError(request.id, -32602, "Invalid documentation_search arguments");
|
|
9256
9420
|
}
|
|
9257
|
-
const
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9421
|
+
const results = searchDocumentationCorpus(corpus, query, Number(limit)).map((result) => ({
|
|
9422
|
+
...result,
|
|
9423
|
+
url: new URL(documentationPublicPath(basePath, result.route), origin).toString()
|
|
9424
|
+
}));
|
|
9425
|
+
return toolResult(request.id, {
|
|
9426
|
+
...results[0]?.termCoverage === 1 ? {} : {
|
|
9427
|
+
notice: "No section matches every search term \u2014 the topic may be absent or spread across sections."
|
|
9428
|
+
},
|
|
9429
|
+
results
|
|
9430
|
+
});
|
|
9264
9431
|
}
|
|
9265
9432
|
if (name === "documentation_read") {
|
|
9266
9433
|
const route = typeof args.route === "string" ? args.route.trim() : "";
|
|
9267
|
-
if (!route
|
|
9434
|
+
if (!route || route.length > 500) {
|
|
9268
9435
|
return mcpError(request.id, -32602, "Invalid documentation_read arguments");
|
|
9269
9436
|
}
|
|
9437
|
+
const hash = route.indexOf("#");
|
|
9438
|
+
const fragment = hash === -1 ? "" : route.slice(hash + 1);
|
|
9270
9439
|
try {
|
|
9271
9440
|
const page = await readDocumentationPublishedPage(
|
|
9272
9441
|
env,
|
|
@@ -9276,8 +9445,11 @@ async function callDocumentationTool(rawRequest, request, corpus, env, basePath
|
|
|
9276
9445
|
assetBasePath
|
|
9277
9446
|
);
|
|
9278
9447
|
if (!page) return toolError(request.id, `No published documentation page exists at ${route}`);
|
|
9448
|
+
const section = fragment ? documentationPageSection(page.markdown, fragment) : null;
|
|
9279
9449
|
return toolResult(request.id, {
|
|
9450
|
+
...fragment && section === null ? { notice: `The fragment #${fragment} was not found; returning the whole page.` } : {},
|
|
9280
9451
|
...page,
|
|
9452
|
+
...section === null ? {} : { markdown: section },
|
|
9281
9453
|
url: new URL(documentationPublicPath(basePath, page.route), origin).toString()
|
|
9282
9454
|
});
|
|
9283
9455
|
} catch (error) {
|