deepspace 0.10.0 → 0.10.2
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 +15 -0
- package/dist/{chunk-DQ4G6VL6.js → chunk-T7V4GUIN.js} +283 -180
- package/dist/chunk-T7V4GUIN.js.map +1 -0
- package/dist/cli.js +1010 -6123
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.d.ts +18 -0
- package/dist/{docs-client-core.js → documentation-client-core.js} +60 -51
- package/dist/documentation-client-core.js.map +1 -0
- package/dist/documentation-default-renderer.cjs +124 -0
- package/dist/documentation-react.d.ts +2 -0
- package/dist/{docs-react.js → documentation-react.js} +8 -8
- package/dist/documentation-runtime.js +71 -0
- package/dist/documentation-server-core.d.ts +11 -0
- package/dist/{docs-server-core.js → documentation-server-core.js} +13 -13
- package/dist/documentation-server-core.js.map +1 -0
- package/dist/documentation.d.ts +366 -0
- package/dist/documentation.js +3953 -0
- package/dist/documentation.js.map +1 -0
- package/dist/feature-installer.js +145 -0
- package/dist/feature-installer.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.js +5 -15
- package/dist/index.js.map +1 -1
- package/dist/public-BGRX_Cjd.d.ts +234 -0
- package/dist/schema.d.ts +186 -0
- package/dist/schema.js +610 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +19 -167
- package/dist/server.js +76 -1069
- package/dist/server.js.map +1 -1
- package/dist/testing.d.ts +5 -8
- package/dist/testing.js +48 -14
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +52 -173
- package/dist/worker.js +563 -492
- package/dist/worker.js.map +1 -1
- package/features/admin-page/src/admin-schema.ts +1 -1
- package/features/ai-chat/feature.json +2 -2
- package/features/ai-chat/src/ChatPanel.tsx +0 -1
- package/features/ai-chat/src/ai-chat-schema.ts +1 -2
- package/features/canvas/src/canvas-schema.ts +1 -1
- package/features/documentation/content/index.mdx +14 -0
- package/features/documentation/content/quickstart.mdx +12 -0
- package/features/documentation/documentation.json +18 -0
- package/features/documentation/feature.json +68 -0
- package/features/{docs → documents}/feature.json +27 -27
- package/features/{docs/src/DocsEditorPage.tsx → documents/src/DocumentsEditorPage.tsx} +74 -74
- package/features/{docs/src/DocsListPage.tsx → documents/src/DocumentsListPage.tsx} +102 -102
- package/features/{docs/src/DocsOutlinePanel.tsx → documents/src/DocumentsOutlinePanel.tsx} +9 -9
- package/features/{docs/src/DocsPresence.tsx → documents/src/DocumentsPresence.tsx} +18 -18
- package/features/{docs → documents}/src/InviteDialog.tsx +21 -21
- package/features/{docs → documents}/src/LibrarySidebar.tsx +25 -25
- package/features/documents/src/__tests__/documents-presence-access.test.ts +71 -0
- package/features/{docs/src/docs-library-types.ts → documents/src/documents-library-types.ts} +3 -3
- package/features/{docs/src/docs-schema.ts → documents/src/documents-schema.ts} +6 -6
- package/features/{docs/src/docs-ui.css → documents/src/documents-ui.css} +89 -89
- package/features/{docs → documents}/src/editor/DocEditorSurface.tsx +11 -11
- package/features/{docs/src/editor/DocsTiptapToolbar.tsx → documents/src/editor/DocumentsTiptapToolbar.tsx} +21 -21
- package/features/{docs → documents}/src/editor/useDocEditor.ts +4 -4
- package/features/{docs/src/use-docs-presence-access.ts → documents/src/use-documents-presence-access.ts} +51 -51
- package/features/file-attachments/src/attachments-schema.ts +1 -1
- package/features/items/src/items-schema.ts +1 -1
- package/features/leaderboard/src/leaderboard-schema.ts +1 -1
- package/features/messaging/src/ChatMultiPage.tsx +1 -1
- package/features/messaging/src/ChatPage.tsx +1 -1
- package/features/messaging/src/messaging-schema.ts +2 -2
- package/features/tasks/src/tasks-schema.ts +1 -1
- package/features/testing/src/test-schema.ts +2 -2
- package/package.json +12 -7
- package/dist/chunk-DQ4G6VL6.js.map +0 -1
- package/dist/docs-client-core.d.ts +0 -18
- package/dist/docs-client-core.js.map +0 -1
- package/dist/docs-react.d.ts +0 -2
- package/dist/docs-runtime.js +0 -71
- package/dist/docs-server-core.d.ts +0 -11
- package/dist/docs-server-core.js.map +0 -1
- package/dist/docs.d.ts +0 -350
- package/dist/docs.js +0 -5139
- package/dist/docs.js.map +0 -1
- package/dist/public-Cp1X8ZQ3.d.ts +0 -230
- package/features/docs/src/__tests__/docs-presence-access.test.ts +0 -71
- /package/dist/{docs-react.js.map → documentation-react.js.map} +0 -0
package/dist/server.js
CHANGED
|
@@ -475,7 +475,7 @@ function authWorkerFetch(env, path, init) {
|
|
|
475
475
|
function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
476
476
|
const roomIdObj = env.RECORD_ROOMS.idFromName(roomId);
|
|
477
477
|
const room = env.RECORD_ROOMS.get(roomIdObj);
|
|
478
|
-
async function executeTool3(
|
|
478
|
+
async function executeTool3(tool2, params) {
|
|
479
479
|
const response = await room.fetch(
|
|
480
480
|
new Request("https://internal/api/tools/execute", {
|
|
481
481
|
method: "POST",
|
|
@@ -484,12 +484,12 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
|
|
|
484
484
|
"X-User-Id": ownerUserId,
|
|
485
485
|
"X-App-Action": "true"
|
|
486
486
|
},
|
|
487
|
-
body: JSON.stringify({ tool:
|
|
487
|
+
body: JSON.stringify({ tool: tool2, params })
|
|
488
488
|
})
|
|
489
489
|
);
|
|
490
490
|
const result = await response.json();
|
|
491
491
|
if (!result.success) {
|
|
492
|
-
throw new Error(`RecordRoom tool ${
|
|
492
|
+
throw new Error(`RecordRoom tool ${tool2} failed: ${result.error || "Unknown error"}`);
|
|
493
493
|
}
|
|
494
494
|
return result.data;
|
|
495
495
|
}
|
|
@@ -702,7 +702,8 @@ var DEEPSPACE_AGENT_PROFILES = {
|
|
|
702
702
|
application: {
|
|
703
703
|
id: "application",
|
|
704
704
|
defaultModel: DEEPSPACE_AI_DEFAULTS.agent,
|
|
705
|
-
maxSteps:
|
|
705
|
+
maxSteps: 21,
|
|
706
|
+
maxToolCalls: 20,
|
|
706
707
|
allowedTools: "application-defined"
|
|
707
708
|
},
|
|
708
709
|
documentation: {
|
|
@@ -712,7 +713,7 @@ var DEEPSPACE_AGENT_PROFILES = {
|
|
|
712
713
|
// step. The runner disables tools once the tool-call budget is exhausted.
|
|
713
714
|
maxSteps: 21,
|
|
714
715
|
maxToolCalls: 20,
|
|
715
|
-
allowedTools: ["
|
|
716
|
+
allowedTools: ["documentation_search", "documentation_read"]
|
|
716
717
|
}
|
|
717
718
|
};
|
|
718
719
|
function getDeepSpaceAIModel(modelId) {
|
|
@@ -760,11 +761,7 @@ function streamDeepSpaceAgent(env, options) {
|
|
|
760
761
|
}
|
|
761
762
|
}
|
|
762
763
|
}
|
|
763
|
-
const provider = createDeepSpaceAI(
|
|
764
|
-
env,
|
|
765
|
-
selection.provider,
|
|
766
|
-
authToken ? { authToken } : {}
|
|
767
|
-
);
|
|
764
|
+
const provider = createDeepSpaceAI(env, selection.provider, authToken ? { authToken } : {});
|
|
768
765
|
const { prepareStep, tools, ...remainingStreamOptions } = streamOptions;
|
|
769
766
|
const boundedTools = limitToolExecutions(tools, selection.profile.maxToolCalls);
|
|
770
767
|
const result = streamText({
|
|
@@ -792,22 +789,27 @@ function enforceToolCallLimit(prepareStep, maxToolCalls) {
|
|
|
792
789
|
function limitToolExecutions(tools, maxToolCalls) {
|
|
793
790
|
if (!tools || maxToolCalls === void 0) return tools;
|
|
794
791
|
let executions = 0;
|
|
795
|
-
return Object.fromEntries(
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
792
|
+
return Object.fromEntries(
|
|
793
|
+
Object.entries(tools).map(([name, definition]) => {
|
|
794
|
+
if (typeof definition.execute !== "function") return [name, definition];
|
|
795
|
+
const execute = definition.execute;
|
|
796
|
+
return [
|
|
797
|
+
name,
|
|
798
|
+
{
|
|
799
|
+
...definition,
|
|
800
|
+
execute: (...args) => {
|
|
801
|
+
if (executions >= maxToolCalls) {
|
|
802
|
+
return Promise.resolve({
|
|
803
|
+
error: `The agent tool-call limit of ${maxToolCalls} has been reached`
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
executions++;
|
|
807
|
+
return execute(...args);
|
|
808
|
+
}
|
|
805
809
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}];
|
|
810
|
-
}));
|
|
810
|
+
];
|
|
811
|
+
})
|
|
812
|
+
);
|
|
811
813
|
}
|
|
812
814
|
function deepSpaceAgentErrorSummary(error, context = {}) {
|
|
813
815
|
const details = [];
|
|
@@ -872,7 +874,10 @@ function safeLogText(value) {
|
|
|
872
874
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
873
875
|
return codePoint <= 31 || codePoint === 127 ? " " : character;
|
|
874
876
|
}).join("");
|
|
875
|
-
return withoutControlCharacters.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(
|
|
877
|
+
return withoutControlCharacters.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(
|
|
878
|
+
/\b(api[_ -]?key|authorization|password|secret|token)\b\s*[:=]\s*\S+/gi,
|
|
879
|
+
"$1=[redacted]"
|
|
880
|
+
).trim().slice(0, 300);
|
|
876
881
|
}
|
|
877
882
|
|
|
878
883
|
// src/server/utils/composio-tools.ts
|
|
@@ -1311,11 +1316,10 @@ function formatTranscript(messages) {
|
|
|
1311
1316
|
}
|
|
1312
1317
|
function makeDefaultSummarizer(env, options = {}) {
|
|
1313
1318
|
return async (messages) => {
|
|
1314
|
-
const
|
|
1315
|
-
if (!model) throw new Error("DeepSpace summarization model is missing from the catalog");
|
|
1316
|
-
const provider = createDeepSpaceAI(env, model.provider, { authToken: options.authToken });
|
|
1319
|
+
const anthropic = createDeepSpaceAI(env, "anthropic", { authToken: options.authToken });
|
|
1317
1320
|
const { text } = await generateText({
|
|
1318
|
-
|
|
1321
|
+
// Stable alias — provider bug-fix snapshots roll forward without code change.
|
|
1322
|
+
model: anthropic("claude-haiku-4-5"),
|
|
1319
1323
|
// 2500 gives the model room to finish a sentence after hitting the
|
|
1320
1324
|
// "under 2000 tokens" soft limit in the prompt.
|
|
1321
1325
|
maxOutputTokens: 2500,
|
|
@@ -1327,7 +1331,7 @@ function makeDefaultSummarizer(env, options = {}) {
|
|
|
1327
1331
|
}
|
|
1328
1332
|
|
|
1329
1333
|
// src/server/utils/chat-history.ts
|
|
1330
|
-
async function executeTool(stub, userId,
|
|
1334
|
+
async function executeTool(stub, userId, tool2, params) {
|
|
1331
1335
|
const res = await stub.fetch(new Request("https://internal/api/tools/execute", {
|
|
1332
1336
|
method: "POST",
|
|
1333
1337
|
headers: {
|
|
@@ -1335,11 +1339,11 @@ async function executeTool(stub, userId, tool3, params) {
|
|
|
1335
1339
|
"X-User-Id": userId,
|
|
1336
1340
|
"X-App-Action": "true"
|
|
1337
1341
|
},
|
|
1338
|
-
body: JSON.stringify({ tool:
|
|
1342
|
+
body: JSON.stringify({ tool: tool2, params })
|
|
1339
1343
|
}));
|
|
1340
1344
|
const body = await res.json();
|
|
1341
1345
|
if (!body.success) {
|
|
1342
|
-
throw new Error(`chat-history ${
|
|
1346
|
+
throw new Error(`chat-history ${tool2} failed: ${body.error ?? "unknown error"}`);
|
|
1343
1347
|
}
|
|
1344
1348
|
return body.data;
|
|
1345
1349
|
}
|
|
@@ -1451,1002 +1455,6 @@ async function appendMessage(stub, msg) {
|
|
|
1451
1455
|
});
|
|
1452
1456
|
}
|
|
1453
1457
|
|
|
1454
|
-
// src/server/utils/docs-assistant.ts
|
|
1455
|
-
import { tool as tool2 } from "ai";
|
|
1456
|
-
import { z } from "zod/v4";
|
|
1457
|
-
var MAX_CHUNKS = 1e4;
|
|
1458
|
-
var MAX_CHUNK_CHARS = 2500;
|
|
1459
|
-
var MAX_CORPUS_CHARS = 12e6;
|
|
1460
|
-
var SEARCH_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1461
|
-
"a",
|
|
1462
|
-
"about",
|
|
1463
|
-
"an",
|
|
1464
|
-
"and",
|
|
1465
|
-
"are",
|
|
1466
|
-
"can",
|
|
1467
|
-
"could",
|
|
1468
|
-
"do",
|
|
1469
|
-
"does",
|
|
1470
|
-
"for",
|
|
1471
|
-
"from",
|
|
1472
|
-
"has",
|
|
1473
|
-
"have",
|
|
1474
|
-
"how",
|
|
1475
|
-
"i",
|
|
1476
|
-
"in",
|
|
1477
|
-
"is",
|
|
1478
|
-
"it",
|
|
1479
|
-
"me",
|
|
1480
|
-
"my",
|
|
1481
|
-
"of",
|
|
1482
|
-
"on",
|
|
1483
|
-
"or",
|
|
1484
|
-
"our",
|
|
1485
|
-
"should",
|
|
1486
|
-
"the",
|
|
1487
|
-
"this",
|
|
1488
|
-
"to",
|
|
1489
|
-
"was",
|
|
1490
|
-
"we",
|
|
1491
|
-
"were",
|
|
1492
|
-
"what",
|
|
1493
|
-
"when",
|
|
1494
|
-
"where",
|
|
1495
|
-
"which",
|
|
1496
|
-
"will",
|
|
1497
|
-
"with",
|
|
1498
|
-
"would",
|
|
1499
|
-
"you",
|
|
1500
|
-
"your"
|
|
1501
|
-
]);
|
|
1502
|
-
var indexedChunkCache = /* @__PURE__ */ new WeakMap();
|
|
1503
|
-
function parseDocsCorpus(value) {
|
|
1504
|
-
if (!Array.isArray(value)) throw new Error("Docs assistant index must be an array");
|
|
1505
|
-
if (value.length > MAX_CHUNKS) {
|
|
1506
|
-
throw new Error(`Docs assistant index exceeds ${MAX_CHUNKS} chunks`);
|
|
1507
|
-
}
|
|
1508
|
-
const chunks = [];
|
|
1509
|
-
let totalChars2 = 0;
|
|
1510
|
-
for (let index = 0; index < value.length; index++) {
|
|
1511
|
-
const raw = value[index];
|
|
1512
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1513
|
-
throw new Error(`Docs assistant chunk ${index} must be an object`);
|
|
1514
|
-
}
|
|
1515
|
-
const chunk = raw;
|
|
1516
|
-
if (typeof chunk.id !== "string" || !chunk.id || typeof chunk.route !== "string" || !chunk.route.startsWith("/") || typeof chunk.title !== "string" || !chunk.title || typeof chunk.text !== "string" || !chunk.text) {
|
|
1517
|
-
throw new Error(`Docs assistant chunk ${index} has invalid required fields`);
|
|
1518
|
-
}
|
|
1519
|
-
if (chunk.text.length > MAX_CHUNK_CHARS) {
|
|
1520
|
-
throw new Error(`Docs assistant chunk ${chunk.id} exceeds ${MAX_CHUNK_CHARS} characters`);
|
|
1521
|
-
}
|
|
1522
|
-
totalChars2 += chunk.text.length;
|
|
1523
|
-
if (totalChars2 > MAX_CORPUS_CHARS) {
|
|
1524
|
-
throw new Error(`Docs assistant corpus exceeds ${MAX_CORPUS_CHARS} characters`);
|
|
1525
|
-
}
|
|
1526
|
-
chunks.push({
|
|
1527
|
-
id: chunk.id,
|
|
1528
|
-
route: chunk.route,
|
|
1529
|
-
title: chunk.title,
|
|
1530
|
-
...typeof chunk.heading === "string" && chunk.heading ? { heading: chunk.heading } : {},
|
|
1531
|
-
text: chunk.text
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
return chunks;
|
|
1535
|
-
}
|
|
1536
|
-
function searchDocsCorpus(corpus, query, limit = 6) {
|
|
1537
|
-
const normalized = normalize(query);
|
|
1538
|
-
const terms = [
|
|
1539
|
-
...new Set(
|
|
1540
|
-
normalized.split(" ").filter((term) => term.length > 1 && !SEARCH_STOP_WORDS.has(term))
|
|
1541
|
-
)
|
|
1542
|
-
];
|
|
1543
|
-
if (!terms.length) return [];
|
|
1544
|
-
const requiredTerms = new Set(terms);
|
|
1545
|
-
return corpus.map((chunk) => {
|
|
1546
|
-
const { text, textTerms, title, titleTerms } = indexChunk(chunk);
|
|
1547
|
-
const textCounts = countTerms(textTerms, requiredTerms);
|
|
1548
|
-
let score = 0;
|
|
1549
|
-
if (title === normalized) score += 120;
|
|
1550
|
-
else if (title.includes(normalized)) score += 55;
|
|
1551
|
-
if (text.includes(normalized)) score += 30;
|
|
1552
|
-
for (const term of terms) {
|
|
1553
|
-
if (titleTerms.has(term)) score += 14;
|
|
1554
|
-
score += Math.min(6, textCounts.get(term) ?? 0) * 3;
|
|
1555
|
-
}
|
|
1556
|
-
const matchedTerms = terms.filter(
|
|
1557
|
-
(term) => titleTerms.has(term) || textCounts.has(term)
|
|
1558
|
-
).length;
|
|
1559
|
-
score += matchedTerms * matchedTerms * 4;
|
|
1560
|
-
score += proximityBonus([...titleTerms, ...textTerms], terms);
|
|
1561
|
-
return {
|
|
1562
|
-
route: chunk.route,
|
|
1563
|
-
title: chunk.title,
|
|
1564
|
-
...chunk.heading ? { heading: chunk.heading } : {},
|
|
1565
|
-
excerpt: excerpt(chunk.text, terms),
|
|
1566
|
-
score
|
|
1567
|
-
};
|
|
1568
|
-
}).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)));
|
|
1569
|
-
}
|
|
1570
|
-
function readDocsRoute(corpus, route) {
|
|
1571
|
-
const normalizedRoute = normalizeDocsRoute(route);
|
|
1572
|
-
return corpus.filter((chunk) => chunk.route === normalizedRoute).slice(0, 12).map(({ route: chunkRoute, title, heading, text }) => ({
|
|
1573
|
-
route: chunkRoute,
|
|
1574
|
-
title,
|
|
1575
|
-
...heading ? { heading } : {},
|
|
1576
|
-
text
|
|
1577
|
-
}));
|
|
1578
|
-
}
|
|
1579
|
-
function buildDocsAssistantTools(corpus) {
|
|
1580
|
-
return {
|
|
1581
|
-
docs_search: tool2({
|
|
1582
|
-
description: "Search the current documentation. Use this before answering factual or how-to questions.",
|
|
1583
|
-
inputSchema: z.object({
|
|
1584
|
-
query: z.string().trim().min(2).max(300),
|
|
1585
|
-
limit: z.number().int().min(1).max(10).optional()
|
|
1586
|
-
}),
|
|
1587
|
-
execute: async ({ query, limit }) => ({
|
|
1588
|
-
results: searchDocsCorpus(corpus, query, limit ?? 6)
|
|
1589
|
-
})
|
|
1590
|
-
}),
|
|
1591
|
-
docs_read: tool2({
|
|
1592
|
-
description: "Read bounded source chunks for one exact documentation route returned by docs_search.",
|
|
1593
|
-
inputSchema: z.object({ route: z.string().trim().startsWith("/").max(500) }),
|
|
1594
|
-
execute: async ({ route }) => ({
|
|
1595
|
-
route: normalizeDocsRoute(route),
|
|
1596
|
-
chunks: readDocsRoute(corpus, route)
|
|
1597
|
-
})
|
|
1598
|
-
})
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
function buildDocsAssistantPrompt(appName, currentRoute) {
|
|
1602
|
-
return [
|
|
1603
|
-
`You are the public documentation assistant for "${appName}" on DeepSpace.`,
|
|
1604
|
-
"Answer only from the documentation available through docs_search and docs_read.",
|
|
1605
|
-
"Search before answering unless the user is only greeting you.",
|
|
1606
|
-
"Use docs_read when search excerpts are insufficient or ambiguous.",
|
|
1607
|
-
"Never claim access to application records, integrations, private data, or mutation tools.",
|
|
1608
|
-
"If the documentation does not support an answer, say so plainly and suggest the closest documented page.",
|
|
1609
|
-
"Cite factual answers with Markdown links to the exact documentation routes returned by tools.",
|
|
1610
|
-
"Do not invent URLs, APIs, configuration keys, versions, or behavior.",
|
|
1611
|
-
"Keep answers concise, practical, and explicit about uncertainty.",
|
|
1612
|
-
...currentRoute ? [`The reader is currently viewing ${normalizeDocsRoute(currentRoute)}.`] : []
|
|
1613
|
-
].join("\n");
|
|
1614
|
-
}
|
|
1615
|
-
function normalize(value) {
|
|
1616
|
-
return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(" ").filter(Boolean).map(canonicalSearchTerm).join(" ");
|
|
1617
|
-
}
|
|
1618
|
-
function canonicalSearchTerm(term) {
|
|
1619
|
-
if (SEARCH_STOP_WORDS.has(term)) return term;
|
|
1620
|
-
if (["doc", "docs", "document", "documents", "documentation"].includes(term)) return "docs";
|
|
1621
|
-
if (["builds", "building", "built", "compile", "compiled", "compiles", "compiling"].includes(term)) {
|
|
1622
|
-
return "build";
|
|
1623
|
-
}
|
|
1624
|
-
return term.length > 3 && term.endsWith("s") && !term.endsWith("ss") ? term.slice(0, -1) : term;
|
|
1625
|
-
}
|
|
1626
|
-
function indexChunk(chunk) {
|
|
1627
|
-
const cached = indexedChunkCache.get(chunk);
|
|
1628
|
-
if (cached) return cached;
|
|
1629
|
-
const title = normalize(`${chunk.title} ${chunk.heading ?? ""}`);
|
|
1630
|
-
const text = normalize(chunk.text);
|
|
1631
|
-
const indexed = {
|
|
1632
|
-
text,
|
|
1633
|
-
textTerms: text.split(" "),
|
|
1634
|
-
title,
|
|
1635
|
-
titleTerms: new Set(title.split(" "))
|
|
1636
|
-
};
|
|
1637
|
-
indexedChunkCache.set(chunk, indexed);
|
|
1638
|
-
return indexed;
|
|
1639
|
-
}
|
|
1640
|
-
function normalizeDocsRoute(value) {
|
|
1641
|
-
let route = `/${value}`.replace(/\/{2,}/g, "/");
|
|
1642
|
-
if (route.length > 1) route = route.replace(/\/$/, "");
|
|
1643
|
-
return route;
|
|
1644
|
-
}
|
|
1645
|
-
function countTerms(tokens, required) {
|
|
1646
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1647
|
-
for (const token of tokens) {
|
|
1648
|
-
if (required.has(token)) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
1649
|
-
}
|
|
1650
|
-
return counts;
|
|
1651
|
-
}
|
|
1652
|
-
function proximityBonus(tokens, terms) {
|
|
1653
|
-
const required = new Set(terms);
|
|
1654
|
-
if (required.size < 2) return 0;
|
|
1655
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1656
|
-
let matched = 0;
|
|
1657
|
-
let start = 0;
|
|
1658
|
-
let smallestWindow = Number.POSITIVE_INFINITY;
|
|
1659
|
-
for (let end = 0; end < tokens.length; end++) {
|
|
1660
|
-
const token = tokens[end] ?? "";
|
|
1661
|
-
if (required.has(token)) {
|
|
1662
|
-
const count = counts.get(token) ?? 0;
|
|
1663
|
-
counts.set(token, count + 1);
|
|
1664
|
-
if (count === 0) matched++;
|
|
1665
|
-
}
|
|
1666
|
-
while (matched === required.size && start <= end) {
|
|
1667
|
-
smallestWindow = Math.min(smallestWindow, end - start + 1);
|
|
1668
|
-
const first = tokens[start] ?? "";
|
|
1669
|
-
if (required.has(first)) {
|
|
1670
|
-
const count = counts.get(first) ?? 0;
|
|
1671
|
-
counts.set(first, count - 1);
|
|
1672
|
-
if (count === 1) matched--;
|
|
1673
|
-
}
|
|
1674
|
-
start++;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
return Number.isFinite(smallestWindow) ? Math.max(0, 42 - smallestWindow * 2) : 0;
|
|
1678
|
-
}
|
|
1679
|
-
function excerpt(text, terms) {
|
|
1680
|
-
const lower = text.toLowerCase();
|
|
1681
|
-
const first = terms.map((term) => lower.indexOf(term)).filter((index) => index >= 0).sort((left, right) => left - right)[0] ?? 0;
|
|
1682
|
-
const start = Math.max(0, first - 160);
|
|
1683
|
-
const end = Math.min(text.length, start + 520);
|
|
1684
|
-
return `${start > 0 ? "\u2026" : ""}${text.slice(start, end).trim()}${end < text.length ? "\u2026" : ""}`;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// src/server/utils/docs-assistant-limiter.ts
|
|
1688
|
-
var WINDOW_MS = 6e4;
|
|
1689
|
-
var CLIENT_REQUESTS_PER_WINDOW = 12;
|
|
1690
|
-
var APP_REQUESTS_PER_WINDOW = 120;
|
|
1691
|
-
var MAX_CONCURRENT_REQUESTS = 4;
|
|
1692
|
-
var LEASE_TTL_MS = 2 * 6e4;
|
|
1693
|
-
var DocsAssistantLimiter = class {
|
|
1694
|
-
sql;
|
|
1695
|
-
constructor(state) {
|
|
1696
|
-
this.sql = state.storage.sql;
|
|
1697
|
-
this.sql.exec(`
|
|
1698
|
-
CREATE TABLE IF NOT EXISTS request_windows (
|
|
1699
|
-
scope TEXT NOT NULL,
|
|
1700
|
-
client_key TEXT NOT NULL,
|
|
1701
|
-
window INTEGER NOT NULL,
|
|
1702
|
-
count INTEGER NOT NULL,
|
|
1703
|
-
PRIMARY KEY (scope, client_key, window)
|
|
1704
|
-
);
|
|
1705
|
-
CREATE TABLE IF NOT EXISTS leases (
|
|
1706
|
-
id TEXT PRIMARY KEY,
|
|
1707
|
-
expires_at INTEGER NOT NULL
|
|
1708
|
-
);
|
|
1709
|
-
`);
|
|
1710
|
-
}
|
|
1711
|
-
async fetch(request) {
|
|
1712
|
-
const url = new URL(request.url);
|
|
1713
|
-
if (request.method !== "POST") return Response.json({ error: "method_not_allowed" }, { status: 405 });
|
|
1714
|
-
if (url.pathname === "/acquire") return this.acquire(request);
|
|
1715
|
-
if (url.pathname === "/release") return this.release(request);
|
|
1716
|
-
return Response.json({ error: "not_found" }, { status: 404 });
|
|
1717
|
-
}
|
|
1718
|
-
async acquire(request) {
|
|
1719
|
-
const body = await request.json().catch(() => null);
|
|
1720
|
-
if (!body || typeof body.clientKey !== "string" || body.clientKey.length !== 64) {
|
|
1721
|
-
return Response.json({ error: "invalid_client_key" }, { status: 400 });
|
|
1722
|
-
}
|
|
1723
|
-
const now = Date.now();
|
|
1724
|
-
const window = Math.floor(now / WINDOW_MS);
|
|
1725
|
-
this.sql.exec("DELETE FROM request_windows WHERE window < ?", window - 1);
|
|
1726
|
-
this.sql.exec("DELETE FROM leases WHERE expires_at <= ?", now);
|
|
1727
|
-
if (!this.consumeWindow("client", body.clientKey, window, CLIENT_REQUESTS_PER_WINDOW)) {
|
|
1728
|
-
return Response.json({ ok: false, reason: "client_rate" }, { status: 429 });
|
|
1729
|
-
}
|
|
1730
|
-
if (!this.consumeWindow("app", "all", window, APP_REQUESTS_PER_WINDOW)) {
|
|
1731
|
-
return Response.json({ ok: false, reason: "app_rate" }, { status: 429 });
|
|
1732
|
-
}
|
|
1733
|
-
const concurrent = this.sql.exec("SELECT COUNT(*) AS count FROM leases").one().count;
|
|
1734
|
-
if (concurrent >= MAX_CONCURRENT_REQUESTS) {
|
|
1735
|
-
return Response.json({ ok: false, reason: "app_concurrency" }, { status: 429 });
|
|
1736
|
-
}
|
|
1737
|
-
const leaseId = crypto.randomUUID();
|
|
1738
|
-
this.sql.exec("INSERT INTO leases (id, expires_at) VALUES (?, ?)", leaseId, now + LEASE_TTL_MS);
|
|
1739
|
-
return Response.json({ ok: true, leaseId });
|
|
1740
|
-
}
|
|
1741
|
-
async release(request) {
|
|
1742
|
-
const body = await request.json().catch(() => null);
|
|
1743
|
-
if (!body || typeof body.leaseId !== "string") {
|
|
1744
|
-
return Response.json({ error: "invalid_lease_id" }, { status: 400 });
|
|
1745
|
-
}
|
|
1746
|
-
this.sql.exec("DELETE FROM leases WHERE id = ?", body.leaseId);
|
|
1747
|
-
return Response.json({ ok: true });
|
|
1748
|
-
}
|
|
1749
|
-
consumeWindow(scope, clientKey, window, limit) {
|
|
1750
|
-
const existing = this.sql.exec(
|
|
1751
|
-
"SELECT count FROM request_windows WHERE scope = ? AND client_key = ? AND window = ?",
|
|
1752
|
-
scope,
|
|
1753
|
-
clientKey,
|
|
1754
|
-
window
|
|
1755
|
-
).toArray()[0]?.count ?? 0;
|
|
1756
|
-
if (existing >= limit) return false;
|
|
1757
|
-
this.sql.exec(
|
|
1758
|
-
`INSERT INTO request_windows (scope, client_key, window, count)
|
|
1759
|
-
VALUES (?, ?, ?, 1)
|
|
1760
|
-
ON CONFLICT(scope, client_key, window) DO UPDATE SET count = count + 1`,
|
|
1761
|
-
scope,
|
|
1762
|
-
clientKey,
|
|
1763
|
-
window
|
|
1764
|
-
);
|
|
1765
|
-
return true;
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
1768
|
-
|
|
1769
|
-
// src/server/utils/docs-published-corpus.ts
|
|
1770
|
-
var cachedCorpus;
|
|
1771
|
-
async function loadDocsPublishedManifest(env, requestUrl = "https://assets.local/") {
|
|
1772
|
-
const response = await env.ASSETS.fetch(new URL("/_docs/manifest.json", requestUrl).toString());
|
|
1773
|
-
if (!response.ok) return null;
|
|
1774
|
-
const value = await response.json().catch(() => null);
|
|
1775
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1776
|
-
const manifest = value;
|
|
1777
|
-
const assistant = parseAssistantPolicy(manifest.assistant);
|
|
1778
|
-
if (typeof manifest.sourceHash !== "string" || typeof manifest.name !== "string" || !assistant) return null;
|
|
1779
|
-
return {
|
|
1780
|
-
sourceHash: manifest.sourceHash,
|
|
1781
|
-
name: manifest.name,
|
|
1782
|
-
assistant,
|
|
1783
|
-
// Older docs artifacts did not carry an MCP policy. Fail closed until the
|
|
1784
|
-
// docs bundle is rebuilt by an MCP-aware SDK release.
|
|
1785
|
-
mcp: parseMcpPolicy(manifest.mcp) ?? { access: "disabled" }
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
async function loadDocsPublishedCorpus(env, sourceHash, requestUrl = "https://assets.local/") {
|
|
1789
|
-
if (cachedCorpus?.sourceHash === sourceHash) return cachedCorpus.chunks;
|
|
1790
|
-
const response = await env.ASSETS.fetch(
|
|
1791
|
-
new URL("/_docs/assistant-index.json", requestUrl).toString()
|
|
1792
|
-
);
|
|
1793
|
-
if (!response.ok) throw new Error(`assistant-index.json returned ${response.status}`);
|
|
1794
|
-
const chunks = parseDocsCorpus(await response.json());
|
|
1795
|
-
cachedCorpus = { sourceHash, chunks };
|
|
1796
|
-
return chunks;
|
|
1797
|
-
}
|
|
1798
|
-
async function resolveDocsPublishedCorpus(env, sourceHash, requestUrl = "https://assets.local/") {
|
|
1799
|
-
try {
|
|
1800
|
-
return { ok: true, chunks: await loadDocsPublishedCorpus(env, sourceHash, requestUrl) };
|
|
1801
|
-
} catch (error) {
|
|
1802
|
-
return { ok: false, error };
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
async function readDocsPublishedPage(env, corpus, requestedRoute, requestUrl = "https://assets.local/") {
|
|
1806
|
-
const route = normalizeDocsRoute(requestedRoute.split("#", 1)[0] ?? "/");
|
|
1807
|
-
const firstChunk = corpus.find((chunk) => chunk.route === route);
|
|
1808
|
-
if (!firstChunk) return null;
|
|
1809
|
-
const pathname = route === "/" ? "/_docs/index.md" : `/_docs${route}.md`;
|
|
1810
|
-
const response = await env.ASSETS.fetch(new URL(pathname, requestUrl).toString());
|
|
1811
|
-
if (!response.ok) return null;
|
|
1812
|
-
const markdown = await response.text();
|
|
1813
|
-
if (markdown.length > 5e5) throw new Error("Documentation page exceeds 500000 characters");
|
|
1814
|
-
return { route, title: firstChunk.title, markdown };
|
|
1815
|
-
}
|
|
1816
|
-
function parseAssistantPolicy(value) {
|
|
1817
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1818
|
-
const policy = value;
|
|
1819
|
-
if (!["disabled", "public", "authenticated"].includes(String(policy.access))) return null;
|
|
1820
|
-
return {
|
|
1821
|
-
access: policy.access,
|
|
1822
|
-
...typeof policy.model === "string" ? { model: policy.model } : {}
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
function parseMcpPolicy(value) {
|
|
1826
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1827
|
-
const policy = value;
|
|
1828
|
-
if (!["disabled", "public"].includes(String(policy.access))) return null;
|
|
1829
|
-
return { access: policy.access };
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
// src/server/utils/docs-public-limiter.ts
|
|
1833
|
-
async function docsPublicClientKey(value) {
|
|
1834
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
1835
|
-
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1836
|
-
}
|
|
1837
|
-
async function acquireDocsPublicLease(env, clientKey, logPrefix) {
|
|
1838
|
-
if (!env.DOCS_ASSISTANT_LIMITER) return { ok: false, status: 503 };
|
|
1839
|
-
const id = env.DOCS_ASSISTANT_LIMITER.idFromName(env.DEEPSPACE_APP_ID);
|
|
1840
|
-
const stub = env.DOCS_ASSISTANT_LIMITER.get(id);
|
|
1841
|
-
const response = await stub.fetch("https://docs-limiter/acquire", {
|
|
1842
|
-
method: "POST",
|
|
1843
|
-
headers: { "Content-Type": "application/json" },
|
|
1844
|
-
body: JSON.stringify({ clientKey })
|
|
1845
|
-
}).catch((error) => {
|
|
1846
|
-
console.error(`[${logPrefix}] limiter unavailable:`, error);
|
|
1847
|
-
return null;
|
|
1848
|
-
});
|
|
1849
|
-
if (!response?.ok) {
|
|
1850
|
-
return { ok: false, status: response?.status === 429 ? 429 : 503 };
|
|
1851
|
-
}
|
|
1852
|
-
const body = await response.json().catch(() => null);
|
|
1853
|
-
if (!body || typeof body.leaseId !== "string") return { ok: false, status: 503 };
|
|
1854
|
-
return { ok: true, lease: { leaseId: body.leaseId, stub } };
|
|
1855
|
-
}
|
|
1856
|
-
async function releaseDocsPublicLease(lease, logPrefix) {
|
|
1857
|
-
if (!lease) return;
|
|
1858
|
-
await lease.stub.fetch("https://docs-limiter/release", {
|
|
1859
|
-
method: "POST",
|
|
1860
|
-
headers: { "Content-Type": "application/json" },
|
|
1861
|
-
body: JSON.stringify({ leaseId: lease.leaseId })
|
|
1862
|
-
}).catch((error) => console.error(`[${logPrefix}] limiter release failed:`, error));
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
// src/server/utils/request-body.ts
|
|
1866
|
-
var RequestBodyTooLargeError = class extends Error {
|
|
1867
|
-
constructor(maxBytes) {
|
|
1868
|
-
super(`Request body exceeds ${maxBytes} bytes`);
|
|
1869
|
-
this.maxBytes = maxBytes;
|
|
1870
|
-
this.name = "RequestBodyTooLargeError";
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
async function readBoundedRequestText(request, maxBytes) {
|
|
1874
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
1875
|
-
throw new TypeError("maxBytes must be a positive safe integer");
|
|
1876
|
-
}
|
|
1877
|
-
const declared = request.headers.get("content-length");
|
|
1878
|
-
if (declared && /^\d+$/.test(declared.trim()) && Number(declared) > maxBytes) {
|
|
1879
|
-
throw new RequestBodyTooLargeError(maxBytes);
|
|
1880
|
-
}
|
|
1881
|
-
if (!request.body) return "";
|
|
1882
|
-
const reader = request.body.getReader();
|
|
1883
|
-
const decoder = new TextDecoder();
|
|
1884
|
-
let bytes = 0;
|
|
1885
|
-
let text = "";
|
|
1886
|
-
while (true) {
|
|
1887
|
-
const chunk = await reader.read();
|
|
1888
|
-
if (chunk.done) break;
|
|
1889
|
-
bytes += chunk.value.byteLength;
|
|
1890
|
-
if (bytes > maxBytes) {
|
|
1891
|
-
await reader.cancel().catch(() => void 0);
|
|
1892
|
-
throw new RequestBodyTooLargeError(maxBytes);
|
|
1893
|
-
}
|
|
1894
|
-
text += decoder.decode(chunk.value, { stream: true });
|
|
1895
|
-
}
|
|
1896
|
-
return text + decoder.decode();
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
// src/server/utils/docs-assistant-routes.ts
|
|
1900
|
-
var MAX_QUESTION_CHARS = 4e3;
|
|
1901
|
-
var MAX_HISTORY_MESSAGES = 12;
|
|
1902
|
-
var MAX_HISTORY_CHARS = 2e4;
|
|
1903
|
-
var MAX_REQUEST_BYTES = 128 * 1024;
|
|
1904
|
-
function registerDocsAssistantRoutes(app, resolveAuth) {
|
|
1905
|
-
app.post("/api/ai/docs", async (c) => {
|
|
1906
|
-
if (c.req.header("x-deepspace-surface") !== "docs") return c.notFound();
|
|
1907
|
-
const manifest = await loadDocsPublishedManifest(c.env, c.req.url);
|
|
1908
|
-
if (!manifest || manifest.assistant.access === "disabled") return c.notFound();
|
|
1909
|
-
let authToken;
|
|
1910
|
-
let publicClientKey;
|
|
1911
|
-
if (manifest.assistant.access === "authenticated") {
|
|
1912
|
-
const auth = await resolveAuth(c.req.raw, c.env);
|
|
1913
|
-
if (!auth) return c.json({ error: "Sign in required" }, 401);
|
|
1914
|
-
const header = c.req.header("Authorization") ?? "";
|
|
1915
|
-
if (!header.startsWith("Bearer ")) return c.json({ error: "Sign in required" }, 401);
|
|
1916
|
-
authToken = header.slice(7);
|
|
1917
|
-
} else {
|
|
1918
|
-
if (!c.env.APP_OWNER_JWT) {
|
|
1919
|
-
return c.json({ error: "Public documentation assistant billing is not configured" }, 503);
|
|
1920
|
-
}
|
|
1921
|
-
publicClientKey = await docsPublicClientKey(c.req.header("CF-Connecting-IP") ?? "unknown");
|
|
1922
|
-
}
|
|
1923
|
-
let body = null;
|
|
1924
|
-
try {
|
|
1925
|
-
const raw = await readBoundedRequestText(c.req.raw, MAX_REQUEST_BYTES);
|
|
1926
|
-
const parsed = JSON.parse(raw);
|
|
1927
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1928
|
-
body = parsed;
|
|
1929
|
-
}
|
|
1930
|
-
} catch (error) {
|
|
1931
|
-
if (error instanceof RequestBodyTooLargeError) {
|
|
1932
|
-
return c.json({ error: `request exceeds ${MAX_REQUEST_BYTES} bytes` }, 413);
|
|
1933
|
-
}
|
|
1934
|
-
return c.json({ error: "request body must be valid JSON" }, 400);
|
|
1935
|
-
}
|
|
1936
|
-
const question = typeof body?.question === "string" ? body.question.trim() : "";
|
|
1937
|
-
if (!question) return c.json({ error: "question is required" }, 400);
|
|
1938
|
-
if (question.length > MAX_QUESTION_CHARS) {
|
|
1939
|
-
return c.json({ error: `question exceeds ${MAX_QUESTION_CHARS} characters` }, 413);
|
|
1940
|
-
}
|
|
1941
|
-
const route = typeof body?.route === "string" && body.route.startsWith("/") ? body.route.slice(0, 500) : void 0;
|
|
1942
|
-
let history;
|
|
1943
|
-
try {
|
|
1944
|
-
history = normalizeDocsAssistantHistory(body?.messages);
|
|
1945
|
-
} catch (error) {
|
|
1946
|
-
return c.json({ error: error instanceof Error ? error.message : "messages are invalid" }, 400);
|
|
1947
|
-
}
|
|
1948
|
-
const corpusResult = await resolveDocsPublishedCorpus(c.env, manifest.sourceHash, c.req.url);
|
|
1949
|
-
if (!corpusResult.ok) {
|
|
1950
|
-
console.error("[docs-assistant] corpus unavailable:", corpusResult.error);
|
|
1951
|
-
return c.json({ error: "Documentation corpus is unavailable" }, 503);
|
|
1952
|
-
}
|
|
1953
|
-
const corpus = corpusResult.chunks;
|
|
1954
|
-
const selectedModel = resolveDeepSpaceAgentModel(
|
|
1955
|
-
manifest.assistant.model,
|
|
1956
|
-
"documentation"
|
|
1957
|
-
);
|
|
1958
|
-
if (!selectedModel) {
|
|
1959
|
-
console.error("[docs-assistant] unsupported configured model:", manifest.assistant.model);
|
|
1960
|
-
return c.json({ error: "Documentation assistant model is not supported" }, 503);
|
|
1961
|
-
}
|
|
1962
|
-
const diagnosticContext = {
|
|
1963
|
-
profile: "documentation",
|
|
1964
|
-
provider: selectedModel.provider,
|
|
1965
|
-
modelId: selectedModel.modelId
|
|
1966
|
-
};
|
|
1967
|
-
if (publicClientKey && !c.env.DOCS_ASSISTANT_LIMITER) {
|
|
1968
|
-
return c.json({ error: "Documentation assistant rate limiting is not configured" }, 503);
|
|
1969
|
-
}
|
|
1970
|
-
const leaseResult = publicClientKey ? await acquireDocsPublicLease(c.env, publicClientKey, "docs-assistant") : void 0;
|
|
1971
|
-
if (leaseResult && !leaseResult.ok) {
|
|
1972
|
-
return c.json(
|
|
1973
|
-
{
|
|
1974
|
-
error: leaseResult.status === 429 ? "Documentation assistant rate limit exceeded. Try again shortly." : "Documentation assistant is temporarily unavailable"
|
|
1975
|
-
},
|
|
1976
|
-
leaseResult.status,
|
|
1977
|
-
leaseResult.status === 429 ? { "Retry-After": "60" } : void 0
|
|
1978
|
-
);
|
|
1979
|
-
}
|
|
1980
|
-
const lease = leaseResult?.ok ? leaseResult.lease : void 0;
|
|
1981
|
-
let released = false;
|
|
1982
|
-
const releaseLease = async () => {
|
|
1983
|
-
if (!lease || released) return;
|
|
1984
|
-
released = true;
|
|
1985
|
-
await releaseDocsPublicLease(lease, "docs-assistant");
|
|
1986
|
-
};
|
|
1987
|
-
let result;
|
|
1988
|
-
try {
|
|
1989
|
-
result = streamDeepSpaceAgent(c.env, {
|
|
1990
|
-
profile: "documentation",
|
|
1991
|
-
modelId: selectedModel.modelId,
|
|
1992
|
-
...authToken ? { authToken } : {},
|
|
1993
|
-
system: buildDocsAssistantPrompt(manifest.name || c.env.APP_NAME, route),
|
|
1994
|
-
messages: [...history, { role: "user", content: question }],
|
|
1995
|
-
tools: buildDocsAssistantTools(corpus),
|
|
1996
|
-
abortSignal: c.req.raw.signal,
|
|
1997
|
-
onAbort: () => releaseLease(),
|
|
1998
|
-
onFinish: releaseLease,
|
|
1999
|
-
onError: ({ error }) => {
|
|
2000
|
-
console.error(
|
|
2001
|
-
`[docs-assistant] stream error: ${deepSpaceAgentErrorSummary(error, diagnosticContext)}`
|
|
2002
|
-
);
|
|
2003
|
-
void releaseLease();
|
|
2004
|
-
}
|
|
2005
|
-
}).result;
|
|
2006
|
-
} catch (error) {
|
|
2007
|
-
console.error(
|
|
2008
|
-
`[docs-assistant] agent unavailable: ${deepSpaceAgentErrorSummary(error, diagnosticContext)}`
|
|
2009
|
-
);
|
|
2010
|
-
await releaseLease();
|
|
2011
|
-
return c.json({ error: "Documentation assistant is not configured" }, 503);
|
|
2012
|
-
}
|
|
2013
|
-
return result.toUIMessageStreamResponse({
|
|
2014
|
-
sendReasoning: false,
|
|
2015
|
-
onError: (error) => {
|
|
2016
|
-
console.error(
|
|
2017
|
-
`[docs-assistant] response error: ${deepSpaceAgentErrorSummary(error, diagnosticContext)}`
|
|
2018
|
-
);
|
|
2019
|
-
void releaseLease();
|
|
2020
|
-
return "The documentation assistant could not complete this response.";
|
|
2021
|
-
}
|
|
2022
|
-
});
|
|
2023
|
-
});
|
|
2024
|
-
}
|
|
2025
|
-
var docsAssistantErrorSummary = deepSpaceAgentErrorSummary;
|
|
2026
|
-
function normalizeDocsAssistantHistory(value) {
|
|
2027
|
-
if (value === void 0) return [];
|
|
2028
|
-
if (!Array.isArray(value)) throw new TypeError("messages must be an array");
|
|
2029
|
-
if (value.length > MAX_HISTORY_MESSAGES) {
|
|
2030
|
-
throw new TypeError(`messages exceeds ${MAX_HISTORY_MESSAGES} turns`);
|
|
2031
|
-
}
|
|
2032
|
-
const messages = [];
|
|
2033
|
-
let totalChars2 = 0;
|
|
2034
|
-
for (const [index, item] of value.entries()) {
|
|
2035
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
2036
|
-
throw new TypeError(`messages[${index}] must be an object`);
|
|
2037
|
-
}
|
|
2038
|
-
const candidate = item;
|
|
2039
|
-
const expectedRole = index % 2 === 0 ? "user" : "assistant";
|
|
2040
|
-
if (candidate.role !== expectedRole) {
|
|
2041
|
-
throw new TypeError(`messages[${index}].role must be ${expectedRole}`);
|
|
2042
|
-
}
|
|
2043
|
-
const content = typeof candidate.content === "string" ? candidate.content.trim() : "";
|
|
2044
|
-
if (!content) throw new TypeError(`messages[${index}].content is required`);
|
|
2045
|
-
if (content.length > MAX_QUESTION_CHARS) {
|
|
2046
|
-
throw new TypeError(`messages[${index}].content exceeds ${MAX_QUESTION_CHARS} characters`);
|
|
2047
|
-
}
|
|
2048
|
-
totalChars2 += content.length;
|
|
2049
|
-
if (totalChars2 > MAX_HISTORY_CHARS) {
|
|
2050
|
-
throw new TypeError(`messages exceeds ${MAX_HISTORY_CHARS} characters`);
|
|
2051
|
-
}
|
|
2052
|
-
messages.push({ role: expectedRole, content });
|
|
2053
|
-
}
|
|
2054
|
-
if (messages.length % 2 !== 0) {
|
|
2055
|
-
throw new TypeError("messages must end with a completed assistant response");
|
|
2056
|
-
}
|
|
2057
|
-
return messages;
|
|
2058
|
-
}
|
|
2059
|
-
function registerDocsStaticRoutes(app) {
|
|
2060
|
-
app.get("/_docs/*", async (c) => {
|
|
2061
|
-
const pathname = new URL(c.req.url).pathname;
|
|
2062
|
-
let response = await c.env.ASSETS.fetch(c.req.raw);
|
|
2063
|
-
let status = response.status;
|
|
2064
|
-
if (status === 404) {
|
|
2065
|
-
const url = new URL(c.req.url);
|
|
2066
|
-
url.pathname = "/_docs/404.html";
|
|
2067
|
-
response = await c.env.ASSETS.fetch(new Request(url.toString(), c.req.raw));
|
|
2068
|
-
status = 404;
|
|
2069
|
-
}
|
|
2070
|
-
return secureDocsResponse(response, status, pathname);
|
|
2071
|
-
});
|
|
2072
|
-
}
|
|
2073
|
-
function secureDocsResponse(response, status, pathname) {
|
|
2074
|
-
const headers = new Headers(response.headers);
|
|
2075
|
-
if (status !== 404 && pathname.endsWith(".md")) {
|
|
2076
|
-
headers.set("Content-Type", "text/markdown; charset=utf-8");
|
|
2077
|
-
}
|
|
2078
|
-
if (headers.get("Content-Type")?.toLowerCase().includes("text/html")) {
|
|
2079
|
-
const cacheControl = headers.get("Cache-Control") ?? "public, max-age=0, must-revalidate";
|
|
2080
|
-
if (!/(?:^|,)\s*no-transform(?:\s*,|$)/i.test(cacheControl)) {
|
|
2081
|
-
headers.set("Cache-Control", `${cacheControl}, no-transform`);
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
headers.set("X-Content-Type-Options", "nosniff");
|
|
2085
|
-
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
2086
|
-
headers.set(
|
|
2087
|
-
"Content-Security-Policy",
|
|
2088
|
-
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'"
|
|
2089
|
-
);
|
|
2090
|
-
return new Response(response.body, { status, headers });
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
// src/server/utils/docs-mcp/protocol.ts
|
|
2094
|
-
var MCP_SERVER_VERSION = "1.0.0";
|
|
2095
|
-
var CURRENT_PROTOCOL_VERSION = "2025-11-25";
|
|
2096
|
-
var SUPPORTED_PROTOCOL_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PROTOCOL_VERSION, "2025-06-18", "2025-03-26"]);
|
|
2097
|
-
function initializeResponse(request) {
|
|
2098
|
-
const params = objectValue(request.params);
|
|
2099
|
-
const requested = typeof params?.protocolVersion === "string" ? params.protocolVersion : CURRENT_PROTOCOL_VERSION;
|
|
2100
|
-
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(requested) ? requested : CURRENT_PROTOCOL_VERSION;
|
|
2101
|
-
return mcpResult(request.id, {
|
|
2102
|
-
protocolVersion,
|
|
2103
|
-
capabilities: {
|
|
2104
|
-
tools: { listChanged: false },
|
|
2105
|
-
resources: { subscribe: false, listChanged: false }
|
|
2106
|
-
},
|
|
2107
|
-
serverInfo: {
|
|
2108
|
-
name: "DeepSpace Docs",
|
|
2109
|
-
title: "DeepSpace documentation",
|
|
2110
|
-
version: MCP_SERVER_VERSION
|
|
2111
|
-
},
|
|
2112
|
-
instructions: "Search the published docs before answering, then read exact pages when full context is needed."
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
function hasValidOrigin(request) {
|
|
2116
|
-
const origin = request.headers.get("Origin");
|
|
2117
|
-
if (!origin) return true;
|
|
2118
|
-
try {
|
|
2119
|
-
return new URL(origin).origin === new URL(request.url).origin;
|
|
2120
|
-
} catch {
|
|
2121
|
-
return false;
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
function parseRequest(raw) {
|
|
2125
|
-
let value;
|
|
2126
|
-
try {
|
|
2127
|
-
value = JSON.parse(raw);
|
|
2128
|
-
} catch {
|
|
2129
|
-
return { ok: false, message: "Invalid JSON" };
|
|
2130
|
-
}
|
|
2131
|
-
const candidate = objectValue(value);
|
|
2132
|
-
if (!candidate || candidate.jsonrpc !== "2.0" || typeof candidate.method !== "string") {
|
|
2133
|
-
return { ok: false, message: "Invalid JSON-RPC request" };
|
|
2134
|
-
}
|
|
2135
|
-
if (candidate.id !== void 0 && candidate.id !== null && typeof candidate.id !== "string" && typeof candidate.id !== "number")
|
|
2136
|
-
return { ok: false, message: "Invalid JSON-RPC id" };
|
|
2137
|
-
return {
|
|
2138
|
-
ok: true,
|
|
2139
|
-
request: {
|
|
2140
|
-
jsonrpc: "2.0",
|
|
2141
|
-
method: candidate.method,
|
|
2142
|
-
...candidate.id !== void 0 ? { id: candidate.id } : {},
|
|
2143
|
-
...candidate.params !== void 0 ? { params: candidate.params } : {}
|
|
2144
|
-
}
|
|
2145
|
-
};
|
|
2146
|
-
}
|
|
2147
|
-
function objectValue(value) {
|
|
2148
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2149
|
-
}
|
|
2150
|
-
function toolResult(id, value) {
|
|
2151
|
-
return mcpResult(id, {
|
|
2152
|
-
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
2153
|
-
structuredContent: value,
|
|
2154
|
-
isError: false
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2157
|
-
function toolError(id, message) {
|
|
2158
|
-
return mcpResult(id, {
|
|
2159
|
-
content: [{ type: "text", text: message }],
|
|
2160
|
-
isError: true
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2163
|
-
function mcpResult(id, result) {
|
|
2164
|
-
return jsonResponse({ jsonrpc: "2.0", id, result });
|
|
2165
|
-
}
|
|
2166
|
-
function mcpError(id, code, message, status = 200, headers) {
|
|
2167
|
-
return jsonResponse({ jsonrpc: "2.0", id, error: { code, message } }, status, headers);
|
|
2168
|
-
}
|
|
2169
|
-
function jsonResponse(value, status = 200, headers) {
|
|
2170
|
-
return new Response(JSON.stringify(value), {
|
|
2171
|
-
status,
|
|
2172
|
-
headers: {
|
|
2173
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
2174
|
-
...mcpSecurityHeaders(),
|
|
2175
|
-
...headers
|
|
2176
|
-
}
|
|
2177
|
-
});
|
|
2178
|
-
}
|
|
2179
|
-
function mcpSecurityHeaders() {
|
|
2180
|
-
return {
|
|
2181
|
-
"Cache-Control": "no-store",
|
|
2182
|
-
"X-Content-Type-Options": "nosniff",
|
|
2183
|
-
"Referrer-Policy": "no-referrer"
|
|
2184
|
-
};
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
// src/server/utils/docs-mcp/tools.ts
|
|
2188
|
-
var DOCS_TOOLS = [
|
|
2189
|
-
{
|
|
2190
|
-
name: "docs_search",
|
|
2191
|
-
title: "Search documentation",
|
|
2192
|
-
description: "Search the published documentation and return ranked, linked excerpts.",
|
|
2193
|
-
inputSchema: {
|
|
2194
|
-
type: "object",
|
|
2195
|
-
properties: {
|
|
2196
|
-
query: { type: "string", minLength: 2, maxLength: 300 },
|
|
2197
|
-
limit: { type: "integer", minimum: 1, maximum: 10, default: 6 }
|
|
2198
|
-
},
|
|
2199
|
-
required: ["query"],
|
|
2200
|
-
additionalProperties: false
|
|
2201
|
-
},
|
|
2202
|
-
outputSchema: {
|
|
2203
|
-
type: "object",
|
|
2204
|
-
properties: { results: { type: "array" } },
|
|
2205
|
-
required: ["results"]
|
|
2206
|
-
},
|
|
2207
|
-
annotations: {
|
|
2208
|
-
readOnlyHint: true,
|
|
2209
|
-
destructiveHint: false,
|
|
2210
|
-
idempotentHint: true,
|
|
2211
|
-
openWorldHint: false
|
|
2212
|
-
}
|
|
2213
|
-
},
|
|
2214
|
-
{
|
|
2215
|
-
name: "docs_read",
|
|
2216
|
-
title: "Read documentation page",
|
|
2217
|
-
description: "Read one complete published documentation page as Markdown.",
|
|
2218
|
-
inputSchema: {
|
|
2219
|
-
type: "object",
|
|
2220
|
-
properties: {
|
|
2221
|
-
route: { type: "string", minLength: 1, maxLength: 500, pattern: "^/" }
|
|
2222
|
-
},
|
|
2223
|
-
required: ["route"],
|
|
2224
|
-
additionalProperties: false
|
|
2225
|
-
},
|
|
2226
|
-
outputSchema: {
|
|
2227
|
-
type: "object",
|
|
2228
|
-
properties: {
|
|
2229
|
-
route: { type: "string" },
|
|
2230
|
-
title: { type: "string" },
|
|
2231
|
-
url: { type: "string" },
|
|
2232
|
-
markdown: { type: "string" }
|
|
2233
|
-
},
|
|
2234
|
-
required: ["route", "title", "url", "markdown"]
|
|
2235
|
-
},
|
|
2236
|
-
annotations: {
|
|
2237
|
-
readOnlyHint: true,
|
|
2238
|
-
destructiveHint: false,
|
|
2239
|
-
idempotentHint: true,
|
|
2240
|
-
openWorldHint: false
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
];
|
|
2244
|
-
async function callDocsTool(rawRequest, request, corpus, env) {
|
|
2245
|
-
const params = objectValue(request.params);
|
|
2246
|
-
const name = typeof params?.name === "string" ? params.name : "";
|
|
2247
|
-
const args = objectValue(params?.arguments) ?? {};
|
|
2248
|
-
const origin = new URL(rawRequest.url).origin;
|
|
2249
|
-
if (name === "docs_search") {
|
|
2250
|
-
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
2251
|
-
const limit = args.limit === void 0 ? 6 : args.limit;
|
|
2252
|
-
if (query.length < 2 || query.length > 300 || !Number.isInteger(limit) || Number(limit) < 1 || Number(limit) > 10) {
|
|
2253
|
-
return mcpError(request.id, -32602, "Invalid docs_search arguments");
|
|
2254
|
-
}
|
|
2255
|
-
const value = {
|
|
2256
|
-
results: searchDocsCorpus(corpus, query, Number(limit)).map((result) => ({
|
|
2257
|
-
...result,
|
|
2258
|
-
url: new URL(result.route, origin).toString()
|
|
2259
|
-
}))
|
|
2260
|
-
};
|
|
2261
|
-
return toolResult(request.id, value);
|
|
2262
|
-
}
|
|
2263
|
-
if (name === "docs_read") {
|
|
2264
|
-
const route = typeof args.route === "string" ? args.route.trim() : "";
|
|
2265
|
-
if (!route.startsWith("/") || route.length > 500) {
|
|
2266
|
-
return mcpError(request.id, -32602, "Invalid docs_read arguments");
|
|
2267
|
-
}
|
|
2268
|
-
try {
|
|
2269
|
-
const page = await readDocsPublishedPage(env, corpus, route, rawRequest.url);
|
|
2270
|
-
if (!page) return toolError(request.id, `No published documentation page exists at ${route}`);
|
|
2271
|
-
return toolResult(request.id, {
|
|
2272
|
-
...page,
|
|
2273
|
-
url: new URL(page.route, origin).toString()
|
|
2274
|
-
});
|
|
2275
|
-
} catch (error) {
|
|
2276
|
-
console.error("[docs-mcp] page unavailable:", error);
|
|
2277
|
-
return toolError(request.id, "The documentation page could not be read");
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
return mcpError(request.id, -32602, `Unknown documentation tool: ${name || "<missing>"}`);
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
// src/server/utils/docs-mcp-routes.ts
|
|
2284
|
-
var MAX_REQUEST_CHARS = 64e3;
|
|
2285
|
-
var MAX_REQUEST_BYTES2 = MAX_REQUEST_CHARS * 4;
|
|
2286
|
-
var MAX_SKILL_CHARS = 256e3;
|
|
2287
|
-
function registerDocsMcpRoutes(app) {
|
|
2288
|
-
app.get("/mcp", async (c) => {
|
|
2289
|
-
if (!await publicManifest(c.req.raw, c.env)) return c.notFound();
|
|
2290
|
-
return new Response(null, {
|
|
2291
|
-
status: 405,
|
|
2292
|
-
headers: { Allow: "POST", ...mcpSecurityHeaders() }
|
|
2293
|
-
});
|
|
2294
|
-
});
|
|
2295
|
-
app.post("/mcp", async (c) => {
|
|
2296
|
-
const manifest = await publicManifest(c.req.raw, c.env);
|
|
2297
|
-
if (!manifest) return c.notFound();
|
|
2298
|
-
if (!hasValidOrigin(c.req.raw)) {
|
|
2299
|
-
return mcpError(null, -32e3, "Invalid Origin", 403);
|
|
2300
|
-
}
|
|
2301
|
-
let raw;
|
|
2302
|
-
try {
|
|
2303
|
-
raw = await readBoundedRequestText(c.req.raw, MAX_REQUEST_BYTES2);
|
|
2304
|
-
} catch (error) {
|
|
2305
|
-
if (error instanceof RequestBodyTooLargeError) {
|
|
2306
|
-
return mcpError(null, -32600, `Request exceeds ${MAX_REQUEST_BYTES2} bytes`, 413);
|
|
2307
|
-
}
|
|
2308
|
-
return mcpError(null, -32700, "Unable to read request body", 400);
|
|
2309
|
-
}
|
|
2310
|
-
if (raw.length > MAX_REQUEST_CHARS) {
|
|
2311
|
-
return mcpError(null, -32600, `Request exceeds ${MAX_REQUEST_CHARS} characters`, 413);
|
|
2312
|
-
}
|
|
2313
|
-
const parsed = parseRequest(raw);
|
|
2314
|
-
if (!parsed.ok) return mcpError(null, -32700, parsed.message, 400);
|
|
2315
|
-
const request = parsed.request;
|
|
2316
|
-
if (request.method !== "initialize") {
|
|
2317
|
-
const protocolVersion = c.req.header("MCP-Protocol-Version");
|
|
2318
|
-
if (protocolVersion && !SUPPORTED_PROTOCOL_VERSIONS.has(protocolVersion)) {
|
|
2319
|
-
return mcpError(request.id ?? null, -32600, "Unsupported MCP-Protocol-Version", 400);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
const requestId = request.id;
|
|
2323
|
-
if (requestId === void 0) return new Response(null, { status: 202 });
|
|
2324
|
-
const requestWithId = { ...request, id: requestId };
|
|
2325
|
-
if (request.method === "initialize") return initializeResponse(requestWithId);
|
|
2326
|
-
if (request.method === "ping") return mcpResult(requestId, {});
|
|
2327
|
-
if (request.method === "tools/list") {
|
|
2328
|
-
return mcpResult(requestId, { tools: DOCS_TOOLS });
|
|
2329
|
-
}
|
|
2330
|
-
if (request.method === "resources/list") {
|
|
2331
|
-
return mcpResult(requestId, { resources: [skillResource(c.req.raw, manifest)] });
|
|
2332
|
-
}
|
|
2333
|
-
if (request.method === "resources/read") {
|
|
2334
|
-
return readSkillResource(c.req.raw, requestWithId, manifest, c.env);
|
|
2335
|
-
}
|
|
2336
|
-
if (request.method !== "tools/call") {
|
|
2337
|
-
return mcpError(requestId, -32601, `Method not found: ${request.method}`);
|
|
2338
|
-
}
|
|
2339
|
-
const corpusResult = await resolveDocsPublishedCorpus(c.env, manifest.sourceHash, c.req.url);
|
|
2340
|
-
if (!corpusResult.ok) {
|
|
2341
|
-
console.error("[docs-mcp] corpus unavailable:", corpusResult.error);
|
|
2342
|
-
return mcpError(requestId, -32002, "Documentation corpus is unavailable", 503);
|
|
2343
|
-
}
|
|
2344
|
-
const corpus = corpusResult.chunks;
|
|
2345
|
-
const clientKey = await docsPublicClientKey(c.req.header("CF-Connecting-IP") ?? "unknown");
|
|
2346
|
-
const leaseResult = await acquireDocsPublicLease(c.env, clientKey, "docs-mcp");
|
|
2347
|
-
if (!leaseResult.ok) {
|
|
2348
|
-
return mcpError(
|
|
2349
|
-
requestId,
|
|
2350
|
-
leaseResult.status === 429 ? -32029 : -32002,
|
|
2351
|
-
leaseResult.status === 429 ? "Documentation tool rate limit exceeded. Try again shortly." : "Documentation tools are temporarily unavailable",
|
|
2352
|
-
leaseResult.status,
|
|
2353
|
-
leaseResult.status === 429 ? { "Retry-After": "60" } : void 0
|
|
2354
|
-
);
|
|
2355
|
-
}
|
|
2356
|
-
try {
|
|
2357
|
-
return await callDocsTool(c.req.raw, requestWithId, corpus, c.env);
|
|
2358
|
-
} finally {
|
|
2359
|
-
await releaseDocsPublicLease(leaseResult.lease, "docs-mcp");
|
|
2360
|
-
}
|
|
2361
|
-
});
|
|
2362
|
-
for (const path of [
|
|
2363
|
-
"/.well-known/mcp",
|
|
2364
|
-
"/.well-known/mcp.json",
|
|
2365
|
-
"/.well-known/mcp/server-card.json"
|
|
2366
|
-
]) {
|
|
2367
|
-
app.get(path, async (c) => {
|
|
2368
|
-
const manifest = await publicManifest(c.req.raw, c.env);
|
|
2369
|
-
if (!manifest) return c.notFound();
|
|
2370
|
-
return discoveryResponse(c.req.raw, manifest);
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2373
|
-
app.get("/.well-known/mcp/server-cards.json", async (c) => {
|
|
2374
|
-
const manifest = await publicManifest(c.req.raw, c.env);
|
|
2375
|
-
if (!manifest) return c.notFound();
|
|
2376
|
-
return jsonResponse({ servers: [serverCard(c.req.raw, manifest)] }, 200, {
|
|
2377
|
-
"Cache-Control": "public, max-age=300"
|
|
2378
|
-
});
|
|
2379
|
-
});
|
|
2380
|
-
}
|
|
2381
|
-
async function readSkillResource(rawRequest, request, manifest, env) {
|
|
2382
|
-
const params = objectValue(request.params);
|
|
2383
|
-
const requestedUri = typeof params?.uri === "string" ? params.uri : "";
|
|
2384
|
-
const resource = skillResource(rawRequest, manifest);
|
|
2385
|
-
if (requestedUri !== resource.uri) {
|
|
2386
|
-
return mcpError(request.id, -32602, "Unknown documentation resource");
|
|
2387
|
-
}
|
|
2388
|
-
const assetUrl = new URL("/_docs/skill.md", rawRequest.url);
|
|
2389
|
-
const response = await env.ASSETS.fetch(new Request(assetUrl, { method: "GET" }));
|
|
2390
|
-
if (!response.ok) return mcpError(request.id, -32002, "Documentation skill is unavailable", 503);
|
|
2391
|
-
const text = await response.text();
|
|
2392
|
-
if (text.length > MAX_SKILL_CHARS) {
|
|
2393
|
-
return mcpError(request.id, -32002, "Documentation skill exceeds the published size limit", 503);
|
|
2394
|
-
}
|
|
2395
|
-
return mcpResult(request.id, {
|
|
2396
|
-
contents: [{ uri: resource.uri, mimeType: resource.mimeType, text }]
|
|
2397
|
-
});
|
|
2398
|
-
}
|
|
2399
|
-
function skillResource(request, manifest) {
|
|
2400
|
-
return {
|
|
2401
|
-
uri: new URL("/skill.md", request.url).toString(),
|
|
2402
|
-
name: `${manifest.name} documentation skill`,
|
|
2403
|
-
title: `Use ${manifest.name}`,
|
|
2404
|
-
description: `Agent instructions grounded in the published ${manifest.name} documentation.`,
|
|
2405
|
-
mimeType: "text/markdown"
|
|
2406
|
-
};
|
|
2407
|
-
}
|
|
2408
|
-
async function publicManifest(request, env) {
|
|
2409
|
-
if (request.headers.get("x-deepspace-surface") !== "docs") return null;
|
|
2410
|
-
const manifest = await loadDocsPublishedManifest(env, request.url);
|
|
2411
|
-
return manifest?.mcp.access === "public" ? manifest : null;
|
|
2412
|
-
}
|
|
2413
|
-
function discoveryResponse(request, manifest) {
|
|
2414
|
-
return jsonResponse(serverCard(request, manifest), 200, {
|
|
2415
|
-
"Cache-Control": "public, max-age=300"
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
function serverCard(request, manifest) {
|
|
2419
|
-
return {
|
|
2420
|
-
name: manifest.name,
|
|
2421
|
-
description: `Search and read the published ${manifest.name} documentation.`,
|
|
2422
|
-
version: MCP_SERVER_VERSION,
|
|
2423
|
-
protocolVersion: CURRENT_PROTOCOL_VERSION,
|
|
2424
|
-
transport: {
|
|
2425
|
-
type: "streamable-http",
|
|
2426
|
-
url: new URL("/mcp", request.url).toString()
|
|
2427
|
-
},
|
|
2428
|
-
authentication: { required: false },
|
|
2429
|
-
capabilities: {
|
|
2430
|
-
tools: DOCS_TOOLS.map(({ name, title, description }) => ({ name, title, description })),
|
|
2431
|
-
resources: [skillResource(request, manifest)]
|
|
2432
|
-
}
|
|
2433
|
-
};
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
// src/server/utils/docs-routes.ts
|
|
2437
|
-
function registerDeepSpaceDocs(app, options) {
|
|
2438
|
-
registerDocsAssistantRoutes(app, options.resolveAuth);
|
|
2439
|
-
registerDocsMcpRoutes(app);
|
|
2440
|
-
registerDocsStaticRoutes(app);
|
|
2441
|
-
}
|
|
2442
|
-
function docsAssistantLimiterManifestEntry(className) {
|
|
2443
|
-
return {
|
|
2444
|
-
binding: "DOCS_ASSISTANT_LIMITER",
|
|
2445
|
-
className,
|
|
2446
|
-
sqlite: true
|
|
2447
|
-
};
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
1458
|
// src/server/utils/metering.ts
|
|
2451
1459
|
function meterUsage(env, kind, fields = {}) {
|
|
2452
1460
|
const ds = env.USAGE_EVENTS;
|
|
@@ -2890,16 +1898,6 @@ var MSG_YJS_SYNC = 22;
|
|
|
2890
1898
|
var MSG_YJS_AWARENESS = 23;
|
|
2891
1899
|
var ROLE_ANONYMOUS = "viewer";
|
|
2892
1900
|
|
|
2893
|
-
// src/shared/users-schema.ts
|
|
2894
|
-
var USERS_COLUMNS = [
|
|
2895
|
-
{ name: "email", storage: "text", interpretation: "plain" },
|
|
2896
|
-
{ name: "name", storage: "text", interpretation: "plain" },
|
|
2897
|
-
{ name: "imageUrl", storage: "text", interpretation: "plain" },
|
|
2898
|
-
{ name: "role", storage: "text", interpretation: "plain" },
|
|
2899
|
-
{ name: "createdAt", storage: "text", interpretation: { kind: "datetime" } },
|
|
2900
|
-
{ name: "lastSeenAt", storage: "text", interpretation: { kind: "datetime" } }
|
|
2901
|
-
];
|
|
2902
|
-
|
|
2903
1901
|
// src/server/schemas/registry.ts
|
|
2904
1902
|
function collectionTableName(name) {
|
|
2905
1903
|
return `c_${name.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
@@ -3100,6 +2098,14 @@ var SYSTEM_MANAGED_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
3100
2098
|
"createdAt",
|
|
3101
2099
|
"lastSeenAt"
|
|
3102
2100
|
]);
|
|
2101
|
+
var USERS_COLUMNS = [
|
|
2102
|
+
{ name: "email", storage: "text", interpretation: "plain" },
|
|
2103
|
+
{ name: "name", storage: "text", interpretation: "plain" },
|
|
2104
|
+
{ name: "imageUrl", storage: "text", interpretation: "plain" },
|
|
2105
|
+
{ name: "role", storage: "text", interpretation: "plain" },
|
|
2106
|
+
{ name: "createdAt", storage: "text", interpretation: { kind: "datetime" } },
|
|
2107
|
+
{ name: "lastSeenAt", storage: "text", interpretation: { kind: "datetime" } }
|
|
2108
|
+
];
|
|
3103
2109
|
var BASE_USERS_SCHEMA = {
|
|
3104
2110
|
name: "users",
|
|
3105
2111
|
columns: [...USERS_COLUMNS],
|
|
@@ -4326,11 +3332,11 @@ async function handleToolsRequest(ctx, request, path) {
|
|
|
4326
3332
|
}
|
|
4327
3333
|
if (path.startsWith("tools/describe/")) {
|
|
4328
3334
|
const toolName = path.replace("tools/describe/", "");
|
|
4329
|
-
const
|
|
4330
|
-
if (!
|
|
3335
|
+
const tool2 = BUILT_IN_TOOLS.find((t) => t.name === toolName);
|
|
3336
|
+
if (!tool2) {
|
|
4331
3337
|
return Response.json({ error: `Tool not found: ${toolName}` }, { status: 404, headers: CORS_HEADERS2 });
|
|
4332
3338
|
}
|
|
4333
|
-
return Response.json({ tool:
|
|
3339
|
+
return Response.json({ tool: tool2 }, { headers: CORS_HEADERS2 });
|
|
4334
3340
|
}
|
|
4335
3341
|
if (path.startsWith("tools/execute") && request.method === "POST") {
|
|
4336
3342
|
return handleToolExecute(ctx, request);
|
|
@@ -4347,8 +3353,8 @@ async function handleToolExecute(ctx, request) {
|
|
|
4347
3353
|
{ status: 400, headers: CORS_HEADERS2 }
|
|
4348
3354
|
);
|
|
4349
3355
|
}
|
|
4350
|
-
const { tool:
|
|
4351
|
-
if (!
|
|
3356
|
+
const { tool: tool2, params = {} } = body;
|
|
3357
|
+
if (!tool2) {
|
|
4352
3358
|
return Response.json(
|
|
4353
3359
|
{ success: false, error: 'Missing "tool" in request body' },
|
|
4354
3360
|
{ status: 400, headers: CORS_HEADERS2 }
|
|
@@ -4365,8 +3371,8 @@ async function handleToolExecute(ctx, request) {
|
|
|
4365
3371
|
userRole = user.role;
|
|
4366
3372
|
}
|
|
4367
3373
|
}
|
|
4368
|
-
if (
|
|
4369
|
-
const result2 = executeYjsTool(ctx,
|
|
3374
|
+
if (tool2.startsWith("yjs.")) {
|
|
3375
|
+
const result2 = executeYjsTool(ctx, tool2, params, userId, userRole);
|
|
4370
3376
|
const status2 = result2.success ? 200 : result2.error?.includes("not found") ? 404 : 400;
|
|
4371
3377
|
return Response.json(result2, { status: status2, headers: CORS_HEADERS2 });
|
|
4372
3378
|
}
|
|
@@ -4377,7 +3383,7 @@ async function handleToolExecute(ctx, request) {
|
|
|
4377
3383
|
getPermissionContext: ctx.getPermissionContext,
|
|
4378
3384
|
send: ctx.send
|
|
4379
3385
|
};
|
|
4380
|
-
const result = await executeTool2(recordCtx,
|
|
3386
|
+
const result = await executeTool2(recordCtx, tool2, params, userId, userRole, isAppAction);
|
|
4381
3387
|
const status = result.success ? 200 : result.error?.includes("not found") ? 404 : 400;
|
|
4382
3388
|
return Response.json(result, { status, headers: CORS_HEADERS2 });
|
|
4383
3389
|
}
|
|
@@ -7257,7 +6263,8 @@ var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
|
|
|
7257
6263
|
"queue",
|
|
7258
6264
|
"browser_rendering",
|
|
7259
6265
|
"analytics_engine",
|
|
7260
|
-
"hyperdrive"
|
|
6266
|
+
"hyperdrive",
|
|
6267
|
+
"ratelimit"
|
|
7261
6268
|
]);
|
|
7262
6269
|
var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
|
|
7263
6270
|
"ASSETS",
|
|
@@ -7265,6 +6272,7 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
|
|
|
7265
6272
|
"API_WORKER",
|
|
7266
6273
|
"DEEPSPACE_APP_ID",
|
|
7267
6274
|
"DEEPSPACE_RESOURCE_ID",
|
|
6275
|
+
"DEEPSPACE_RELEASE_OPERATION_ID",
|
|
7268
6276
|
"APP_NAME",
|
|
7269
6277
|
"OWNER_USER_ID",
|
|
7270
6278
|
"AUTH_JWT_PUBLIC_KEY",
|
|
@@ -7355,6 +6363,17 @@ function requiredFieldError(b) {
|
|
|
7355
6363
|
return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
|
|
7356
6364
|
}
|
|
7357
6365
|
return null;
|
|
6366
|
+
case "ratelimit":
|
|
6367
|
+
if (!/^\d+$/u.test(b.namespace_id) || Number(b.namespace_id) <= 0) {
|
|
6368
|
+
return `ratelimit binding "${b.name}" requires a positive integer namespace_id string`;
|
|
6369
|
+
}
|
|
6370
|
+
if (!Number.isInteger(b.simple?.limit) || b.simple.limit <= 0) {
|
|
6371
|
+
return `ratelimit binding "${b.name}" requires simple.limit (positive integer)`;
|
|
6372
|
+
}
|
|
6373
|
+
if (b.simple.period !== 10 && b.simple.period !== 60) {
|
|
6374
|
+
return `ratelimit binding "${b.name}" requires simple.period (10 or 60)`;
|
|
6375
|
+
}
|
|
6376
|
+
return null;
|
|
7358
6377
|
case "ai":
|
|
7359
6378
|
case "browser_rendering":
|
|
7360
6379
|
case "analytics_engine":
|
|
@@ -7432,6 +6451,15 @@ function bindingManifestFromOutputConfig(outputConfig) {
|
|
|
7432
6451
|
const id = pluckString(h, "id");
|
|
7433
6452
|
if (name && id) out.push({ type: "hyperdrive", name, id });
|
|
7434
6453
|
}
|
|
6454
|
+
for (const r of asObjectArray(outputConfig.ratelimits)) {
|
|
6455
|
+
const name = pluckString(r, "name");
|
|
6456
|
+
const namespace_id = pluckString(r, "namespace_id");
|
|
6457
|
+
const simple = isObject(r.simple) ? r.simple : null;
|
|
6458
|
+
const limit = pluckNumber(simple, "limit");
|
|
6459
|
+
const period = pluckNumber(simple, "period");
|
|
6460
|
+
if (!name || !namespace_id || limit === null || period !== 10 && period !== 60) continue;
|
|
6461
|
+
out.push({ type: "ratelimit", name, namespace_id, simple: { limit, period } });
|
|
6462
|
+
}
|
|
7435
6463
|
return out;
|
|
7436
6464
|
}
|
|
7437
6465
|
function isObject(v) {
|
|
@@ -8630,7 +7658,6 @@ export {
|
|
|
8630
7658
|
DIRECTORY_SCHEMAS,
|
|
8631
7659
|
DeepSpaceAgentModelError,
|
|
8632
7660
|
DeepSpaceAgentProfileError,
|
|
8633
|
-
DocsAssistantLimiter,
|
|
8634
7661
|
GLOBAL_DO_TYPES,
|
|
8635
7662
|
GLOBAL_DO_TYPE_NAMES,
|
|
8636
7663
|
GameRoom,
|
|
@@ -8657,7 +7684,6 @@ export {
|
|
|
8657
7684
|
VOTING_SCHEMAS,
|
|
8658
7685
|
WORKSPACE_SCHEMAS,
|
|
8659
7686
|
YjsRoom,
|
|
8660
|
-
acquireDocsPublicLease,
|
|
8661
7687
|
analyzePermissions,
|
|
8662
7688
|
apiWorkerFetch,
|
|
8663
7689
|
appendMessage,
|
|
@@ -8668,8 +7694,6 @@ export {
|
|
|
8668
7694
|
broadcastChange,
|
|
8669
7695
|
broadcastYjsUpdate,
|
|
8670
7696
|
buildCronContext,
|
|
8671
|
-
buildDocsAssistantPrompt,
|
|
8672
|
-
buildDocsAssistantTools,
|
|
8673
7697
|
buildTableSelect,
|
|
8674
7698
|
buildUiParts,
|
|
8675
7699
|
callRegistry,
|
|
@@ -8696,9 +7720,6 @@ export {
|
|
|
8696
7720
|
deepSpaceAgentErrorSummary,
|
|
8697
7721
|
deleteChatCascade,
|
|
8698
7722
|
deleteRecord,
|
|
8699
|
-
docsAssistantErrorSummary,
|
|
8700
|
-
docsAssistantLimiterManifestEntry,
|
|
8701
|
-
docsPublicClientKey,
|
|
8702
7723
|
enqueueJob,
|
|
8703
7724
|
executeQuery,
|
|
8704
7725
|
getAllUsers,
|
|
@@ -8729,8 +7750,6 @@ export {
|
|
|
8729
7750
|
isTestAccountEmail,
|
|
8730
7751
|
isTestAccountTier,
|
|
8731
7752
|
lintSchema,
|
|
8732
|
-
loadDocsPublishedCorpus,
|
|
8733
|
-
loadDocsPublishedManifest,
|
|
8734
7753
|
loadMessages,
|
|
8735
7754
|
makeDefaultSummarizer,
|
|
8736
7755
|
matchOrigin,
|
|
@@ -8738,36 +7757,24 @@ export {
|
|
|
8738
7757
|
meterUsage,
|
|
8739
7758
|
meterVectorize,
|
|
8740
7759
|
noopPermissionContext,
|
|
8741
|
-
normalizeDocsAssistantHistory,
|
|
8742
|
-
normalizeDocsRoute,
|
|
8743
|
-
parseDocsCorpus,
|
|
8744
7760
|
parseQuotaLimit,
|
|
8745
7761
|
platformCorsPatterns,
|
|
8746
7762
|
platformWorkerFetch,
|
|
8747
7763
|
prepareMessagesWithCompaction,
|
|
8748
7764
|
priceBindingUsageEvent,
|
|
8749
7765
|
putRecord,
|
|
8750
|
-
readDocsPublishedPage,
|
|
8751
|
-
readDocsRoute,
|
|
8752
7766
|
readRecord,
|
|
8753
7767
|
recordMatchesSubscription,
|
|
8754
7768
|
refundInvoice,
|
|
8755
|
-
registerDeepSpaceDocs,
|
|
8756
|
-
registerDocsAssistantRoutes,
|
|
8757
|
-
registerDocsMcpRoutes,
|
|
8758
|
-
registerDocsStaticRoutes,
|
|
8759
7769
|
registerUser,
|
|
8760
7770
|
registryErrorJson,
|
|
8761
7771
|
registryStub,
|
|
8762
|
-
releaseDocsPublicLease,
|
|
8763
7772
|
requireSubscription,
|
|
8764
7773
|
resolveAppName,
|
|
8765
7774
|
resolveColumn,
|
|
8766
|
-
resolveDocsPublishedCorpus,
|
|
8767
7775
|
rowToData,
|
|
8768
7776
|
runMigrations,
|
|
8769
7777
|
saveYjsDoc,
|
|
8770
|
-
searchDocsCorpus,
|
|
8771
7778
|
streamDeepSpaceAgent,
|
|
8772
7779
|
timingSafeEqualHex,
|
|
8773
7780
|
timingSafeEqualStrings,
|