@shoalrun/cli 0.1.0-alpha.16 → 0.1.0-alpha.17

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 (103) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +3 -3
  3. package/agent-docs/SNAPSHOT.json +5 -5
  4. package/agent-docs/author/frontmatter-reference.md +15 -11
  5. package/agent-docs/author/frontmatter-reference.sections/choose-a-publication-and-visibility.md +13 -11
  6. package/agent-docs/author/frontmatter-reference.sections/control-output-details.md +1 -1
  7. package/agent-docs/author/frontmatter-reference.sections/global-defaults-and-enforced-values.md +1 -1
  8. package/agent-docs/author/frontmatter-reference.sections/html-markdown-and-agents.md +1 -1
  9. package/agent-docs/author/frontmatter-reference.sections/identify-and-route-a-page.md +1 -1
  10. package/agent-docs/author/frontmatter-reference.sections/point-agents-to-it.md +1 -1
  11. package/agent-docs/author/frontmatter-reference.sections/project-defined-fields.md +1 -1
  12. package/agent-docs/author/frontmatter-reference.sections/related-reading.md +1 -1
  13. package/agent-docs/author/frontmatter-reference.sections/use-the-local-files-while-authoring.md +1 -1
  14. package/agent-docs/author/frontmatter-reference.sections/when-the-files-update.md +1 -1
  15. package/agent-docs/author/recipe-drafts.md +2 -2
  16. package/agent-docs/author/recipe-drafts.sections/1-write-one-reusable-step.md +1 -1
  17. package/agent-docs/author/recipe-drafts.sections/2-list-the-steps-for-one-complete-goal.md +1 -1
  18. package/agent-docs/author/recipe-drafts.sections/3-generate-a-draft-and-review-it.md +1 -1
  19. package/agent-docs/author/recipe-drafts.sections/4-maintain-shoalrun-s-published-recipe-snapshots.md +1 -1
  20. package/agent-docs/author/recipe-drafts.sections/related-reading.md +1 -1
  21. package/agent-docs/author/recipe-drafts.sections/start-an-ingredient-from-an-api-example.md +2 -2
  22. package/agent-docs/author/recipes.md +3 -3
  23. package/agent-docs/author/recipes.sections/how-agents-use-this.md +2 -2
  24. package/agent-docs/author/recipes.sections/one-example-from-shoalrun.md +2 -2
  25. package/agent-docs/author/recipes.sections/related-reading.md +2 -2
  26. package/agent-docs/author/recipes.sections/try-a-complete-workflow.md +3 -3
  27. package/agent-docs/author/recipes.sections/use-recipes-for-task-oriented-how-tos.md +2 -2
  28. package/agent-docs/author/recipes.sections/what-changing-an-ingredient-updates.md +2 -2
  29. package/agent-docs/author/recipes.sections/what-each-file-does.md +2 -2
  30. package/agent-docs/author/recipes.sections/what-happens-and-when.md +2 -2
  31. package/agent-docs/author/search.md +55 -6
  32. package/agent-docs/author/search.sections/agent-and-mcp-search-scope.md +48 -0
  33. package/agent-docs/author/search.sections/choose-a-provider.md +7 -6
  34. package/agent-docs/author/search.sections/cli-and-mcp-ranking-details.md +1 -1
  35. package/agent-docs/author/search.sections/enable-included-semantic-search.md +66 -0
  36. package/agent-docs/author/search.sections/hosted-semantic-search-limits.md +1 -1
  37. package/agent-docs/author/search.sections/html-markdown-and-agents.md +1 -1
  38. package/agent-docs/author/search.sections/lexical-search.md +1 -1
  39. package/agent-docs/author/search.sections/local-semantic-preview.md +1 -1
  40. package/agent-docs/author/search.sections/read-a-search-result-with-an-agent.md +1 -1
  41. package/agent-docs/author/search.sections/related-reading.md +1 -1
  42. package/agent-docs/author/search.sections/reuse-the-lexical-index.md +1 -1
  43. package/agent-docs/author/search.sections/turn-search-off.md +1 -1
  44. package/agent-docs/author/search.sections/use-the-search-box.md +1 -1
  45. package/agent-docs/author/search.sections/write-a-custom-provider.md +1 -1
  46. package/agent-docs/chat/passage-index.md +16 -12
  47. package/agent-docs/chat/passage-index.sections/build-the-index.md +3 -3
  48. package/agent-docs/chat/passage-index.sections/embed-passages.md +3 -3
  49. package/agent-docs/chat/passage-index.sections/how-the-default-chunker-splits-a-page.md +3 -3
  50. package/agent-docs/chat/passage-index.sections/limits.md +3 -3
  51. package/agent-docs/chat/passage-index.sections/passage-ids-and-links.md +14 -10
  52. package/agent-docs/chat/passage-index.sections/related-reading.md +3 -3
  53. package/agent-docs/chat/passage-index.sections/what-a-passage-contains.md +5 -5
  54. package/agent-docs/cookbooks/welcome-email.md +12 -9
  55. package/agent-docs/documents.json +207 -183
  56. package/agent-docs/provenance.json +4 -4
  57. package/agent-docs/recipes/queue-email.md +1 -1
  58. package/agent-docs/recipes/send-welcome-email.md +3 -3
  59. package/agent-docs/search-index.json +1 -1
  60. package/agent-docs/start.md +2 -2
  61. package/agent-docs/start.sections/add-the-page-to-navigation.md +1 -1
  62. package/agent-docs/start.sections/build-and-check-both-outputs.md +1 -1
  63. package/agent-docs/start.sections/build-this-site.md +1 -1
  64. package/agent-docs/start.sections/check-writing-style.md +1 -1
  65. package/agent-docs/start.sections/create-a-page.md +1 -1
  66. package/agent-docs/start.sections/make-it-yours.md +1 -1
  67. package/agent-docs/start.sections/migrating-from-github-packages.md +1 -1
  68. package/agent-docs/start.sections/prerequisites.md +1 -1
  69. package/agent-docs/start.sections/related-reading.md +1 -1
  70. package/agent-docs/start.sections/start-your-own-project.md +2 -2
  71. package/dist/ai-directory.d.ts +1 -0
  72. package/dist/ai-directory.js +12 -0
  73. package/dist/answer-cards.js +1 -1
  74. package/dist/ask-function.d.ts +4 -18
  75. package/dist/ask-function.js +12 -71
  76. package/dist/ask-runtime.d.ts +45 -0
  77. package/dist/ask-runtime.js +96 -0
  78. package/dist/ask-setup.d.ts +3 -42
  79. package/dist/ask-setup.js +5 -93
  80. package/dist/embeddings.d.ts +1 -1
  81. package/dist/embeddings.js +1 -1
  82. package/dist/function-package.js +5 -0
  83. package/dist/function-runtime.d.ts +23 -0
  84. package/dist/function-runtime.js +45 -0
  85. package/dist/index-build.d.ts +1 -2
  86. package/dist/index-build.js +3 -11
  87. package/dist/retrieval-runtime.d.ts +5 -0
  88. package/dist/retrieval-runtime.js +21 -0
  89. package/dist/search-function.d.ts +2 -0
  90. package/dist/search-function.js +30 -0
  91. package/dist/search-handler.js +2 -2
  92. package/dist/search-identifier-core.d.ts +4 -0
  93. package/dist/search-identifier-core.js +30 -0
  94. package/dist/search-identifier.d.ts +3 -3
  95. package/dist/search-identifier.js +7 -30
  96. package/dist/search-literal.js +1 -1
  97. package/dist/search-results.js +1 -1
  98. package/dist/search-snippet.d.ts +1 -1
  99. package/dist/search-snippet.js +2 -14
  100. package/dist/search-text.d.ts +1 -0
  101. package/dist/search-text.js +14 -0
  102. package/dist/vercel-function.js +10 -2
  103. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ export type FunctionRuntime = {
3
+ schemaVersion: 1;
4
+ settings: Record<string, unknown>;
5
+ variant?: {
6
+ channel: string;
7
+ version: string;
8
+ language: string;
9
+ };
10
+ hosts?: string[];
11
+ callerHeader?: string;
12
+ maxRequestsPerMinute?: number;
13
+ searchStore?: {
14
+ provider: 'upstash';
15
+ urlEnv: string;
16
+ tokenEnv: string;
17
+ };
18
+ };
19
+ export type AskFunction = (request: Request) => Promise<Response>;
20
+ export declare function allowedHosts(runtime: FunctionRuntime, env: NodeJS.ProcessEnv): string[];
21
+ export declare function nodeAdapter(load: () => Promise<AskFunction>, limits?: {
22
+ bodyBytes: number;
23
+ }): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -0,0 +1,45 @@
1
+ export function allowedHosts(runtime, env) {
2
+ return [...new Set([...(runtime.hosts ?? []), env.VERCEL_URL, env.VERCEL_BRANCH_URL, env.VERCEL_PROJECT_PRODUCTION_URL, ...(env.SHOALRUN_ASK_HOSTS?.split(',') ?? [])].map(host => host?.trim().toLowerCase()).filter((host) => Boolean(host)))];
3
+ }
4
+ const failure = (status, message) => new Response(JSON.stringify({ error: message }), { status, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' } });
5
+ // Vercel's Node runtime calls `(req, res)`. This turns that into a Web request and back. Starting up happens on the first request, and a
6
+ // failed start is tried again on the next one. The reader never sees why it failed; the reason goes to the function log.
7
+ export function nodeAdapter(load, limits = { bodyBytes: 32_768 }) {
8
+ let ready;
9
+ const write = async (res, response) => {
10
+ res.statusCode = response.status;
11
+ response.headers.forEach((value, name) => { if (name !== 'set-cookie')
12
+ res.setHeader(name, value); });
13
+ const cookies = response.headers.getSetCookie();
14
+ if (cookies.length)
15
+ res.setHeader('set-cookie', cookies);
16
+ res.end(Buffer.from(await response.arrayBuffer()));
17
+ };
18
+ return async (req, res) => {
19
+ try {
20
+ const handler = await (ready ??= load().catch(error => { ready = undefined; throw error; }));
21
+ const chunks = [];
22
+ let size = 0;
23
+ for await (const chunk of req) {
24
+ size += chunk.length;
25
+ if (size > limits.bodyBytes) {
26
+ await write(res, failure(413, 'The request is too large.'));
27
+ return;
28
+ }
29
+ chunks.push(chunk);
30
+ }
31
+ const headers = new Headers();
32
+ for (const [name, value] of Object.entries(req.headers))
33
+ for (const item of Array.isArray(value) ? value : value === undefined ? [] : [value])
34
+ headers.append(name, item);
35
+ const method = req.method ?? 'GET';
36
+ const request = new Request(`https://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, { method, headers, ...(method === 'GET' || method === 'HEAD' ? {} : { body: Buffer.concat(chunks) }) });
37
+ await write(res, await handler(request));
38
+ }
39
+ catch (error) {
40
+ console.error(`The answer function failed: ${error.message}`);
41
+ if (!res.headersSent)
42
+ await write(res, failure(503, 'The chat is unavailable right now.'));
43
+ }
44
+ };
45
+ }
@@ -1,3 +1,4 @@
1
+ export { aiDirectory } from './ai-directory.js';
1
2
  import { type PreparedSearchPassage } from './search-snippet.js';
2
3
  export type Chunk = {
3
4
  id: string;
@@ -46,10 +47,8 @@ export declare function chunkMarkdown(markdown: string, document: ChunkSource, a
46
47
  id: string;
47
48
  title: string;
48
49
  }>, options?: ChunkOptions): Chunk[];
49
- export declare function aiDirectory(directory: string): Promise<string>;
50
50
  export declare function buildChunkIndex(directory: string, options?: ChunkOptions): Promise<{
51
51
  index: ChunkIndex;
52
52
  output: string;
53
53
  documents: number;
54
54
  }>;
55
- export {};
@@ -1,5 +1,7 @@
1
+ import { aiDirectory } from './ai-directory.js';
2
+ export { aiDirectory } from './ai-directory.js';
1
3
  import { createHash } from 'node:crypto';
2
- import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
4
+ import { readFile, realpath, writeFile } from 'node:fs/promises';
3
5
  import path from 'node:path';
4
6
  import { validateSearchSynonyms } from './search-synonyms.js';
5
7
  import { prepareSearchPassage } from './search-snippet.js';
@@ -187,16 +189,6 @@ async function readJson(file) {
187
189
  }
188
190
  // The directory that holds the passage and vector files. It is created on demand, and it must stay inside the site even if
189
191
  // `ai` is a symlink.
190
- export async function aiDirectory(directory) {
191
- const root = path.resolve(directory);
192
- const target = path.join(root, 'ai');
193
- await mkdir(target, { recursive: true });
194
- const real = await realpath(root);
195
- const inside = await realpath(target);
196
- if (inside !== real && !inside.startsWith(real + path.sep))
197
- throw new Error('The ai directory leaves the site directory');
198
- return target;
199
- }
200
192
  export async function buildChunkIndex(directory, options = {}) {
201
193
  const root = path.resolve(directory);
202
194
  const manifest = await readJson(path.join(root, 'documents.json'));
@@ -0,0 +1,5 @@
1
+ import { Retriever } from './retrieval.js';
2
+ export declare function prepareSearchRetrieval(directory: string, settings: Record<string, unknown>): Promise<{
3
+ retriever: Retriever;
4
+ embedQuery: ((query: string, signal?: AbortSignal) => Promise<Float32Array>) | undefined;
5
+ }>;
@@ -0,0 +1,21 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createProvider, readEmbeddings } from './embeddings.js';
4
+ import { queryProviderConfig, validateRetrieval } from './retrieval-config.js';
5
+ import { Retriever } from './retrieval.js';
6
+ // Search needs only passage retrieval and query embeddings, never chat models,
7
+ // answer cards, source configuration, rendering, or component compilation.
8
+ export async function prepareSearchRetrieval(directory, settings) {
9
+ const retrieval = validateRetrieval(settings.retrieval);
10
+ const chunks = JSON.parse(await readFile(path.join(directory, 'ai', 'chunks.json'), 'utf8'));
11
+ const embeddings = await readEmbeddings(directory);
12
+ const retriever = new Retriever(chunks, embeddings);
13
+ let embedQuery;
14
+ if (embeddings) {
15
+ const provider = createProvider(queryProviderConfig(retrieval, embeddings.meta));
16
+ if (provider.space !== embeddings.meta.space || (embeddings.meta.backend && provider.backend !== embeddings.meta.backend))
17
+ throw new Error(`${provider.model} is not in the embedding space or backend of ${embeddings.meta.model}`);
18
+ embedQuery = async (query, signal) => (await provider.embed([query], 'query', signal))[0];
19
+ }
20
+ return { retriever, embedQuery };
21
+ }
@@ -0,0 +1,2 @@
1
+ import { type AskFunction } from './function-runtime.js';
2
+ export declare function createHostedSearchFunction(directory: string, env?: NodeJS.ProcessEnv): Promise<AskFunction>;
@@ -0,0 +1,30 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { DailyBudget, SharedRateLimit } from './budget.js';
4
+ import { envBudgetStore } from './budget-store.js';
5
+ import { validateReaderFilters } from './reader-filters.js';
6
+ import { createSearchHandler } from './search-handler.js';
7
+ import { validateSearch } from './search-config.js';
8
+ import { prepareSearchRetrieval } from './retrieval-runtime.js';
9
+ import { allowedHosts } from './function-runtime.js';
10
+ export async function createHostedSearchFunction(directory, env = process.env) {
11
+ const runtime = JSON.parse(await readFile(path.join(directory, 'runtime.json'), 'utf8'));
12
+ if (runtime?.schemaVersion !== 1 || !runtime.settings)
13
+ throw new Error('runtime.json is not a Shoalrun search function file');
14
+ const filters = validateReaderFilters(runtime.settings.readerFilters);
15
+ // Accept old function metadata while new builds omit the chat configuration.
16
+ const legacy = runtime.settings.chat;
17
+ const settings = runtime.searchStore ?? legacy?.budget?.store;
18
+ if (!settings || typeof settings !== 'object' || !('provider' in settings) || settings.provider !== 'upstash' || !('urlEnv' in settings) || typeof settings.urlEnv !== 'string' || !('tokenEnv' in settings) || typeof settings.tokenEnv !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(settings.urlEnv) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(settings.tokenEnv))
19
+ throw new Error('Hosted semantic search needs a shared budget store');
20
+ const setup = await prepareSearchRetrieval(path.join(directory, 'site'), runtime.settings);
21
+ const store = envBudgetStore({ urlEnv: settings.urlEnv, tokenEnv: settings.tokenEnv }, env);
22
+ const { limits } = validateSearch(runtime.settings.search);
23
+ return createSearchHandler({
24
+ ...setup, readerFilters: filters, hosts: allowedHosts(runtime, env),
25
+ onFallback: reason => console.warn(`Search fallback: ${reason}`),
26
+ rateLimiter: new SharedRateLimit(store, limits.perCallerPerMinute, Date.now, error => console.error(`Search rate limit: ${error.message}`), 'search-rate'),
27
+ budget: new DailyBudget(store, { perCaller: limits.perCallerPerDay, perDay: limits.perDay }, Date.now, error => console.error(`Search budget: ${error.message}`), 'search-budget'),
28
+ clientKey: request => request.headers.get(runtime.callerHeader ?? 'x-vercel-forwarded-for')?.split(',')[0].trim() || 'unknown'
29
+ });
30
+ }
@@ -1,7 +1,7 @@
1
1
  import { compareHits } from './retrieval.js';
2
2
  import { validateReaderSelection } from './reader-filters.js';
3
- import { searchSnippetFromParagraphs } from './search-snippet.js';
4
- import { identifierMatch } from './search-identifier.js';
3
+ import { searchSnippetFromParagraphs } from './search-text.js';
4
+ import { identifierMatch } from './search-identifier-core.js';
5
5
  import { createSearchResultSelector } from './search-results.js';
6
6
  export function createSearchHandler(options) {
7
7
  const selectResults = createSearchResultSelector(options.retriever);
@@ -0,0 +1,4 @@
1
+ import type { Hit } from './retrieval.js';
2
+ export declare function searchIdentifiers(query: string): string[];
3
+ export declare function identifierRescue<T extends Hit>(query: string, semantic: T[], lexical: T[], paragraphs?: (hit: Hit) => string[]): T[];
4
+ export declare function identifierMatch(query: string, paragraphs?: (hit: Hit) => string[]): ((hit: Hit) => boolean) | undefined;
@@ -0,0 +1,30 @@
1
+ // Reserve at most one search-box result for a literal configuration key or flag
2
+ // explained in prose. Tables, code listings and identifier prefixes do not qualify.
3
+ export function searchIdentifiers(query) {
4
+ const identifiers = query.match(/\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b|--[a-z][\w-]*/g) ?? [];
5
+ // File names and URLs need ordinary retrieval, not a config-key shortcut.
6
+ return [...new Set(identifiers.filter(id => !/\.(?:txt|json|ya?ml|mdx?|[cm]?js|tsx?|html?|css|svg|png|jpe?g|com|org|net)$/i.test(id)))];
7
+ }
8
+ export function identifierRescue(query, semantic, lexical, paragraphs) {
9
+ const matches = identifierMatch(query, paragraphs);
10
+ if (!matches)
11
+ return semantic;
12
+ const candidate = lexical.find(matches);
13
+ if (!candidate)
14
+ return semantic;
15
+ const pages = [...new Set(semantic.map(h => h.chunk.docId))];
16
+ if (pages.slice(0, 3).includes(candidate.chunk.docId))
17
+ return semantic;
18
+ const remaining = semantic.filter(h => h.chunk.docId !== candidate.chunk.docId);
19
+ const seen = new Set();
20
+ const at = remaining.findIndex(hit => { seen.add(hit.chunk.docId); return seen.size === 3; });
21
+ const position = at < 0 ? remaining.length : at;
22
+ return [...remaining.slice(0, position), candidate, ...remaining.slice(position)];
23
+ }
24
+ export function identifierMatch(query, paragraphs = hit => hit.chunk.search?.paragraphs ?? []) {
25
+ const identifiers = searchIdentifiers(query);
26
+ if (!identifiers.length)
27
+ return undefined;
28
+ const matches = identifiers.map(id => new RegExp(`(?<![\\w$.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w$-]|\\.[\\w$])`, 'i'));
29
+ return hit => paragraphs(hit).some(p => matches.every(re => re.test(p)));
30
+ }
@@ -1,4 +1,4 @@
1
1
  import type { Hit } from './retrieval.js';
2
- export declare function searchIdentifiers(query: string): string[];
3
- export declare function identifierRescue<T extends Hit>(query: string, semantic: T[], lexical: T[]): T[];
4
- export declare function identifierMatch(query: string): ((hit: Hit) => boolean) | undefined;
2
+ export { searchIdentifiers } from './search-identifier-core.js';
3
+ export declare const identifierMatch: (query: string) => ((hit: Hit) => boolean) | undefined;
4
+ export declare const identifierRescue: <T extends Hit>(query: string, semantic: T[], lexical: T[]) => T[];
@@ -1,31 +1,8 @@
1
1
  import { searchParagraphs } from './search-snippet.js';
2
- // Reserve at most one search-box result for a literal configuration key or flag
3
- // explained in prose. Tables, code listings and identifier prefixes do not qualify.
4
- export function searchIdentifiers(query) {
5
- const identifiers = query.match(/\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b|--[a-z][\w-]*/g) ?? [];
6
- // File names and URLs need ordinary retrieval, not a config-key shortcut.
7
- return [...new Set(identifiers.filter(id => !/\.(?:txt|json|ya?ml|mdx?|[cm]?js|tsx?|html?|css|svg|png|jpe?g|com|org|net)$/i.test(id)))];
8
- }
9
- export function identifierRescue(query, semantic, lexical) {
10
- const matches = identifierMatch(query);
11
- if (!matches)
12
- return semantic;
13
- const candidate = lexical.find(matches);
14
- if (!candidate)
15
- return semantic;
16
- const pages = [...new Set(semantic.map(h => h.chunk.docId))];
17
- if (pages.slice(0, 3).includes(candidate.chunk.docId))
18
- return semantic;
19
- const remaining = semantic.filter(h => h.chunk.docId !== candidate.chunk.docId);
20
- const seen = new Set();
21
- const at = remaining.findIndex(hit => { seen.add(hit.chunk.docId); return seen.size === 3; });
22
- const position = at < 0 ? remaining.length : at;
23
- return [...remaining.slice(0, position), candidate, ...remaining.slice(position)];
24
- }
25
- export function identifierMatch(query) {
26
- const identifiers = searchIdentifiers(query);
27
- if (!identifiers.length)
28
- return undefined;
29
- const matches = identifiers.map(id => new RegExp(`(?<![\\w$.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w$-]|\\.[\\w$])`, 'i'));
30
- return hit => (hit.chunk.search?.paragraphs ?? searchParagraphs(hit.chunk.text)).some(p => matches.every(re => re.test(p)));
31
- }
2
+ import { identifierMatch as preparedMatch, identifierRescue as preparedRescue } from './search-identifier-core.js';
3
+ export { searchIdentifiers } from './search-identifier-core.js';
4
+ // Keep the older unprepared-passage API usable by local callers. Hosted search
5
+ // imports the prepared core and never needs a Markdown parser at request time.
6
+ const paragraphs = (hit) => hit.chunk.search?.paragraphs ?? searchParagraphs(hit.chunk.text);
7
+ export const identifierMatch = (query) => preparedMatch(query, paragraphs);
8
+ export const identifierRescue = (query, semantic, lexical) => preparedRescue(query, semantic, lexical, paragraphs);
@@ -1,4 +1,4 @@
1
- import { searchIdentifiers } from './search-identifier.js';
1
+ import { searchIdentifiers } from './search-identifier-core.js';
2
2
  const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
3
  const boundary = (value) => new RegExp(`(?<![\\w$.-])${escape(value)}(?![\\w$-]|\\.[\\w$])`, 'i');
4
4
  // Only explicit technical syntax gets this exception. Ordinary topic words must
@@ -1,6 +1,6 @@
1
1
  import { compareHits, tokenize } from './retrieval.js';
2
2
  import { literalSearch } from './search-literal.js';
3
- import { identifierRescue } from './search-identifier.js';
3
+ import { identifierRescue } from './search-identifier-core.js';
4
4
  // Select landing passages for the search box, without changing passage retrieval
5
5
  // used by Ask. A gallery's repeated examples must not crowd other pages out.
6
6
  export function createSearchResultSelector(retriever) {
@@ -1,3 +1,4 @@
1
+ export { searchSnippetFromParagraphs } from './search-text.js';
1
2
  export declare function searchParagraphs(markdown: string): string[];
2
3
  export declare function searchPassageParagraphs(markdown: string): string[];
3
4
  export declare function searchLiteralLines(markdown: string): string[];
@@ -8,4 +9,3 @@ export type PreparedSearchPassage = {
8
9
  };
9
10
  export declare function prepareSearchPassage(markdown: string): PreparedSearchPassage;
10
11
  export declare function searchSnippet(markdown: string, query: string, pageSummary?: string): string;
11
- export declare function searchSnippetFromParagraphs(choices: string[], query: string, pageSummary?: string): string;
@@ -1,3 +1,5 @@
1
+ import { searchSnippetFromParagraphs } from './search-text.js';
2
+ export { searchSnippetFromParagraphs } from './search-text.js';
1
3
  import { unified } from 'unified';
2
4
  import remarkParse from 'remark-parse';
3
5
  import remarkGfm from 'remark-gfm';
@@ -71,23 +73,9 @@ export function prepareSearchPassage(markdown) {
71
73
  const tree = parser.parse(markdown);
72
74
  return { schemaVersion: 1, paragraphs: passageParagraphs(tree), literalLines: cleanLines(literalLines(tree)) };
73
75
  }
74
- const stopWords = new Set('a an the i me my we our you your how do does can could would should will is are was were be to of in on for with please what where when why which and or it this that have has after before'.split(' '));
75
76
  // Prefer substantive query matches; Markdown boilerplate and common question words
76
77
  // must not hide a useful paragraph. Keep a matching term visible in long paragraphs.
77
78
  export function searchSnippet(markdown, query, pageSummary = '') {
78
79
  const choices = searchParagraphs(markdown).filter(p => !discovery.test(p));
79
80
  return searchSnippetFromParagraphs(choices, query, pageSummary);
80
81
  }
81
- export function searchSnippetFromParagraphs(choices, query, pageSummary = '') {
82
- const terms = [...new Set((query.toLocaleLowerCase().match(/[\p{L}\p{N}_$.-]+/gu) ?? []).map(t => t.replace(/^[.-]+|[.-]+$/g, '')).filter(t => t && !stopWords.has(t)))];
83
- const matches = (text) => terms.filter(term => new RegExp(`(?<![\\p{L}\\p{N}_])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\p{L}\\p{N}_])`, 'iu').test(text));
84
- const ranked = choices.map((text, at) => ({ text, at, terms: matches(text) })).sort((a, b) => b.terms.length - a.terms.length || a.at - b.at);
85
- const selected = ranked[0];
86
- const text = (selected?.text ?? pageSummary).replace(/\s+/g, ' ').trim();
87
- if (text.length <= 180)
88
- return text;
89
- const first = selected?.terms.length ? Math.min(...selected.terms.map(t => text.toLocaleLowerCase().indexOf(t))) : 0;
90
- const start = Math.max(0, first - 50);
91
- const length = 180 - (start ? 1 : 0) - 1;
92
- return `${start ? '…' : ''}${text.slice(start, start + length).trim()}…`;
93
- }
@@ -0,0 +1 @@
1
+ export declare function searchSnippetFromParagraphs(choices: string[], query: string, pageSummary?: string): string;
@@ -0,0 +1,14 @@
1
+ const stopWords = new Set('a an the i me my we our you your how do does can could would should will is are was were be to of in on for with please what where when why which and or it this that have has after before'.split(' '));
2
+ export function searchSnippetFromParagraphs(choices, query, pageSummary = '') {
3
+ const terms = [...new Set((query.toLocaleLowerCase().match(/[\p{L}\p{N}_$.-]+/gu) ?? []).map(t => t.replace(/^[.-]+|[.-]+$/g, '')).filter(t => t && !stopWords.has(t)))];
4
+ const matches = (text) => terms.filter(term => new RegExp(`(?<![\\p{L}\\p{N}_])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\p{L}\\p{N}_])`, 'iu').test(text));
5
+ const ranked = choices.map((text, at) => ({ text, at, terms: matches(text) })).sort((a, b) => b.terms.length - a.terms.length || a.at - b.at);
6
+ const selected = ranked[0];
7
+ const text = (selected?.text ?? pageSummary).replace(/\s+/g, ' ').trim();
8
+ if (text.length <= 180)
9
+ return text;
10
+ const first = selected?.terms.length ? Math.min(...selected.terms.map(t => text.toLocaleLowerCase().indexOf(t))) : 0;
11
+ const start = Math.max(0, first - 50);
12
+ const length = 180 - (start ? 1 : 0) - 1;
13
+ return `${start ? '…' : ''}${text.slice(start, start + length).trim()}…`;
14
+ }
@@ -100,8 +100,16 @@ export async function addAskFunction(input) {
100
100
  await writeFile(path.join(func, 'index.js'), `import { createAskFunction, nodeAdapter } from './lib/ask-function.js';\n\nconst directory = new URL('.', import.meta.url).pathname;\nexport default nodeAdapter(() => createAskFunction(directory));\n`);
101
101
  // The answer can take about a minute. Hobby allows up to 300 seconds with Fluid compute, so 120 leaves room.
102
102
  await writeFile(path.join(func, '.vc-config.json'), JSON.stringify({ runtime: 'nodejs22.x', handler: 'index.js', launcherType: 'Nodejs', shouldAddHelpers: false, maxDuration: 120, memory: 1024 }, null, 2) + '\n');
103
- await cp(func, searchFunc, { recursive: true });
104
- await writeFile(path.join(searchFunc, 'index.js'), `import { createHostedSearchFunction, nodeAdapter } from './lib/ask-function.js';\n\nconst directory = new URL('.', import.meta.url).pathname;\nexport default nodeAdapter(() => createHostedSearchFunction(directory));\n`);
103
+ // Build Search from its own runtime closure and private retrieval data. It
104
+ // must not inherit chat generation, cards, logging, or compiler dependencies.
105
+ await mkdir(path.join(searchFunc, 'site', 'ai'), { recursive: true });
106
+ for (const name of ['chunks.json', 'embeddings.json', 'embeddings.bin'])
107
+ await cp(path.join(site, 'ai', name), path.join(searchFunc, 'site', 'ai', name));
108
+ const { chat: _chat, ...searchSettings } = runtime.settings;
109
+ await writeFile(path.join(searchFunc, 'runtime.json'), JSON.stringify({ ...runtime, settings: searchSettings, searchStore: chat.budget.store }, null, 2) + '\n');
110
+ await writeFile(path.join(searchFunc, 'package.json'), JSON.stringify({ type: 'module' }) + '\n');
111
+ await copyFunctionRuntime(dist, 'search-function.js', searchFunc);
112
+ await writeFile(path.join(searchFunc, 'index.js'), `import { createHostedSearchFunction } from './lib/search-function.js';\nimport { nodeAdapter } from './lib/function-runtime.js';\n\nconst directory = new URL('.', import.meta.url).pathname;\nexport default nodeAdapter(() => createHostedSearchFunction(directory));\n`);
105
113
  await writeFile(path.join(searchFunc, '.vc-config.json'), JSON.stringify({ runtime: 'nodejs22.x', handler: 'index.js', launcherType: 'Nodejs', shouldAddHelpers: false, maxDuration: 30, memory: 1024 }, null, 2) + '\n');
106
114
  await readdir(func);
107
115
  return { added: true, directory: func, chunks: built.index.chunks.length, embedded: true, embeddingCounts: { embedded: embedded.embedded, reused: embedded.reused } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoalrun/cli",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.17",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public"