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.
Files changed (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/{chunk-A7UFIOWO.js → chunk-KDCDRHBA.js} +7 -2
  3. package/dist/chunk-KDCDRHBA.js.map +1 -0
  4. package/dist/cli.js +193 -62
  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 +21 -0
  22. package/dist/server.js +148 -39
  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 +48 -3
  31. package/dist/worker.js +234 -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) {
@@ -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
- { success: true, key, url: fileUrl.toString(), name },
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(bucket, session, reservation, completed, auth, url, scope);
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 listed = await bucket.list({ prefix: listPrefix, limit });
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 truncated = listed.truncated;
7260
- for (const excluded of excludedPrefixes.filter((candidate) => candidate.startsWith(listPrefix))) {
7261
- if (visible.length >= limit) break;
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 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
- });
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
- truncated = after.truncated;
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
- { files, truncated, ...storage ? { storage } : {} },
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 complete published documentation page as Markdown.",
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, pattern: "^/" }
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 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);
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.startsWith("/") || route.length > 500) {
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) {