@silicajs/assistant 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/next.js +1 -1
- package/dist/next.js.map +1 -1
- package/package.json +6 -6
package/dist/next.js
CHANGED
|
@@ -119,7 +119,7 @@ function pruneExpiredBuckets(now) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
function loadSiteContext(config, requestContext) {
|
|
122
|
-
const contentRoot = path.join(getProjectRoot(), "
|
|
122
|
+
const contentRoot = path.join(getProjectRoot(), "data/content");
|
|
123
123
|
const homePage = loadHomePageContext(contentRoot);
|
|
124
124
|
const currentPage = loadCurrentPageContext(
|
|
125
125
|
contentRoot,
|
package/dist/next.js.map
CHANGED
|
@@ -1 +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":[]}
|
|
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(), \"data/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":"AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,OAGK;AAGP,MAAM,uBAAuB;AAC7B,MAAM,kCAAkC;AACxC,MAAM,+BAA+B;AACrC,MAAM,gCAAgC,CAAC,iBAAiB;AACxD,MAAM,yBAAyB;AAC/B,MAAM,8BAA8B;AAEpC,MAAM,mBAAmB,oBAAI,IAAgD;AAgDtE,SAAS,4BACd,UAAiC,CAAC,GACO;AACzC,SAAO,uBAAuB;AAAA,IAC5B,kBAAkB,OAAO,YAAY;AACnC,YAAM,qBACJ,QAAQ,cAAc,SAClB,QAAQ,YACR,UAAU,EAAE,WAAW;AAC7B,UAAI,uBAAuB,MAAO,QAAO;AACzC,aAAO,qBAAqB,kBAAkB,EAAE,OAAO;AAAA,IACzD;AAAA,IACA,SAAS,OAAO,mBAA8C;AAC5D,YAAM,SAAS,UAAU;AACzB,YAAM,YAAY,OAAO;AACzB,UAAI,CAAC,WAAW;AACd,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,YAAM,0BAA0B,QAAQ,IAAI,oBAAoB;AAChE,UAAI,CAAC,yBAAyB;AAC5B,cAAM,IAAI;AAAA,UACR,+CAA+C,oBAAoB;AAAA,QACrE;AAAA,MACF;AACA,YAAM,kBACJ,QAAQ,oBACP,CAAC,iBAAyC;AACzC,YAAI,CAAC,QAAQ,gBAAgB;AAC3B,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,UACL,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAEF,aAAO;AAAA,QACL,OAAO,MAAM,gBAAgB,EAAE,UAAU,CAAC;AAAA,QAC1C,MAAM,gBAAgB,QAAQ,cAAc;AAAA,QAC5C;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,qBACP,SACqD;AACrD,QAAM,cAAc,KAAK;AAAA,IACvB;AAAA,IACA,SAAS,eAAe;AAAA,EAC1B;AACA,QAAM,WAAW,KAAK;AAAA,IACpB;AAAA,IACA,SAAS,YAAY;AAAA,EACvB;AACA,QAAM,sBACJ,SAAS,uBAAuB;AAElC,SAAO,OAAO,YAAY;AACxB,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,MAAM,MAAM,aAAa,SAAS;AAAA,MACtC,KAAK,SAAS;AAAA,MACd;AAAA,IACF,CAAC;AACD,UAAM,SAAS,iBAAiB,IAAI,GAAG;AACvC,QAAI,CAAC,UAAU,OAAO,WAAW,KAAK;AACpC,uBAAiB,IAAI,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,SAAS,CAAC;AAC/D,0BAAoB,GAAG;AACvB,aAAO;AAAA,IACT;AAEA,WAAO,SAAS;AAChB,QAAI,OAAO,SAAS,YAAa,QAAO;AAExC,WAAO,SAAS;AAAA,MACd,EAAE,OAAO,yDAAyD;AAAA,MAClE;AAAA,QACE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,eAAe,OAAO,KAAK,MAAM,OAAO,UAAU,OAAO,GAAI,CAAC;AAAA,QAChE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAe,aACb,SACA,SACiB;AACjB,QAAM,YAAY,MAAM,QAAQ,MAAM,OAAO;AAC7C,MAAI,WAAW,KAAK,EAAG,QAAO,UAAU,KAAK;AAE7C,aAAW,UAAU,QAAQ,uBAAuB,CAAC,GAAG;AACtD,UAAM,QAAQ,qBAAqB,QAAQ,QAAQ,QAAQ,IAAI,MAAM,CAAC;AACtE,QAAI,MAAO,QAAO;AAAA,EACpB;AAEA,SAAO;AACT;AAEA,SAAS,qBACP,QACA,OACoB;AACpB,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,mBAAmB,OAAO,YAAY;AAC5C,QAAM,YACJ,qBAAqB,oBAAoB,MAAM,MAAM,GAAG,EAAE,CAAC,IAAI;AACjE,SAAO,WAAW,KAAK,KAAK;AAC9B;AAEA,SAAS,oBAAoB,KAAmB;AAC9C,MAAI,iBAAiB,QAAQ,uBAAwB;AACrD,aAAW,CAAC,KAAK,MAAM,KAAK,kBAAkB;AAC5C,QAAI,OAAO,WAAW,IAAK,kBAAiB,OAAO,GAAG;AAAA,EACxD;AACF;AAEA,SAAS,gBACP,QACA,gBACsB;AACtB,QAAM,cAAc,KAAK,KAAK,eAAe,GAAG,cAAc;AAC9D,QAAM,WAAW,oBAAoB,WAAW;AAChD,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EACZ;AACA,SAAO;AAAA,IACL,WAAW,OAAO;AAAA,IAClB,iBAAiB,OAAO;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA,iBAAiB,CAAC,eAAe;AAC/B,YAAM,QAAQ,oBAAoB,UAAU;AAC5C,UAAI,CAAC,MAAO,QAAO;AACnB,aAAO;AAAA,QACL,MAAM,MAAM;AAAA,QACZ,OAAO,MAAM;AAAA,QACb,MAAM,WAAW,MAAM,IAAI;AAAA,QAC3B,YAAY,MAAM;AAAA,MACpB;AAAA,IACF;AAAA,IACA,iBAAiB,CAAC,mBAAmB,WAAW;AAC9C,YAAM,eACJ,oBAAoB,iBAAiB,MACpC,WAAW,oBAAoB,SAAS,UAAU,IAAI,WACvD,QAAQ,OAAO;AACjB,UAAI,CAAC,aAAc,QAAO;AAE1B,YAAM,eAAe;AAAA,QACnB,aAAa;AAAA,QACb;AAAA,QACA,OAAO,UAAU;AAAA,QACjB,OAAO;AAAA,MACT;AACA,UAAI,CAAC,aAAc,QAAO;AAE1B,YAAM,QAAQ,QAAQ,YAAY;AAClC,UAAI,CAAC,MAAO,QAAO;AACnB,aAAO;AAAA,QACL,MAAM,MAAM;AAAA,QACZ,OAAO,MAAM;AAAA,QACb,MAAM,WAAW,MAAM,IAAI;AAAA,QAC3B,YAAY,MAAM;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,oBACP,aACkC;AAClC,QAAM,WAAW,QAAQ,OAAO;AAChC,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI;AACF,UAAM,MAAM,GAAG,aAAa,KAAK,KAAK,aAAa,SAAS,UAAU,GAAG;AAAA,MACvE,UAAU;AAAA,IACZ,CAAC;AACD,UAAM,UAAU,wBAAwB,iBAAiB,GAAG,EAAE,KAAK,CAAC;AACpE,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO;AAAA,MACL,OAAO,SAAS;AAAA,MAChB,YAAY,SAAS;AAAA,MACrB;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,uBACP,aACA,gBACA,oBACqC;AACrC,QAAM,aACJ,eAAe,qBACf,kBAAkB,eAAe,WAAW,KAC5C;AACF,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,QAAQ,oBAAoB,UAAU;AAC5C,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI;AACF,UAAM,MAAM,GAAG,aAAa,KAAK,KAAK,aAAa,MAAM,UAAU,GAAG;AAAA,MACpE,UAAU;AAAA,IACZ,CAAC;AACD,UAAM,UAAU,wBAAwB,iBAAiB,GAAG,EAAE,KAAK,CAAC;AACpE,WAAO;AAAA,MACL,OAAO,MAAM;AAAA,MACb,MAAM,MAAM;AAAA,MACZ,YAAY,MAAM;AAAA,MAClB;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,MAA8C;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,QAAQ,IAAI,GAAG;AACxB;AAEA,SAAS,iBAAiB,UAA0B;AAClD,SAAO,SAAS,QAAQ,mCAAmC,EAAE;AAC/D;AAEA,SAAS,wBAAwB,UAA0B;AACzD,QAAM,aAAa,SAAS,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACtD,MAAI,WAAW,UAAU,4BAA6B,QAAO;AAC7D,SAAO,GAAG,WAAW,MAAM,GAAG,2BAA2B,EAAE,QAAQ,CAAC;AACtE;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silicajs/assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Optional AI assistant for Silica knowledge sites — core-ai runtime, constrained markdown tools, and theme-consumable UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@core-ai/core-ai": "^0.11.0",
|
|
40
|
-
"@silicajs/components": "^0.4.
|
|
41
|
-
"@silicajs/core": "^0.
|
|
42
|
-
"@silicajs/ui": "^0.2.
|
|
40
|
+
"@silicajs/components": "^0.4.1",
|
|
41
|
+
"@silicajs/core": "^0.9.0",
|
|
42
|
+
"@silicajs/ui": "^0.2.1",
|
|
43
43
|
"just-bash": "^3.0.1",
|
|
44
|
-
"lucide-react": "^1.
|
|
44
|
+
"lucide-react": "^1.21.0",
|
|
45
45
|
"react-markdown": "^10.1.0",
|
|
46
46
|
"remark-gfm": "^4.0.1",
|
|
47
47
|
"zod": "^4.0.0"
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react-dom": "^19.2.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@silicajs/next": "^0.
|
|
54
|
+
"@silicajs/next": "^0.6.0",
|
|
55
55
|
"@types/react": "^19.2.17",
|
|
56
56
|
"@types/react-dom": "^19.2.3",
|
|
57
57
|
"react": "^19.2.6",
|