@silicajs/assistant 0.1.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 (50) hide show
  1. package/README.md +52 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/next.d.ts +48 -0
  6. package/dist/next.js +219 -0
  7. package/dist/next.js.map +1 -0
  8. package/dist/server/handler.d.ts +39 -0
  9. package/dist/server/handler.js +239 -0
  10. package/dist/server/handler.js.map +1 -0
  11. package/dist/server/index.d.ts +10 -0
  12. package/dist/server/index.js +43 -0
  13. package/dist/server/index.js.map +1 -0
  14. package/dist/server/prompt.d.ts +5 -0
  15. package/dist/server/prompt.js +48 -0
  16. package/dist/server/prompt.js.map +1 -0
  17. package/dist/server/provider.d.ts +7 -0
  18. package/dist/server/provider.js +51 -0
  19. package/dist/server/provider.js.map +1 -0
  20. package/dist/server/runtime.d.ts +26 -0
  21. package/dist/server/runtime.js +111 -0
  22. package/dist/server/runtime.js.map +1 -0
  23. package/dist/server/sources.d.ts +29 -0
  24. package/dist/server/sources.js +73 -0
  25. package/dist/server/sources.js.map +1 -0
  26. package/dist/server/tools.d.ts +14 -0
  27. package/dist/server/tools.js +45 -0
  28. package/dist/server/tools.js.map +1 -0
  29. package/dist/server/wikilinks.d.ts +28 -0
  30. package/dist/server/wikilinks.js +149 -0
  31. package/dist/server/wikilinks.js.map +1 -0
  32. package/dist/types.d.ts +83 -0
  33. package/dist/types.js +1 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/ui/index.d.ts +6 -0
  36. package/dist/ui/index.js +15 -0
  37. package/dist/ui/index.js.map +1 -0
  38. package/dist/ui/message.d.ts +11 -0
  39. package/dist/ui/message.js +114 -0
  40. package/dist/ui/message.js.map +1 -0
  41. package/dist/ui/panel.d.ts +13 -0
  42. package/dist/ui/panel.js +201 -0
  43. package/dist/ui/panel.js.map +1 -0
  44. package/dist/ui/provider.d.ts +46 -0
  45. package/dist/ui/provider.js +292 -0
  46. package/dist/ui/provider.js.map +1 -0
  47. package/dist/ui/trigger.d.ts +10 -0
  48. package/dist/ui/trigger.js +91 -0
  49. package/dist/ui/trigger.js.map +1 -0
  50. package/package.json +70 -0
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @silicajs/assistant
2
+
3
+ Optional AI assistant for [Silica](https://github.com/agdevhq/silica) knowledge sites.
4
+
5
+ The assistant answers reader questions from the site's original markdown files, cites the source pages it used, and supports multi-turn follow-ups. It is built on [core-ai](https://core-ai.dev) for provider-agnostic model access and uses an in-process, read-only shell sandbox ([just-bash](https://github.com/vercel-labs/just-bash)) for markdown exploration (`ls`, `grep`, `cat`, …).
6
+
7
+ ## Usage
8
+
9
+ Install alongside the provider package for your model:
10
+
11
+ ```bash
12
+ npm install @silicajs/assistant @core-ai/openai
13
+ ```
14
+
15
+ Enable it in `silica.config.ts`:
16
+
17
+ ```typescript
18
+ assistant: {
19
+ provider: "openai",
20
+ model: "gpt-5-mini",
21
+ },
22
+ ```
23
+
24
+ and set the provider API key (e.g. `OPENAI_API_KEY`) plus a server-only `SILICA_ASSISTANT_SECRET` in your environment. The Silica CLI generates the `/api/assistant` route and passes the assistant UI to the active theme.
25
+
26
+ For providers without a Silica preset, configure the core-ai package and factory explicitly:
27
+
28
+ ```typescript
29
+ assistant: {
30
+ provider: {
31
+ package: "@acme/core-ai-provider",
32
+ factory: "createAcme",
33
+ env: { baseURL: "ACME_BASE_URL" },
34
+ secrets: { apiKey: "ACME_API_KEY" },
35
+ options: { region: "eu" },
36
+ },
37
+ model: "acme-chat",
38
+ },
39
+ ```
40
+
41
+ Generated assistant routes include a 10 requests/minute rate limit keyed by `x-forwarded-for`. In `silica.config.ts`, set `assistant.rateLimit.trustedProxyHeaders` if your deployment proxy uses a different client-IP header, and only include headers your proxy sets or overwrites.
42
+
43
+ ## Entry points
44
+
45
+ | Export | Contents |
46
+ | ---------------------------- | ---------------------------------------------------------------------------- |
47
+ | `@silicajs/assistant` | Shared types (citations, transcript, stream events) |
48
+ | `@silicajs/assistant/ui` | Client components: `AssistantProvider`, `AssistantTrigger`, `AssistantPanel` |
49
+ | `@silicajs/assistant/server` | Framework-agnostic runtime: handler, agent loop, sandbox, citations |
50
+ | `@silicajs/assistant/next` | Route glue for the generated Silica Next.js app |
51
+
52
+ See the [Silica docs](https://github.com/agdevhq/silica) for the full feature documentation.
@@ -0,0 +1 @@
1
+ export { AssistantCitation, AssistantCitationResolver, AssistantRequest, AssistantSignedTranscriptMessage, AssistantSiteContext, AssistantStreamEvent, AssistantTranscriptMessage, AssistantUserTranscriptMessage, AssistantWikiLinkResolver } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/next.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { ChatModel } from '@core-ai/core-ai';
2
+ import { ResolvedSilicaAssistantConfig } from '@silicajs/core/runtime';
3
+ import { AssistantProviderModule } from './server/provider.js';
4
+
5
+ type CreateChatModelOptions = {
6
+ assistant: ResolvedSilicaAssistantConfig;
7
+ };
8
+ type AssistantRateLimitOptions = {
9
+ /** Maximum assistant requests allowed per client in the configured window. */
10
+ maxRequests?: number;
11
+ /** Window size in milliseconds. Defaults to one minute. */
12
+ windowMs?: number;
13
+ /**
14
+ * Headers set or overwritten by the deployment proxy and used to derive the
15
+ * caller IP for the built-in rate limit. Defaults to `x-forwarded-for`.
16
+ */
17
+ trustedProxyHeaders?: readonly string[];
18
+ /**
19
+ * Custom bucket key. Prefer this for authenticated routes where a stable
20
+ * session or user id is available.
21
+ */
22
+ key?: (request: Request) => string | Promise<string>;
23
+ };
24
+ type AssistantRouteOptions = {
25
+ /**
26
+ * Provider package module imported by the generated route. Keeping this
27
+ * import static lets Next trace and bundle optional provider packages.
28
+ */
29
+ providerModule?: AssistantProviderModule;
30
+ /**
31
+ * Instantiates the configured chat model. Defaults to resolving the factory
32
+ * from `providerModule` and the resolved Silica assistant config.
33
+ */
34
+ createChatModel?: (options: CreateChatModelOptions) => ChatModel | Promise<ChatModel>;
35
+ /**
36
+ * Basic per-client request cap for assistant routes. Pass `false` only when
37
+ * another quota guard protects this endpoint.
38
+ */
39
+ rateLimit?: AssistantRateLimitOptions | false;
40
+ };
41
+ /**
42
+ * `POST` handler for the generated `/api/assistant` route. Reads the AI
43
+ * configuration and generated runtime content from the Silica build output
44
+ * and answers with a streamed, cited response.
45
+ */
46
+ declare function createAssistantRouteHandler(options?: AssistantRouteOptions): (request: Request) => Promise<Response>;
47
+
48
+ export { type AssistantRateLimitOptions, type AssistantRouteOptions, type CreateChatModelOptions, createAssistantRouteHandler };
package/dist/next.js ADDED
@@ -0,0 +1,219 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ slugToHref
5
+ } from "@silicajs/core/runtime";
6
+ import {
7
+ getConfig,
8
+ getPage,
9
+ getPageBySourcePath,
10
+ getProjectRoot,
11
+ resolveWikiLinkFromDb
12
+ } from "@silicajs/next/server-data";
13
+ import {
14
+ AssistantUnavailableError,
15
+ createChatModelFromConfig,
16
+ createAssistantHandler
17
+ } from "./server/index.js";
18
+ const ASSISTANT_SECRET_ENV = "SILICA_ASSISTANT_SECRET";
19
+ const DEFAULT_RATE_LIMIT_MAX_REQUESTS = 10;
20
+ const DEFAULT_RATE_LIMIT_WINDOW_MS = 6e4;
21
+ const DEFAULT_TRUSTED_PROXY_HEADERS = ["x-forwarded-for"];
22
+ const MAX_RATE_LIMIT_BUCKETS = 1e4;
23
+ const MAX_HOME_PAGE_EXCERPT_CHARS = 2e3;
24
+ const rateLimitBuckets = /* @__PURE__ */ new Map();
25
+ function createAssistantRouteHandler(options = {}) {
26
+ return createAssistantHandler({
27
+ authorizeRequest: async (request) => {
28
+ const assistantRateLimit = options.rateLimit !== void 0 ? options.rateLimit : getConfig().assistant?.rateLimit;
29
+ if (assistantRateLimit === false) return void 0;
30
+ return createRateLimitGuard(assistantRateLimit)(request);
31
+ },
32
+ resolve: async (requestContext) => {
33
+ const config = getConfig();
34
+ const assistant = config.assistant;
35
+ if (!assistant) {
36
+ throw new AssistantUnavailableError(
37
+ "The AI assistant is not enabled for this site."
38
+ );
39
+ }
40
+ const transcriptSigningSecret = process.env[ASSISTANT_SECRET_ENV];
41
+ if (!transcriptSigningSecret) {
42
+ throw new AssistantUnavailableError(
43
+ `The AI assistant is not configured: set the ${ASSISTANT_SECRET_ENV} environment variable.`
44
+ );
45
+ }
46
+ const createChatModel = options.createChatModel ?? ((modelOptions) => {
47
+ if (!options.providerModule) {
48
+ throw new AssistantUnavailableError(
49
+ "The AI assistant provider module is not configured."
50
+ );
51
+ }
52
+ return createChatModelFromConfig(
53
+ modelOptions.assistant,
54
+ options.providerModule
55
+ );
56
+ });
57
+ return {
58
+ model: await createChatModel({ assistant }),
59
+ site: loadSiteContext(config, requestContext),
60
+ transcriptSigningSecret
61
+ };
62
+ }
63
+ });
64
+ }
65
+ function createRateLimitGuard(options) {
66
+ const maxRequests = Math.max(
67
+ 1,
68
+ options?.maxRequests ?? DEFAULT_RATE_LIMIT_MAX_REQUESTS
69
+ );
70
+ const windowMs = Math.max(
71
+ 1,
72
+ options?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS
73
+ );
74
+ const trustedProxyHeaders = options?.trustedProxyHeaders ?? DEFAULT_TRUSTED_PROXY_HEADERS;
75
+ return async (request) => {
76
+ const now = Date.now();
77
+ const key = await rateLimitKey(request, {
78
+ key: options?.key,
79
+ trustedProxyHeaders
80
+ });
81
+ const bucket = rateLimitBuckets.get(key);
82
+ if (!bucket || bucket.resetAt <= now) {
83
+ rateLimitBuckets.set(key, { count: 1, resetAt: now + windowMs });
84
+ pruneExpiredBuckets(now);
85
+ return void 0;
86
+ }
87
+ bucket.count += 1;
88
+ if (bucket.count <= maxRequests) return void 0;
89
+ return Response.json(
90
+ { error: "Too many assistant requests. Please try again shortly." },
91
+ {
92
+ status: 429,
93
+ headers: {
94
+ "retry-after": String(Math.ceil((bucket.resetAt - now) / 1e3))
95
+ }
96
+ }
97
+ );
98
+ };
99
+ }
100
+ async function rateLimitKey(request, options) {
101
+ const customKey = await options.key?.(request);
102
+ if (customKey?.trim()) return customKey.trim();
103
+ for (const header of options.trustedProxyHeaders ?? []) {
104
+ const value = rateLimitHeaderValue(header, request.headers.get(header));
105
+ if (value) return value;
106
+ }
107
+ return "anonymous";
108
+ }
109
+ function rateLimitHeaderValue(header, value) {
110
+ if (!value) return void 0;
111
+ const normalizedHeader = header.toLowerCase();
112
+ const candidate = normalizedHeader === "x-forwarded-for" ? value.split(",")[0] : value;
113
+ return candidate?.trim() || void 0;
114
+ }
115
+ function pruneExpiredBuckets(now) {
116
+ if (rateLimitBuckets.size <= MAX_RATE_LIMIT_BUCKETS) return;
117
+ for (const [key, bucket] of rateLimitBuckets) {
118
+ if (bucket.resetAt <= now) rateLimitBuckets.delete(key);
119
+ }
120
+ }
121
+ function loadSiteContext(config, requestContext) {
122
+ const contentRoot = path.join(getProjectRoot(), ".silica/content");
123
+ const homePage = loadHomePageContext(contentRoot);
124
+ const currentPage = loadCurrentPageContext(
125
+ contentRoot,
126
+ requestContext,
127
+ homePage?.sourcePath
128
+ );
129
+ return {
130
+ siteTitle: config.title,
131
+ siteDescription: config.description,
132
+ homePage,
133
+ currentPage,
134
+ contentRoot,
135
+ resolveCitation: (sourcePath) => {
136
+ const entry = getPageBySourcePath(sourcePath);
137
+ if (!entry) return void 0;
138
+ return {
139
+ slug: entry.slug,
140
+ title: entry.title,
141
+ href: slugToHref(entry.slug),
142
+ sourcePath: entry.sourcePath
143
+ };
144
+ },
145
+ resolveWikiLink: (currentSourcePath, target) => {
146
+ const currentEntry = getPageBySourcePath(currentSourcePath) ?? (homePage ? getPageBySourcePath(homePage.sourcePath) : void 0) ?? getPage("index");
147
+ if (!currentEntry) return void 0;
148
+ const resolvedSlug = resolveWikiLinkFromDb(
149
+ currentEntry.slug,
150
+ target,
151
+ config.wikilinks.strategy,
152
+ config.ordering
153
+ );
154
+ if (!resolvedSlug) return void 0;
155
+ const entry = getPage(resolvedSlug);
156
+ if (!entry) return void 0;
157
+ return {
158
+ slug: entry.slug,
159
+ title: entry.title,
160
+ href: slugToHref(entry.slug),
161
+ sourcePath: entry.sourcePath
162
+ };
163
+ }
164
+ };
165
+ }
166
+ function loadHomePageContext(contentRoot) {
167
+ const homePage = getPage("index");
168
+ if (!homePage) return void 0;
169
+ try {
170
+ const raw = fs.readFileSync(path.join(contentRoot, homePage.sourcePath), {
171
+ encoding: "utf8"
172
+ });
173
+ const excerpt = truncateHomePageExcerpt(stripFrontmatter(raw).trim());
174
+ if (!excerpt) return void 0;
175
+ return {
176
+ title: homePage.title,
177
+ sourcePath: homePage.sourcePath,
178
+ excerpt
179
+ };
180
+ } catch {
181
+ return void 0;
182
+ }
183
+ }
184
+ function loadCurrentPageContext(contentRoot, requestContext, fallbackSourcePath) {
185
+ const sourcePath = requestContext.currentSourcePath ?? sourcePathForSlug(requestContext.currentSlug) ?? fallbackSourcePath;
186
+ if (!sourcePath) return void 0;
187
+ const entry = getPageBySourcePath(sourcePath);
188
+ if (!entry) return void 0;
189
+ try {
190
+ const raw = fs.readFileSync(path.join(contentRoot, entry.sourcePath), {
191
+ encoding: "utf8"
192
+ });
193
+ const excerpt = truncateHomePageExcerpt(stripFrontmatter(raw).trim());
194
+ return {
195
+ title: entry.title,
196
+ slug: entry.slug,
197
+ sourcePath: entry.sourcePath,
198
+ excerpt
199
+ };
200
+ } catch {
201
+ return void 0;
202
+ }
203
+ }
204
+ function sourcePathForSlug(slug) {
205
+ if (!slug) return void 0;
206
+ return getPage(slug)?.sourcePath;
207
+ }
208
+ function stripFrontmatter(markdown) {
209
+ return markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
210
+ }
211
+ function truncateHomePageExcerpt(markdown) {
212
+ const normalized = markdown.replace(/\s+/g, " ").trim();
213
+ if (normalized.length <= MAX_HOME_PAGE_EXCERPT_CHARS) return normalized;
214
+ return `${normalized.slice(0, MAX_HOME_PAGE_EXCERPT_CHARS).trimEnd()}...`;
215
+ }
216
+ export {
217
+ createAssistantRouteHandler
218
+ };
219
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { ChatModel } from \"@core-ai/core-ai\";\nimport {\n slugToHref,\n type ResolvedSilicaAssistantConfig,\n type ResolvedSilicaConfig,\n} from \"@silicajs/core/runtime\";\nimport {\n getConfig,\n getPage,\n getPageBySourcePath,\n getProjectRoot,\n resolveWikiLinkFromDb,\n} from \"@silicajs/next/server-data\";\nimport {\n AssistantUnavailableError,\n type AssistantProviderModule,\n createChatModelFromConfig,\n createAssistantHandler,\n type AssistantRequestContext,\n type AssistantRuntime,\n} from \"./server/index.js\";\nimport type { AssistantSiteContext } from \"./types.js\";\n\nconst ASSISTANT_SECRET_ENV = \"SILICA_ASSISTANT_SECRET\";\nconst DEFAULT_RATE_LIMIT_MAX_REQUESTS = 10;\nconst DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;\nconst DEFAULT_TRUSTED_PROXY_HEADERS = [\"x-forwarded-for\"] as const;\nconst MAX_RATE_LIMIT_BUCKETS = 10_000;\nconst MAX_HOME_PAGE_EXCERPT_CHARS = 2_000;\n\nconst rateLimitBuckets = new Map<string, { count: number; resetAt: number }>();\n\nexport type CreateChatModelOptions = {\n assistant: ResolvedSilicaAssistantConfig;\n};\n\nexport type AssistantRateLimitOptions = {\n /** Maximum assistant requests allowed per client in the configured window. */\n maxRequests?: number;\n /** Window size in milliseconds. Defaults to one minute. */\n windowMs?: number;\n /**\n * Headers set or overwritten by the deployment proxy and used to derive the\n * caller IP for the built-in rate limit. Defaults to `x-forwarded-for`.\n */\n trustedProxyHeaders?: readonly string[];\n /**\n * Custom bucket key. Prefer this for authenticated routes where a stable\n * session or user id is available.\n */\n key?: (request: Request) => string | Promise<string>;\n};\n\nexport type AssistantRouteOptions = {\n /**\n * Provider package module imported by the generated route. Keeping this\n * import static lets Next trace and bundle optional provider packages.\n */\n providerModule?: AssistantProviderModule;\n /**\n * Instantiates the configured chat model. Defaults to resolving the factory\n * from `providerModule` and the resolved Silica assistant config.\n */\n createChatModel?: (\n options: CreateChatModelOptions,\n ) => ChatModel | Promise<ChatModel>;\n /**\n * Basic per-client request cap for assistant routes. Pass `false` only when\n * another quota guard protects this endpoint.\n */\n rateLimit?: AssistantRateLimitOptions | false;\n};\n\n/**\n * `POST` handler for the generated `/api/assistant` route. Reads the AI\n * configuration and generated runtime content from the Silica build output\n * and answers with a streamed, cited response.\n */\nexport function createAssistantRouteHandler(\n options: AssistantRouteOptions = {},\n): (request: Request) => Promise<Response> {\n return createAssistantHandler({\n authorizeRequest: async (request) => {\n const assistantRateLimit =\n options.rateLimit !== undefined\n ? options.rateLimit\n : getConfig().assistant?.rateLimit;\n if (assistantRateLimit === false) return undefined;\n return createRateLimitGuard(assistantRateLimit)(request);\n },\n resolve: async (requestContext): Promise<AssistantRuntime> => {\n const config = getConfig();\n const assistant = config.assistant;\n if (!assistant) {\n throw new AssistantUnavailableError(\n \"The AI assistant is not enabled for this site.\",\n );\n }\n\n const transcriptSigningSecret = process.env[ASSISTANT_SECRET_ENV];\n if (!transcriptSigningSecret) {\n throw new AssistantUnavailableError(\n `The AI assistant is not configured: set the ${ASSISTANT_SECRET_ENV} environment variable.`,\n );\n }\n const createChatModel =\n options.createChatModel ??\n ((modelOptions: CreateChatModelOptions) => {\n if (!options.providerModule) {\n throw new AssistantUnavailableError(\n \"The AI assistant provider module is not configured.\",\n );\n }\n return createChatModelFromConfig(\n modelOptions.assistant,\n options.providerModule,\n );\n });\n\n return {\n model: await createChatModel({ assistant }),\n site: loadSiteContext(config, requestContext),\n transcriptSigningSecret,\n };\n },\n });\n}\n\nfunction createRateLimitGuard(\n options: AssistantRateLimitOptions | undefined,\n): (request: Request) => Promise<Response | undefined> {\n const maxRequests = Math.max(\n 1,\n options?.maxRequests ?? DEFAULT_RATE_LIMIT_MAX_REQUESTS,\n );\n const windowMs = Math.max(\n 1,\n options?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS,\n );\n const trustedProxyHeaders =\n options?.trustedProxyHeaders ?? DEFAULT_TRUSTED_PROXY_HEADERS;\n\n return async (request) => {\n const now = Date.now();\n const key = await rateLimitKey(request, {\n key: options?.key,\n trustedProxyHeaders,\n });\n const bucket = rateLimitBuckets.get(key);\n if (!bucket || bucket.resetAt <= now) {\n rateLimitBuckets.set(key, { count: 1, resetAt: now + windowMs });\n pruneExpiredBuckets(now);\n return undefined;\n }\n\n bucket.count += 1;\n if (bucket.count <= maxRequests) return undefined;\n\n return Response.json(\n { error: \"Too many assistant requests. Please try again shortly.\" },\n {\n status: 429,\n headers: {\n \"retry-after\": String(Math.ceil((bucket.resetAt - now) / 1000)),\n },\n },\n );\n };\n}\n\nasync function rateLimitKey(\n request: Request,\n options: Pick<AssistantRateLimitOptions, \"key\" | \"trustedProxyHeaders\">,\n): Promise<string> {\n const customKey = await options.key?.(request);\n if (customKey?.trim()) return customKey.trim();\n\n for (const header of options.trustedProxyHeaders ?? []) {\n const value = rateLimitHeaderValue(header, request.headers.get(header));\n if (value) return value;\n }\n\n return \"anonymous\";\n}\n\nfunction rateLimitHeaderValue(\n header: string,\n value: string | null,\n): string | undefined {\n if (!value) return undefined;\n const normalizedHeader = header.toLowerCase();\n const candidate =\n normalizedHeader === \"x-forwarded-for\" ? value.split(\",\")[0] : value;\n return candidate?.trim() || undefined;\n}\n\nfunction pruneExpiredBuckets(now: number): void {\n if (rateLimitBuckets.size <= MAX_RATE_LIMIT_BUCKETS) return;\n for (const [key, bucket] of rateLimitBuckets) {\n if (bucket.resetAt <= now) rateLimitBuckets.delete(key);\n }\n}\n\nfunction loadSiteContext(\n config: ResolvedSilicaConfig,\n requestContext: AssistantRequestContext,\n): AssistantSiteContext {\n const contentRoot = path.join(getProjectRoot(), \".silica/content\");\n const homePage = loadHomePageContext(contentRoot);\n const currentPage = loadCurrentPageContext(\n contentRoot,\n requestContext,\n homePage?.sourcePath,\n );\n return {\n siteTitle: config.title,\n siteDescription: config.description,\n homePage,\n currentPage,\n contentRoot,\n resolveCitation: (sourcePath) => {\n const entry = getPageBySourcePath(sourcePath);\n if (!entry) return undefined;\n return {\n slug: entry.slug,\n title: entry.title,\n href: slugToHref(entry.slug),\n sourcePath: entry.sourcePath,\n };\n },\n resolveWikiLink: (currentSourcePath, target) => {\n const currentEntry =\n getPageBySourcePath(currentSourcePath) ??\n (homePage ? getPageBySourcePath(homePage.sourcePath) : undefined) ??\n getPage(\"index\");\n if (!currentEntry) return undefined;\n\n const resolvedSlug = resolveWikiLinkFromDb(\n currentEntry.slug,\n target,\n config.wikilinks.strategy,\n config.ordering,\n );\n if (!resolvedSlug) return undefined;\n\n const entry = getPage(resolvedSlug);\n if (!entry) return undefined;\n return {\n slug: entry.slug,\n title: entry.title,\n href: slugToHref(entry.slug),\n sourcePath: entry.sourcePath,\n };\n },\n };\n}\n\nfunction loadHomePageContext(\n contentRoot: string,\n): AssistantSiteContext[\"homePage\"] {\n const homePage = getPage(\"index\");\n if (!homePage) return undefined;\n\n try {\n const raw = fs.readFileSync(path.join(contentRoot, homePage.sourcePath), {\n encoding: \"utf8\",\n });\n const excerpt = truncateHomePageExcerpt(stripFrontmatter(raw).trim());\n if (!excerpt) return undefined;\n return {\n title: homePage.title,\n sourcePath: homePage.sourcePath,\n excerpt,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction loadCurrentPageContext(\n contentRoot: string,\n requestContext: AssistantRequestContext,\n fallbackSourcePath: string | undefined,\n): AssistantSiteContext[\"currentPage\"] {\n const sourcePath =\n requestContext.currentSourcePath ??\n sourcePathForSlug(requestContext.currentSlug) ??\n fallbackSourcePath;\n if (!sourcePath) return undefined;\n\n const entry = getPageBySourcePath(sourcePath);\n if (!entry) return undefined;\n\n try {\n const raw = fs.readFileSync(path.join(contentRoot, entry.sourcePath), {\n encoding: \"utf8\",\n });\n const excerpt = truncateHomePageExcerpt(stripFrontmatter(raw).trim());\n return {\n title: entry.title,\n slug: entry.slug,\n sourcePath: entry.sourcePath,\n excerpt,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction sourcePathForSlug(slug: string | undefined): string | undefined {\n if (!slug) return undefined;\n return getPage(slug)?.sourcePath;\n}\n\nfunction stripFrontmatter(markdown: string): string {\n return markdown.replace(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?/, \"\");\n}\n\nfunction truncateHomePageExcerpt(markdown: string): string {\n const normalized = markdown.replace(/\\s+/g, \" \").trim();\n if (normalized.length <= MAX_HOME_PAGE_EXCERPT_CHARS) return normalized;\n return `${normalized.slice(0, MAX_HOME_PAGE_EXCERPT_CHARS).trimEnd()}...`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,39 @@
1
+ import { AssistantSiteContext } from '../types.js';
2
+ import { RunAssistantOptions } from './runtime.js';
3
+ import '@core-ai/core-ai';
4
+ import './tools.js';
5
+
6
+ /**
7
+ * Thrown by `resolve` when the assistant cannot run (e.g. the API key
8
+ * environment variable is missing). Reported to the client as a 503 with
9
+ * the given message.
10
+ */
11
+ declare class AssistantUnavailableError extends Error {
12
+ }
13
+ type AssistantRuntime = {
14
+ model: RunAssistantOptions["model"];
15
+ site: AssistantSiteContext;
16
+ /** Server-only secret used to sign client-held assistant transcript turns. */
17
+ transcriptSigningSecret: string;
18
+ maxToolTurns?: number;
19
+ };
20
+ type AssistantRequestContext = {
21
+ currentSourcePath?: string;
22
+ currentSlug?: string;
23
+ };
24
+ type AssistantHandlerOptions = {
25
+ /**
26
+ * Optional request gate for auth, quotas, or rate limits. Return a Response to
27
+ * reject before the body is parsed or runtime dependencies are resolved.
28
+ */
29
+ authorizeRequest?: (request: Request) => Response | undefined | Promise<Response | undefined>;
30
+ /** Resolves the model and site context for the current request. */
31
+ resolve: (context: AssistantRequestContext) => AssistantRuntime | Promise<AssistantRuntime>;
32
+ };
33
+ /**
34
+ * Creates a fetch-style `POST` handler that streams newline-delimited
35
+ * JSON `AssistantStreamEvent`s.
36
+ */
37
+ declare function createAssistantHandler(options: AssistantHandlerOptions): (request: Request) => Promise<Response>;
38
+
39
+ export { type AssistantHandlerOptions, type AssistantRequestContext, type AssistantRuntime, AssistantUnavailableError, createAssistantHandler };
@@ -0,0 +1,239 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { runAssistant } from "./runtime.js";
4
+ const MAX_MESSAGES = 40;
5
+ const MAX_MESSAGE_LENGTH = 8e3;
6
+ const MAX_SIGNATURE_LENGTH = 256;
7
+ const MAX_REQUEST_BODY_BYTES = 512 * 1024;
8
+ const SIGNATURE_VERSION = "v1.";
9
+ const SIGNATURE_CONTEXT = "silica.assistant.transcript.v1\n";
10
+ const messageIdSchema = z.string().uuid();
11
+ const previousMessageIdSchema = messageIdSchema.nullable();
12
+ const messageContentSchema = z.string().min(1).max(MAX_MESSAGE_LENGTH);
13
+ const requestSourcePathSchema = z.string().min(1).max(512).transform(normalizeRequestSourcePath).pipe(z.string().min(1).refine(isSafeRequestSourcePath));
14
+ const requestSlugSchema = z.string().min(1).max(512).transform(normalizeRequestSlug).pipe(z.string().min(1).refine(isSafeRequestSlug));
15
+ const requestSchema = z.object({
16
+ messages: z.array(
17
+ z.discriminatedUnion("role", [
18
+ z.object({
19
+ id: messageIdSchema,
20
+ previousMessageId: previousMessageIdSchema,
21
+ role: z.literal("user"),
22
+ content: messageContentSchema
23
+ }),
24
+ z.object({
25
+ id: messageIdSchema,
26
+ previousMessageId: previousMessageIdSchema,
27
+ role: z.literal("assistant"),
28
+ content: messageContentSchema,
29
+ signature: z.string().min(1).max(MAX_SIGNATURE_LENGTH)
30
+ })
31
+ ])
32
+ ).min(1).max(MAX_MESSAGES),
33
+ responseMessageId: messageIdSchema,
34
+ currentSourcePath: requestSourcePathSchema.optional(),
35
+ currentSlug: requestSlugSchema.optional()
36
+ });
37
+ class AssistantUnavailableError extends Error {
38
+ }
39
+ function createAssistantHandler(options) {
40
+ return async function POST(request) {
41
+ const authorizationResponse = await options.authorizeRequest?.(request);
42
+ if (authorizationResponse) return authorizationResponse;
43
+ const requestBody = await readRequestBody(request);
44
+ if (!requestBody.success) {
45
+ return jsonError("Assistant request body is too large.", 413);
46
+ }
47
+ const parsedJson = parseJson(requestBody.body);
48
+ const parsed = requestSchema.safeParse(parsedJson);
49
+ if (!parsed.success) {
50
+ return jsonError("Invalid assistant request.", 400);
51
+ }
52
+ const transcript = parsed.data.messages;
53
+ if (transcript.at(-1)?.role !== "user") {
54
+ return jsonError("The last message must be a user message.", 400);
55
+ }
56
+ if (transcript.some((message) => message.id === parsed.data.responseMessageId)) {
57
+ return jsonError("Invalid assistant transcript.", 400);
58
+ }
59
+ let runtime;
60
+ try {
61
+ runtime = await options.resolve({
62
+ currentSourcePath: parsed.data.currentSourcePath,
63
+ currentSlug: parsed.data.currentSlug
64
+ });
65
+ } catch (error) {
66
+ if (error instanceof AssistantUnavailableError) {
67
+ return jsonError(error.message, 503);
68
+ }
69
+ throw error;
70
+ }
71
+ if (!runtime.transcriptSigningSecret) {
72
+ return jsonError(
73
+ "The AI assistant signing secret is not configured.",
74
+ 503
75
+ );
76
+ }
77
+ if (!verifyTranscript(transcript, runtime.transcriptSigningSecret)) {
78
+ return jsonError("Invalid assistant transcript.", 400);
79
+ }
80
+ const assistantMessage = {
81
+ id: parsed.data.responseMessageId,
82
+ previousMessageId: transcript.at(-1).id,
83
+ role: "assistant"
84
+ };
85
+ const encoder = new TextEncoder();
86
+ const body = new ReadableStream({
87
+ async start(controller) {
88
+ const emit = (event) => {
89
+ controller.enqueue(encoder.encode(`${JSON.stringify(event)}
90
+ `));
91
+ };
92
+ try {
93
+ const result = await runAssistant({
94
+ model: runtime.model,
95
+ site: runtime.site,
96
+ maxToolTurns: runtime.maxToolTurns,
97
+ transcript,
98
+ currentSourcePath: parsed.data.currentSourcePath,
99
+ emit,
100
+ signal: request.signal
101
+ });
102
+ if (!messageContentSchema.safeParse(result.answer).success) {
103
+ emit({
104
+ type: "error",
105
+ message: "The assistant returned an invalid answer. Please try again."
106
+ });
107
+ return;
108
+ }
109
+ emit({
110
+ type: "message-signature",
111
+ id: assistantMessage.id,
112
+ previousMessageId: assistantMessage.previousMessageId,
113
+ signature: signTranscript(
114
+ [...transcript, { ...assistantMessage, content: result.answer }],
115
+ runtime.transcriptSigningSecret
116
+ )
117
+ });
118
+ emit({ type: "done" });
119
+ } catch (error) {
120
+ if (!request.signal.aborted) {
121
+ console.error("[silica] assistant request failed:", error);
122
+ emit({
123
+ type: "error",
124
+ message: "The assistant failed to answer. Please try again."
125
+ });
126
+ }
127
+ } finally {
128
+ controller.close();
129
+ }
130
+ }
131
+ });
132
+ return new Response(body, {
133
+ headers: {
134
+ "content-type": "application/x-ndjson; charset=utf-8",
135
+ "cache-control": "no-store"
136
+ }
137
+ });
138
+ };
139
+ }
140
+ async function readRequestBody(request) {
141
+ const contentLength = Number(request.headers.get("content-length"));
142
+ if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BODY_BYTES) {
143
+ return { success: false };
144
+ }
145
+ if (!request.body) return { success: true, body: "" };
146
+ const reader = request.body.getReader();
147
+ const decoder = new TextDecoder();
148
+ let bytesRead = 0;
149
+ let body = "";
150
+ try {
151
+ for (; ; ) {
152
+ const { done, value } = await reader.read();
153
+ if (done) break;
154
+ bytesRead += value.byteLength;
155
+ if (bytesRead > MAX_REQUEST_BODY_BYTES) {
156
+ await reader.cancel().catch(() => void 0);
157
+ return { success: false };
158
+ }
159
+ body += decoder.decode(value, { stream: true });
160
+ }
161
+ body += decoder.decode();
162
+ return { success: true, body };
163
+ } catch {
164
+ return { success: true, body: "" };
165
+ }
166
+ }
167
+ function parseJson(body) {
168
+ try {
169
+ return JSON.parse(body);
170
+ } catch {
171
+ return void 0;
172
+ }
173
+ }
174
+ function jsonError(message, status) {
175
+ return Response.json({ error: message }, { status });
176
+ }
177
+ function normalizeRequestSourcePath(value) {
178
+ return value.trim().replace(/\\/g, "/").replace(/^\/+/, "");
179
+ }
180
+ function isSafeRequestSourcePath(value) {
181
+ if (!/\.(md|markdown|mdx)$/i.test(value)) return false;
182
+ return isSafeContentPath(value);
183
+ }
184
+ function normalizeRequestSlug(value) {
185
+ const normalized = value.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
186
+ return normalized || "index";
187
+ }
188
+ function isSafeRequestSlug(value) {
189
+ return isSafeContentPath(value);
190
+ }
191
+ function isSafeContentPath(value) {
192
+ if (!value || value.startsWith("~") || /^[A-Za-z]:/.test(value)) {
193
+ return false;
194
+ }
195
+ if (value.includes("//")) return false;
196
+ return value.split("/").every((segment) => segment && segment !== "." && segment !== "..");
197
+ }
198
+ function verifyTranscript(transcript, secret) {
199
+ if (!verifyTranscriptChain(transcript)) return false;
200
+ return transcript.every((message, index) => {
201
+ if (message.role !== "assistant") return true;
202
+ return verifySignature(transcript.slice(0, index + 1), message, secret);
203
+ });
204
+ }
205
+ function verifyTranscriptChain(transcript) {
206
+ const seen = /* @__PURE__ */ new Set();
207
+ return transcript.every((message, index) => {
208
+ if (seen.has(message.id)) return false;
209
+ seen.add(message.id);
210
+ const previous = transcript[index - 1];
211
+ if (message.previousMessageId !== (previous?.id ?? null)) return false;
212
+ if (index % 2 === 0 && message.role !== "user") return false;
213
+ if (index % 2 === 1 && message.role !== "assistant") return false;
214
+ return true;
215
+ });
216
+ }
217
+ function verifySignature(transcriptPrefix, message, secret) {
218
+ if (!message.signature.startsWith(SIGNATURE_VERSION)) return false;
219
+ const expected = signTranscript(transcriptPrefix, secret);
220
+ const actualSignature = Buffer.from(message.signature);
221
+ const expectedSignature = Buffer.from(expected);
222
+ return actualSignature.byteLength === expectedSignature.byteLength && timingSafeEqual(actualSignature, expectedSignature);
223
+ }
224
+ function signTranscript(transcriptPrefix, secret) {
225
+ const payload = JSON.stringify(
226
+ transcriptPrefix.map((message) => ({
227
+ id: message.id,
228
+ previousMessageId: message.previousMessageId,
229
+ role: message.role,
230
+ content: message.content
231
+ }))
232
+ );
233
+ return SIGNATURE_VERSION + createHmac("sha256", secret).update(SIGNATURE_CONTEXT).update(payload).digest("base64url");
234
+ }
235
+ export {
236
+ AssistantUnavailableError,
237
+ createAssistantHandler
238
+ };
239
+ //# sourceMappingURL=handler.js.map