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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/{chunk-A7UFIOWO.js → chunk-7TDBFEB7.js} +7 -2
  3. package/dist/chunk-7TDBFEB7.js.map +1 -0
  4. package/dist/cli.js +288 -120
  5. package/dist/cli.js.map +1 -1
  6. package/dist/documentation-client-core.d.ts +3 -2
  7. package/dist/documentation-client-core.js +1 -1
  8. package/dist/documentation-default-renderer.cjs +48 -48
  9. package/dist/documentation-react.d.ts +2 -1
  10. package/dist/documentation-react.js +1 -1
  11. package/dist/documentation-runtime.js +7 -7
  12. package/dist/documentation-server-core.d.ts +3 -2
  13. package/dist/documentation-server-core.js +1 -1
  14. package/dist/documentation.d.ts +40 -12
  15. package/dist/documentation.js +40 -7
  16. package/dist/documentation.js.map +1 -1
  17. package/dist/index.d.ts +225 -21
  18. package/dist/index.js +239 -100
  19. package/dist/index.js.map +1 -1
  20. package/dist/{public-CFAj1hL2.d.ts → public-DenQ1TsZ.d.ts} +36 -11
  21. package/dist/server.d.ts +32 -4
  22. package/dist/server.js +177 -43
  23. package/dist/server.js.map +1 -1
  24. package/dist/testing-mcp.d.ts +69 -0
  25. package/dist/testing-mcp.js +99 -0
  26. package/dist/testing-mcp.js.map +1 -0
  27. package/dist/testing.d.ts +17 -20
  28. package/dist/testing.js +47 -25
  29. package/dist/testing.js.map +1 -1
  30. package/dist/worker.d.ts +57 -5
  31. package/dist/worker.js +262 -62
  32. package/dist/worker.js.map +1 -1
  33. package/features/ai-chat/feature.json +2 -2
  34. package/features/ai-chat/src/AiChatPage.tsx +56 -21
  35. package/features/ai-chat/src/ChatPanel.tsx +33 -10
  36. package/features/ai-chat/src/__tests__/ChatPanel.lifecycle.test.tsx +156 -0
  37. package/features/cron/src/CronLogPage.tsx +107 -69
  38. package/features/file-attachments/feature.json +2 -1
  39. package/features/file-manager/feature.json +2 -1
  40. package/features/file-manager/src/FileManagerPage.tsx +227 -194
  41. package/features/integration-test/src/IntegrationTestPage.tsx +54 -34
  42. package/package.json +5 -1
  43. 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
- this.sendTo(ws, {
3998
- type: MSG.ERROR,
3999
- payload: { error: "Write access denied: viewer role cannot modify cron tasks" }
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
- this.sendTo(ws, { type: MSG.ERROR, payload: { error: "Missing taskName" } });
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
- if (!await this.executeTask(taskName)) {
4011
- this.sendTo(ws, {
4012
- type: MSG.ERROR,
4013
- payload: { error: `Unknown cron task: ${taskName}` }
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
- this.sql.exec(`UPDATE cron_tasks SET paused = 0 WHERE name = ?`, taskName);
4027
- this.scheduleNextAlarm();
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 false;
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 true;
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
- { success: true, key, url: fileUrl.toString(), name },
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(bucket, session, reservation, completed, auth, url, scope);
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 listed = await bucket.list({ prefix: listPrefix, limit });
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 truncated = listed.truncated;
7260
- for (const excluded of excludedPrefixes.filter((candidate) => candidate.startsWith(listPrefix))) {
7261
- if (visible.length >= limit) break;
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 lastListedKey = listed.objects.at(-1)?.key;
7264
- const after = await bucket.list({
7265
- prefix: listPrefix,
7266
- startAfter: lastListedKey && lastListedKey > excludedEnd ? lastListedKey : excludedEnd,
7267
- limit: limit - visible.length
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
- truncated = after.truncated;
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
- { files, truncated, ...storage ? { storage } : {} },
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 complete published documentation page as Markdown.",
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, pattern: "^/" }
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 value = {
9258
- results: searchDocumentationCorpus(corpus, query, Number(limit)).map((result) => ({
9259
- ...result,
9260
- url: new URL(documentationPublicPath(basePath, result.route), origin).toString()
9261
- }))
9262
- };
9263
- return toolResult(request.id, value);
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.startsWith("/") || route.length > 500) {
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,