deepspace 0.20.0 → 0.22.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 +28 -0
- package/dist/{chunk-A7UFIOWO.js → chunk-7TDBFEB7.js} +7 -2
- package/dist/chunk-7TDBFEB7.js.map +1 -0
- package/dist/cli.js +288 -120
- 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 +32 -4
- package/dist/server.js +177 -43
- 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 +57 -5
- package/dist/worker.js +262 -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) {
|
|
@@ -6022,6 +6096,32 @@ var REGISTRY_INSTANCE = "v1:global";
|
|
|
6022
6096
|
var APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
6023
6097
|
var STRICT_APP_ID_RE = APP_ID_RE;
|
|
6024
6098
|
var RESOURCE_ID_RE = /^(app_[0-9A-HJKMNP-TV-Z]{26}|[a-z0-9][a-z0-9_-]{0,63})$/;
|
|
6099
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
6100
|
+
function mintUlid(now = Date.now()) {
|
|
6101
|
+
let ts = "";
|
|
6102
|
+
let t = now;
|
|
6103
|
+
for (let i = 0; i < 10; i++) {
|
|
6104
|
+
ts = CROCKFORD[t % 32] + ts;
|
|
6105
|
+
t = Math.floor(t / 32);
|
|
6106
|
+
}
|
|
6107
|
+
const rand = new Uint8Array(10);
|
|
6108
|
+
crypto.getRandomValues(rand);
|
|
6109
|
+
let rs = "";
|
|
6110
|
+
let acc = 0;
|
|
6111
|
+
let bits = 0;
|
|
6112
|
+
for (const byte of rand) {
|
|
6113
|
+
acc = acc << 8 | byte;
|
|
6114
|
+
bits += 8;
|
|
6115
|
+
while (bits >= 5) {
|
|
6116
|
+
bits -= 5;
|
|
6117
|
+
rs += CROCKFORD[acc >> bits & 31];
|
|
6118
|
+
}
|
|
6119
|
+
}
|
|
6120
|
+
return `${ts}${rs}`.slice(0, 26);
|
|
6121
|
+
}
|
|
6122
|
+
function mintAppId(now = Date.now()) {
|
|
6123
|
+
return `app_${mintUlid(now)}`;
|
|
6124
|
+
}
|
|
6025
6125
|
var RegistryClientError = class extends Error {
|
|
6026
6126
|
constructor(message, code, status) {
|
|
6027
6127
|
super(message);
|
|
@@ -6622,6 +6722,9 @@ function declaredLength(request, what) {
|
|
|
6622
6722
|
function isKeyWithinScope(key, prefix, excludedPrefixes) {
|
|
6623
6723
|
return key.startsWith(prefix) && !excludedPrefixes.some((excluded) => key.startsWith(excluded));
|
|
6624
6724
|
}
|
|
6725
|
+
function scopeRelativeKey(key, prefix) {
|
|
6726
|
+
return key.slice(prefix.length);
|
|
6727
|
+
}
|
|
6625
6728
|
function anchorKey(raw, prefix, excludedPrefixes) {
|
|
6626
6729
|
const safe = sanitizeSubpath(raw);
|
|
6627
6730
|
if (safe === null) return fail2(400, "Invalid key: traversal not allowed", "invalid_key");
|
|
@@ -6821,7 +6924,7 @@ async function handleUpload(request, url, bucket, prefix, excludedPrefixes, auth
|
|
|
6821
6924
|
admission.markerKey,
|
|
6822
6925
|
admission.markerKey !== null || fileData.byteLength < replacedBytes
|
|
6823
6926
|
);
|
|
6824
|
-
return storedFileJson(located.key, fileName, url, scope);
|
|
6927
|
+
return storedFileJson(located.key, prefix, fileName, url, scope);
|
|
6825
6928
|
} catch (err) {
|
|
6826
6929
|
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
6827
6930
|
console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
|
|
@@ -6843,11 +6946,17 @@ function fileMetadata(fileName, userId, reservationId, declaredBytes) {
|
|
|
6843
6946
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6844
6947
|
};
|
|
6845
6948
|
}
|
|
6846
|
-
function storedFileJson(key, name, url, scope) {
|
|
6949
|
+
function storedFileJson(key, prefix, name, url, scope) {
|
|
6847
6950
|
const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
|
|
6848
6951
|
fileUrl.searchParams.set("scope", scope);
|
|
6849
6952
|
return Response.json(
|
|
6850
|
-
{
|
|
6953
|
+
{
|
|
6954
|
+
success: true,
|
|
6955
|
+
key,
|
|
6956
|
+
relativeKey: scopeRelativeKey(key, prefix),
|
|
6957
|
+
url: fileUrl.toString(),
|
|
6958
|
+
name
|
|
6959
|
+
},
|
|
6851
6960
|
{ headers: CORS_HEADERS4 }
|
|
6852
6961
|
);
|
|
6853
6962
|
}
|
|
@@ -7127,7 +7236,7 @@ async function completedReservationObject(bucket, session) {
|
|
|
7127
7236
|
const object = await bucket.head(session.key);
|
|
7128
7237
|
return object?.customMetadata?.reservationId === session.reservationId ? object : null;
|
|
7129
7238
|
}
|
|
7130
|
-
async function finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope) {
|
|
7239
|
+
async function finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope, prefix) {
|
|
7131
7240
|
const declaredMetadata = Number(object.customMetadata?.declaredBytes);
|
|
7132
7241
|
const declaredBytes = reservation?.declaredBytes ?? (Number.isSafeInteger(declaredMetadata) && declaredMetadata >= 0 ? declaredMetadata : MAX_APP_FILE_BYTES);
|
|
7133
7242
|
const refusal = overCeiling(object.size) ?? (object.size > declaredBytes ? fail2(
|
|
@@ -7161,7 +7270,7 @@ async function finishCompletedMultipart(bucket, session, reservation, object, au
|
|
|
7161
7270
|
}
|
|
7162
7271
|
}
|
|
7163
7272
|
const name = object.customMetadata?.originalName ?? session.uploadKey.split("/").pop() ?? "file";
|
|
7164
|
-
return storedFileJson(session.key, name, url, scope);
|
|
7273
|
+
return storedFileJson(session.key, prefix, name, url, scope);
|
|
7165
7274
|
}
|
|
7166
7275
|
async function multipartComplete(request, url, bucket, prefix, excludedPrefixes, auth, scope) {
|
|
7167
7276
|
const body = await readControlJson(request);
|
|
@@ -7182,7 +7291,7 @@ async function multipartComplete(request, url, bucket, prefix, excludedPrefixes,
|
|
|
7182
7291
|
if (auth.storage && !reservation) {
|
|
7183
7292
|
const completed = await completedReservationObject(bucket, session);
|
|
7184
7293
|
if (completed) {
|
|
7185
|
-
return finishCompletedMultipart(bucket, session, null, completed, auth, url, scope);
|
|
7294
|
+
return finishCompletedMultipart(bucket, session, null, completed, auth, url, scope, prefix);
|
|
7186
7295
|
}
|
|
7187
7296
|
await abandonMultipart(bucket, session, auth, null);
|
|
7188
7297
|
return quotaUnavailable("This upload reservation is missing or does not match.");
|
|
@@ -7221,14 +7330,23 @@ async function multipartComplete(request, url, bucket, prefix, excludedPrefixes,
|
|
|
7221
7330
|
} catch (error) {
|
|
7222
7331
|
const completed = await completedReservationObject(bucket, session);
|
|
7223
7332
|
if (completed) {
|
|
7224
|
-
return finishCompletedMultipart(
|
|
7333
|
+
return finishCompletedMultipart(
|
|
7334
|
+
bucket,
|
|
7335
|
+
session,
|
|
7336
|
+
reservation,
|
|
7337
|
+
completed,
|
|
7338
|
+
auth,
|
|
7339
|
+
url,
|
|
7340
|
+
scope,
|
|
7341
|
+
prefix
|
|
7342
|
+
);
|
|
7225
7343
|
}
|
|
7226
7344
|
if (r2ErrorCode(error) === 10024 && reservation) {
|
|
7227
7345
|
await releaseQuotaMarker(bucket, auth.storage, reservation.key, true);
|
|
7228
7346
|
}
|
|
7229
7347
|
return multipartFailure(error);
|
|
7230
7348
|
}
|
|
7231
|
-
return finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope);
|
|
7349
|
+
return finishCompletedMultipart(bucket, session, reservation, object, auth, url, scope, prefix);
|
|
7232
7350
|
}
|
|
7233
7351
|
async function multipartAbort(url, bucket, prefix, excludedPrefixes, auth) {
|
|
7234
7352
|
const session = resolveSession(url, prefix, excludedPrefixes);
|
|
@@ -7248,37 +7366,49 @@ async function handleList(bucket, prefix, excludedPrefixes, url, scope, auth) {
|
|
|
7248
7366
|
const listPrefix = `${prefix}${userPrefix}`;
|
|
7249
7367
|
const requestedLimit = Number(url.searchParams.get("limit") ?? 100);
|
|
7250
7368
|
const limit = Number.isSafeInteger(requestedLimit) ? Math.min(1e3, Math.max(1, requestedLimit)) : 100;
|
|
7369
|
+
const requestCursor = url.searchParams.get("cursor");
|
|
7251
7370
|
if (excludedPrefixes.some((excluded) => listPrefix.startsWith(excluded))) {
|
|
7252
7371
|
return Response.json({ files: [], truncated: false }, { headers: CORS_HEADERS4 });
|
|
7253
7372
|
}
|
|
7254
|
-
const
|
|
7373
|
+
const withMetadata = (options) => ({ ...options, include: ["customMetadata"] });
|
|
7374
|
+
const listed = await bucket.list(
|
|
7375
|
+
withMetadata({
|
|
7376
|
+
prefix: listPrefix,
|
|
7377
|
+
limit,
|
|
7378
|
+
...requestCursor ? { cursor: requestCursor } : {}
|
|
7379
|
+
})
|
|
7380
|
+
);
|
|
7255
7381
|
const visible = listed.objects.filter(
|
|
7256
7382
|
(object) => isKeyWithinScope(object.key, prefix, excludedPrefixes)
|
|
7257
7383
|
);
|
|
7258
7384
|
const seen = new Set(visible.map((object) => object.key));
|
|
7259
|
-
let
|
|
7260
|
-
|
|
7261
|
-
|
|
7385
|
+
let frontier = listed;
|
|
7386
|
+
let frontierKey = listed.objects.at(-1)?.key;
|
|
7387
|
+
for (const excluded of excludedPrefixes.filter((candidate) => candidate.startsWith(listPrefix)).sort()) {
|
|
7388
|
+
if (!frontier.truncated || visible.length >= limit) break;
|
|
7262
7389
|
const excludedEnd = `${excluded}\u{10FFFF}`;
|
|
7263
|
-
const
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7390
|
+
const after = await bucket.list(
|
|
7391
|
+
withMetadata({
|
|
7392
|
+
prefix: listPrefix,
|
|
7393
|
+
startAfter: frontierKey && frontierKey > excludedEnd ? frontierKey : excludedEnd,
|
|
7394
|
+
limit: limit - visible.length
|
|
7395
|
+
})
|
|
7396
|
+
);
|
|
7269
7397
|
for (const object of after.objects) {
|
|
7270
7398
|
if (!seen.has(object.key) && isKeyWithinScope(object.key, prefix, excludedPrefixes)) {
|
|
7271
7399
|
seen.add(object.key);
|
|
7272
7400
|
visible.push(object);
|
|
7273
7401
|
}
|
|
7274
7402
|
}
|
|
7275
|
-
|
|
7403
|
+
frontierKey = after.objects.at(-1)?.key ?? frontierKey;
|
|
7404
|
+
frontier = after;
|
|
7276
7405
|
}
|
|
7277
7406
|
const files = visible.slice(0, limit).map((obj) => {
|
|
7278
7407
|
const fileUrl = new URL(`/api/files/${encodeKeyPath(obj.key)}`, url.origin);
|
|
7279
7408
|
fileUrl.searchParams.set("scope", scope);
|
|
7280
7409
|
return {
|
|
7281
7410
|
key: obj.key,
|
|
7411
|
+
relativeKey: scopeRelativeKey(obj.key, prefix),
|
|
7282
7412
|
size: obj.size,
|
|
7283
7413
|
uploaded: obj.uploaded.toISOString(),
|
|
7284
7414
|
url: fileUrl.toString(),
|
|
@@ -7293,7 +7423,12 @@ async function handleList(bucket, prefix, excludedPrefixes, url, scope, auth) {
|
|
|
7293
7423
|
if (storage && limitBytes !== null) storage.limitBytes = limitBytes;
|
|
7294
7424
|
}
|
|
7295
7425
|
return Response.json(
|
|
7296
|
-
{
|
|
7426
|
+
{
|
|
7427
|
+
files,
|
|
7428
|
+
truncated: frontier.truncated,
|
|
7429
|
+
...frontier.truncated ? { cursor: frontier.cursor } : {},
|
|
7430
|
+
...storage ? { storage } : {}
|
|
7431
|
+
},
|
|
7297
7432
|
{ headers: CORS_HEADERS4 }
|
|
7298
7433
|
);
|
|
7299
7434
|
}
|
|
@@ -8593,7 +8728,8 @@ function searchDocumentationCorpus(corpus, query, limit = 6) {
|
|
|
8593
8728
|
title: chunk.title,
|
|
8594
8729
|
...chunk.heading ? { heading: chunk.heading } : {},
|
|
8595
8730
|
excerpt: excerpt(chunk.text, terms),
|
|
8596
|
-
score
|
|
8731
|
+
score,
|
|
8732
|
+
termCoverage: matchedTerms / terms.length
|
|
8597
8733
|
};
|
|
8598
8734
|
}).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
8735
|
}
|
|
@@ -8713,6 +8849,25 @@ function excerpt(text, terms) {
|
|
|
8713
8849
|
return `${start > 0 ? "\u2026" : ""}${text.slice(start, end).trim()}${end < text.length ? "\u2026" : ""}`;
|
|
8714
8850
|
}
|
|
8715
8851
|
|
|
8852
|
+
// src/documentation/text.ts
|
|
8853
|
+
function slugify(value) {
|
|
8854
|
+
return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8855
|
+
}
|
|
8856
|
+
function documentationSubject(name) {
|
|
8857
|
+
const normalized = name.trim();
|
|
8858
|
+
if (!normalized || /^documentation$/i.test(normalized)) return "documentation";
|
|
8859
|
+
return /(?:\bdocs?|documentation)$/i.test(normalized) ? normalized : `${normalized} documentation`;
|
|
8860
|
+
}
|
|
8861
|
+
function createSlugger(fallback = "section") {
|
|
8862
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8863
|
+
return (value) => {
|
|
8864
|
+
const base = slugify(value) || fallback;
|
|
8865
|
+
const count = seen.get(base) ?? 0;
|
|
8866
|
+
seen.set(base, count + 1);
|
|
8867
|
+
return count === 0 ? base : `${base}-${count + 1}`;
|
|
8868
|
+
};
|
|
8869
|
+
}
|
|
8870
|
+
|
|
8716
8871
|
// src/documentation/worker/published-corpus.ts
|
|
8717
8872
|
var cachedCorpus;
|
|
8718
8873
|
async function loadDocumentationPublishedManifest(env, requestUrl = "https://assets.local/", assetBasePath = "/_documentation") {
|
|
@@ -8770,7 +8925,7 @@ async function resolveDocumentationPublishedCorpus(env, sourceHash, requestUrl =
|
|
|
8770
8925
|
}
|
|
8771
8926
|
async function readDocumentationPublishedPage(env, corpus, requestedRoute, requestUrl = "https://assets.local/", assetBasePath = "/_documentation") {
|
|
8772
8927
|
const route = collapseRoute(requestedRoute.split("#", 1)[0] ?? "/");
|
|
8773
|
-
const firstChunk = corpus.find((chunk) => chunk.route === route);
|
|
8928
|
+
const firstChunk = corpus.find((chunk) => (chunk.route.split("#", 1)[0] ?? "") === route);
|
|
8774
8929
|
if (!firstChunk) return null;
|
|
8775
8930
|
const pathname = route === "/" ? `${assetBasePath}/index.md` : `${assetBasePath}${route}.md`;
|
|
8776
8931
|
const response = await env.ASSETS.fetch(new Request(new URL(pathname, requestUrl)));
|
|
@@ -8779,6 +8934,36 @@ async function readDocumentationPublishedPage(env, corpus, requestedRoute, reque
|
|
|
8779
8934
|
if (markdown.length > 5e5) throw new Error("Documentation page exceeds 500000 characters");
|
|
8780
8935
|
return { route, title: firstChunk.title, markdown };
|
|
8781
8936
|
}
|
|
8937
|
+
function documentationPageSection(markdown, fragment) {
|
|
8938
|
+
const target = slugify(fragment);
|
|
8939
|
+
if (!target) return null;
|
|
8940
|
+
const slug = createSlugger();
|
|
8941
|
+
const lines = markdown.split("\n");
|
|
8942
|
+
let fence = null;
|
|
8943
|
+
let start = -1;
|
|
8944
|
+
let startDepth = 0;
|
|
8945
|
+
for (let index = 0; index < lines.length; index++) {
|
|
8946
|
+
const line = lines[index] ?? "";
|
|
8947
|
+
const fenceMarker = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
|
|
8948
|
+
if (fenceMarker && !fence) {
|
|
8949
|
+
fence = fenceMarker;
|
|
8950
|
+
continue;
|
|
8951
|
+
}
|
|
8952
|
+
if (fence) {
|
|
8953
|
+
if (fenceMarker?.startsWith(fence)) fence = null;
|
|
8954
|
+
continue;
|
|
8955
|
+
}
|
|
8956
|
+
const heading = line.match(/^ {0,3}(#{1,6})\s+(.+)$/);
|
|
8957
|
+
if (!heading) continue;
|
|
8958
|
+
const depth = (heading[1] ?? "").length;
|
|
8959
|
+
if (start !== -1 && depth <= startDepth) return lines.slice(start, index).join("\n").trim();
|
|
8960
|
+
if (start === -1 && slug(heading[2] ?? "") === target) {
|
|
8961
|
+
start = index;
|
|
8962
|
+
startDepth = depth;
|
|
8963
|
+
}
|
|
8964
|
+
}
|
|
8965
|
+
return start === -1 ? null : lines.slice(start).join("\n").trim();
|
|
8966
|
+
}
|
|
8782
8967
|
function parseAssistantPolicy(value) {
|
|
8783
8968
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
8784
8969
|
const policy = value;
|
|
@@ -9028,6 +9213,17 @@ function registerDocumentationStaticRoutes(app, publicBasePath = "/docs", assetB
|
|
|
9028
9213
|
status = 404;
|
|
9029
9214
|
}
|
|
9030
9215
|
if (status === 404) {
|
|
9216
|
+
if (pathname.endsWith(".md")) {
|
|
9217
|
+
return secureDocumentationResponse(
|
|
9218
|
+
new Response(
|
|
9219
|
+
`No documentation page at ${pathname}. See ${publicBasePath}/llms.txt for every published page.
|
|
9220
|
+
`,
|
|
9221
|
+
{ status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" } }
|
|
9222
|
+
),
|
|
9223
|
+
404,
|
|
9224
|
+
pathname
|
|
9225
|
+
);
|
|
9226
|
+
}
|
|
9031
9227
|
const url = new URL(c.req.url);
|
|
9032
9228
|
url.pathname = `${assetBasePath}/404.html`;
|
|
9033
9229
|
response = await fetchDocumentationAsset(c.env, c.req.raw, url, assetBasePath);
|
|
@@ -9085,13 +9281,6 @@ function secureDocumentationResponse(response, status, pathname) {
|
|
|
9085
9281
|
return new Response(response.body, { status, headers });
|
|
9086
9282
|
}
|
|
9087
9283
|
|
|
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
9284
|
// src/documentation/worker/mcp/protocol.ts
|
|
9096
9285
|
var MCP_SERVER_VERSION = "1.0.0";
|
|
9097
9286
|
var CURRENT_PROTOCOL_VERSION = "2025-11-25";
|
|
@@ -9203,7 +9392,7 @@ var DOCUMENTATION_TOOLS = [
|
|
|
9203
9392
|
},
|
|
9204
9393
|
outputSchema: {
|
|
9205
9394
|
type: "object",
|
|
9206
|
-
properties: { results: { type: "array" } },
|
|
9395
|
+
properties: { notice: { type: "string" }, results: { type: "array" } },
|
|
9207
9396
|
required: ["results"]
|
|
9208
9397
|
},
|
|
9209
9398
|
annotations: {
|
|
@@ -9216,11 +9405,11 @@ var DOCUMENTATION_TOOLS = [
|
|
|
9216
9405
|
{
|
|
9217
9406
|
name: "documentation_read",
|
|
9218
9407
|
title: "Read documentation page",
|
|
9219
|
-
description: "Read one
|
|
9408
|
+
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
9409
|
inputSchema: {
|
|
9221
9410
|
type: "object",
|
|
9222
9411
|
properties: {
|
|
9223
|
-
route: { type: "string", minLength: 1, maxLength: 500
|
|
9412
|
+
route: { type: "string", minLength: 1, maxLength: 500 }
|
|
9224
9413
|
},
|
|
9225
9414
|
required: ["route"],
|
|
9226
9415
|
additionalProperties: false
|
|
@@ -9228,6 +9417,7 @@ var DOCUMENTATION_TOOLS = [
|
|
|
9228
9417
|
outputSchema: {
|
|
9229
9418
|
type: "object",
|
|
9230
9419
|
properties: {
|
|
9420
|
+
notice: { type: "string" },
|
|
9231
9421
|
route: { type: "string" },
|
|
9232
9422
|
title: { type: "string" },
|
|
9233
9423
|
url: { type: "string" },
|
|
@@ -9254,19 +9444,24 @@ async function callDocumentationTool(rawRequest, request, corpus, env, basePath
|
|
|
9254
9444
|
if (query.length < 2 || query.length > 300 || !Number.isInteger(limit) || Number(limit) < 1 || Number(limit) > 10) {
|
|
9255
9445
|
return mcpError(request.id, -32602, "Invalid documentation_search arguments");
|
|
9256
9446
|
}
|
|
9257
|
-
const
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9447
|
+
const results = searchDocumentationCorpus(corpus, query, Number(limit)).map((result) => ({
|
|
9448
|
+
...result,
|
|
9449
|
+
url: new URL(documentationPublicPath(basePath, result.route), origin).toString()
|
|
9450
|
+
}));
|
|
9451
|
+
return toolResult(request.id, {
|
|
9452
|
+
...results[0]?.termCoverage === 1 ? {} : {
|
|
9453
|
+
notice: "No section matches every search term \u2014 the topic may be absent or spread across sections."
|
|
9454
|
+
},
|
|
9455
|
+
results
|
|
9456
|
+
});
|
|
9264
9457
|
}
|
|
9265
9458
|
if (name === "documentation_read") {
|
|
9266
9459
|
const route = typeof args.route === "string" ? args.route.trim() : "";
|
|
9267
|
-
if (!route
|
|
9460
|
+
if (!route || route.length > 500) {
|
|
9268
9461
|
return mcpError(request.id, -32602, "Invalid documentation_read arguments");
|
|
9269
9462
|
}
|
|
9463
|
+
const hash = route.indexOf("#");
|
|
9464
|
+
const fragment = hash === -1 ? "" : route.slice(hash + 1);
|
|
9270
9465
|
try {
|
|
9271
9466
|
const page = await readDocumentationPublishedPage(
|
|
9272
9467
|
env,
|
|
@@ -9276,8 +9471,11 @@ async function callDocumentationTool(rawRequest, request, corpus, env, basePath
|
|
|
9276
9471
|
assetBasePath
|
|
9277
9472
|
);
|
|
9278
9473
|
if (!page) return toolError(request.id, `No published documentation page exists at ${route}`);
|
|
9474
|
+
const section = fragment ? documentationPageSection(page.markdown, fragment) : null;
|
|
9279
9475
|
return toolResult(request.id, {
|
|
9476
|
+
...fragment && section === null ? { notice: `The fragment #${fragment} was not found; returning the whole page.` } : {},
|
|
9280
9477
|
...page,
|
|
9478
|
+
...section === null ? {} : { markdown: section },
|
|
9281
9479
|
url: new URL(documentationPublicPath(basePath, page.route), origin).toString()
|
|
9282
9480
|
});
|
|
9283
9481
|
} catch (error) {
|
|
@@ -10085,6 +10283,8 @@ export {
|
|
|
10085
10283
|
meterAi,
|
|
10086
10284
|
meterUsage,
|
|
10087
10285
|
meterVectorize,
|
|
10286
|
+
mintAppId,
|
|
10287
|
+
mintUlid,
|
|
10088
10288
|
noopPermissionContext,
|
|
10089
10289
|
parseQuotaLimit,
|
|
10090
10290
|
platformCorsPatterns,
|