@larkup/workspace-server 0.0.0-stage → 0.2.1

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 (180) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +2 -2
  3. package/dist-types/app/api/chat/feedback/route.d.ts +4 -0
  4. package/dist-types/app/api/chat/feedback/route.d.ts.map +1 -0
  5. package/dist-types/app/api/chat/route.d.ts +3 -0
  6. package/dist-types/app/api/chat/route.d.ts.map +1 -0
  7. package/dist-types/app/api/chat/tools.d.ts +67 -0
  8. package/dist-types/app/api/chat/tools.d.ts.map +1 -0
  9. package/dist-types/app/api/config/test-embedding/route.d.ts +3 -0
  10. package/dist-types/app/api/config/test-embedding/route.d.ts.map +1 -0
  11. package/dist-types/app/api/config/verify/route.d.ts +3 -0
  12. package/dist-types/app/api/config/verify/route.d.ts.map +1 -0
  13. package/dist-types/app/api/describe-image/route.d.ts +4 -0
  14. package/dist-types/app/api/describe-image/route.d.ts.map +1 -0
  15. package/dist-types/app/api/doc-editor/edit/route.d.ts +10 -0
  16. package/dist-types/app/api/doc-editor/edit/route.d.ts.map +1 -0
  17. package/dist-types/app/api/doc-editor/export/route.d.ts +9 -0
  18. package/dist-types/app/api/doc-editor/export/route.d.ts.map +1 -0
  19. package/dist-types/app/api/doc-editor/parse/route.d.ts +10 -0
  20. package/dist-types/app/api/doc-editor/parse/route.d.ts.map +1 -0
  21. package/dist-types/app/api/doc-editor/restore/route.d.ts +2 -0
  22. package/dist-types/app/api/doc-editor/restore/route.d.ts.map +1 -0
  23. package/dist-types/app/api/document/sign/route.d.ts +2 -0
  24. package/dist-types/app/api/document/sign/route.d.ts.map +1 -0
  25. package/dist-types/app/api/files/remote/route.d.ts +5 -0
  26. package/dist-types/app/api/files/remote/route.d.ts.map +1 -0
  27. package/dist-types/app/api/mcp/[connectionId]/route.d.ts +9 -0
  28. package/dist-types/app/api/mcp/[connectionId]/route.d.ts.map +1 -0
  29. package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts +8 -0
  30. package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts.map +1 -0
  31. package/dist-types/app/api/mcp/route.d.ts +4 -0
  32. package/dist-types/app/api/mcp/route.d.ts.map +1 -0
  33. package/dist-types/app/api/parse-file/route.d.ts +6 -0
  34. package/dist-types/app/api/parse-file/route.d.ts.map +1 -0
  35. package/dist-types/app/api/pdf-page/route.d.ts +4 -0
  36. package/dist-types/app/api/pdf-page/route.d.ts.map +1 -0
  37. package/dist-types/app/api/proxy/verify/route.d.ts +4 -0
  38. package/dist-types/app/api/proxy/verify/route.d.ts.map +1 -0
  39. package/dist-types/app/api/search/bing/route.d.ts +5 -0
  40. package/dist-types/app/api/search/bing/route.d.ts.map +1 -0
  41. package/dist-types/app/api/search/brave/route.d.ts +5 -0
  42. package/dist-types/app/api/search/brave/route.d.ts.map +1 -0
  43. package/dist-types/app/api/search/exa/route.d.ts +5 -0
  44. package/dist-types/app/api/search/exa/route.d.ts.map +1 -0
  45. package/dist-types/app/api/search/google/route.d.ts +30 -0
  46. package/dist-types/app/api/search/google/route.d.ts.map +1 -0
  47. package/dist-types/app/api/search/route.d.ts +7 -0
  48. package/dist-types/app/api/search/route.d.ts.map +1 -0
  49. package/dist-types/app/api/search/tavily/route.d.ts +5 -0
  50. package/dist-types/app/api/search/tavily/route.d.ts.map +1 -0
  51. package/dist-types/app/api/search/verify/route.d.ts +4 -0
  52. package/dist-types/app/api/search/verify/route.d.ts.map +1 -0
  53. package/dist-types/app/api/tabular/route.d.ts +9 -0
  54. package/dist-types/app/api/tabular/route.d.ts.map +1 -0
  55. package/dist-types/lib/chat/assistant-source-scope.d.ts +10 -0
  56. package/dist-types/lib/chat/assistant-source-scope.d.ts.map +1 -0
  57. package/dist-types/lib/chat/conversation-memory.d.ts +80 -0
  58. package/dist-types/lib/chat/conversation-memory.d.ts.map +1 -0
  59. package/dist-types/lib/chat/gateway-fallbacks.d.ts +7 -0
  60. package/dist-types/lib/chat/gateway-fallbacks.d.ts.map +1 -0
  61. package/dist-types/lib/chat/image-description.d.ts +10 -0
  62. package/dist-types/lib/chat/image-description.d.ts.map +1 -0
  63. package/dist-types/lib/chat/media-retrieval-routing.d.ts +14 -0
  64. package/dist-types/lib/chat/media-retrieval-routing.d.ts.map +1 -0
  65. package/dist-types/lib/chat/media-source-routing.d.ts +53 -0
  66. package/dist-types/lib/chat/media-source-routing.d.ts.map +1 -0
  67. package/dist-types/lib/chat/message-input.d.ts +8 -0
  68. package/dist-types/lib/chat/message-input.d.ts.map +1 -0
  69. package/dist-types/lib/chat/model-provider.d.ts +23 -0
  70. package/dist-types/lib/chat/model-provider.d.ts.map +1 -0
  71. package/dist-types/lib/chat/response-style.d.ts +2 -0
  72. package/dist-types/lib/chat/response-style.d.ts.map +1 -0
  73. package/dist-types/lib/chat/retrieval-ranking.d.ts +32 -0
  74. package/dist-types/lib/chat/retrieval-ranking.d.ts.map +1 -0
  75. package/dist-types/lib/chat/retrieval-routing.d.ts +39 -0
  76. package/dist-types/lib/chat/retrieval-routing.d.ts.map +1 -0
  77. package/dist-types/lib/chat/tabular-dataset-routing.d.ts +15 -0
  78. package/dist-types/lib/chat/tabular-dataset-routing.d.ts.map +1 -0
  79. package/dist-types/lib/chat/tabular-query-plan.d.ts +12 -0
  80. package/dist-types/lib/chat/tabular-query-plan.d.ts.map +1 -0
  81. package/dist-types/lib/chat/tabular-routing.d.ts +41 -0
  82. package/dist-types/lib/chat/tabular-routing.d.ts.map +1 -0
  83. package/dist-types/lib/chat/tabular-sandbox-files.d.ts +33 -0
  84. package/dist-types/lib/chat/tabular-sandbox-files.d.ts.map +1 -0
  85. package/dist-types/lib/chat/tabular-visualization.d.ts +31 -0
  86. package/dist-types/lib/chat/tabular-visualization.d.ts.map +1 -0
  87. package/dist-types/lib/chat/tool-call-repair.d.ts +11 -0
  88. package/dist-types/lib/chat/tool-call-repair.d.ts.map +1 -0
  89. package/dist-types/lib/chat/tool-context.d.ts +72 -0
  90. package/dist-types/lib/chat/tool-context.d.ts.map +1 -0
  91. package/dist-types/lib/chat/tool-registry.d.ts +3 -0
  92. package/dist-types/lib/chat/tool-registry.d.ts.map +1 -0
  93. package/dist-types/lib/chat/video-rag-routing.d.ts +33 -0
  94. package/dist-types/lib/chat/video-rag-routing.d.ts.map +1 -0
  95. package/dist-types/lib/chat/visual-routing.d.ts +53 -0
  96. package/dist-types/lib/chat/visual-routing.d.ts.map +1 -0
  97. package/dist-types/lib/enterprise-client.d.ts +31 -0
  98. package/dist-types/lib/enterprise-client.d.ts.map +1 -0
  99. package/dist-types/lib/marketplace/tool-runtime-config.d.ts +7 -0
  100. package/dist-types/lib/marketplace/tool-runtime-config.d.ts.map +1 -0
  101. package/dist-types/lib/media/knowledge.d.ts +47 -0
  102. package/dist-types/lib/media/knowledge.d.ts.map +1 -0
  103. package/dist-types/lib/media/pdf-inspection.d.ts +30 -0
  104. package/dist-types/lib/media/pdf-inspection.d.ts.map +1 -0
  105. package/dist-types/lib/media/source-identity.d.ts +12 -0
  106. package/dist-types/lib/media/source-identity.d.ts.map +1 -0
  107. package/dist-types/lib/media/source-utils.d.ts +157 -0
  108. package/dist-types/lib/media/source-utils.d.ts.map +1 -0
  109. package/dist-types/lib/media/video/quick-look.d.ts +60 -0
  110. package/dist-types/lib/media/video/quick-look.d.ts.map +1 -0
  111. package/dist-types/lib/media/video-intelligence-adapter.d.ts +521 -0
  112. package/dist-types/lib/media/video-intelligence-adapter.d.ts.map +1 -0
  113. package/dist-types/lib/media/video-intelligence-connection.d.ts +32 -0
  114. package/dist-types/lib/media/video-intelligence-connection.d.ts.map +1 -0
  115. package/dist-types/lib/remote-files.d.ts +12 -0
  116. package/dist-types/lib/remote-files.d.ts.map +1 -0
  117. package/dist-types/lib/search/provider-verification.d.ts +5 -0
  118. package/dist-types/lib/search/provider-verification.d.ts.map +1 -0
  119. package/dist-types/runtime-host.d.ts +20 -0
  120. package/dist-types/runtime-host.d.ts.map +1 -0
  121. package/package.json +338 -3
  122. package/src/app/api/chat/feedback/route.ts +77 -0
  123. package/src/app/api/chat/route.ts +2277 -0
  124. package/src/app/api/chat/tools.ts +3563 -0
  125. package/src/app/api/config/test-embedding/route.ts +39 -0
  126. package/src/app/api/config/verify/route.ts +309 -0
  127. package/src/app/api/describe-image/route.ts +108 -0
  128. package/src/app/api/doc-editor/edit/route.ts +87 -0
  129. package/src/app/api/doc-editor/export/route.ts +32 -0
  130. package/src/app/api/doc-editor/parse/route.ts +74 -0
  131. package/src/app/api/doc-editor/restore/route.ts +70 -0
  132. package/src/app/api/document/sign/route.ts +55 -0
  133. package/src/app/api/files/remote/route.ts +32 -0
  134. package/src/app/api/mcp/[connectionId]/route.ts +29 -0
  135. package/src/app/api/mcp/[connectionId]/test/route.ts +13 -0
  136. package/src/app/api/mcp/route.ts +22 -0
  137. package/src/app/api/parse-file/route.ts +86 -0
  138. package/src/app/api/pdf-page/route.ts +40 -0
  139. package/src/app/api/proxy/verify/route.ts +78 -0
  140. package/src/app/api/search/bing/route.ts +86 -0
  141. package/src/app/api/search/brave/route.ts +88 -0
  142. package/src/app/api/search/exa/route.ts +100 -0
  143. package/src/app/api/search/google/route.ts +147 -0
  144. package/src/app/api/search/route.ts +28 -0
  145. package/src/app/api/search/tavily/route.ts +93 -0
  146. package/src/app/api/search/verify/route.ts +27 -0
  147. package/src/app/api/tabular/route.ts +68 -0
  148. package/src/lib/chat/assistant-source-scope.ts +32 -0
  149. package/src/lib/chat/conversation-memory.ts +621 -0
  150. package/src/lib/chat/gateway-fallbacks.ts +12 -0
  151. package/src/lib/chat/image-description.ts +21 -0
  152. package/src/lib/chat/media-retrieval-routing.ts +37 -0
  153. package/src/lib/chat/media-source-routing.ts +100 -0
  154. package/src/lib/chat/message-input.ts +48 -0
  155. package/src/lib/chat/model-provider.ts +157 -0
  156. package/src/lib/chat/response-style.ts +3 -0
  157. package/src/lib/chat/retrieval-ranking.ts +126 -0
  158. package/src/lib/chat/retrieval-routing.ts +221 -0
  159. package/src/lib/chat/tabular-dataset-routing.ts +93 -0
  160. package/src/lib/chat/tabular-query-plan.ts +195 -0
  161. package/src/lib/chat/tabular-routing.ts +122 -0
  162. package/src/lib/chat/tabular-sandbox-files.ts +78 -0
  163. package/src/lib/chat/tabular-visualization.ts +165 -0
  164. package/src/lib/chat/tool-call-repair.ts +84 -0
  165. package/src/lib/chat/tool-context.ts +1222 -0
  166. package/src/lib/chat/tool-registry.ts +6 -0
  167. package/src/lib/chat/video-rag-routing.ts +149 -0
  168. package/src/lib/chat/visual-routing.ts +225 -0
  169. package/src/lib/enterprise-client.ts +96 -0
  170. package/src/lib/marketplace/tool-runtime-config.ts +155 -0
  171. package/src/lib/media/knowledge.ts +489 -0
  172. package/src/lib/media/pdf-inspection.ts +280 -0
  173. package/src/lib/media/source-identity.ts +50 -0
  174. package/src/lib/media/source-utils.ts +1156 -0
  175. package/src/lib/media/video/quick-look.ts +368 -0
  176. package/src/lib/media/video-intelligence-adapter.ts +1946 -0
  177. package/src/lib/media/video-intelligence-connection.ts +151 -0
  178. package/src/lib/remote-files.ts +182 -0
  179. package/src/lib/search/provider-verification.ts +95 -0
  180. package/src/runtime-host.ts +31 -0
@@ -0,0 +1,100 @@
1
+ import { readConfig } from '@larkup/core/config-store';
2
+ import { SerperSearchResponse, SerperSearchItem } from '../google/route';
3
+
4
+ export const runtime = 'nodejs';
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export async function POST(req: Request) {
8
+ const config = await readConfig();
9
+ const apiKey = config.exaApiKey;
10
+
11
+ if (!apiKey) {
12
+ return Response.json(
13
+ {
14
+ error: 'Exa search is not configured. Set EXA_API_KEY in your environment.',
15
+ configured: false,
16
+ },
17
+ { status: 503 },
18
+ );
19
+ }
20
+
21
+ let body: { query?: string; page?: number };
22
+ try {
23
+ body = await req.json();
24
+ } catch {
25
+ return Response.json({ error: 'Invalid request body.' }, { status: 400 });
26
+ }
27
+
28
+ const { query, page = 1 } = body;
29
+ if (!query?.trim()) {
30
+ return Response.json({ error: 'A search query is required.' }, { status: 400 });
31
+ }
32
+
33
+ if (page > 1) {
34
+ return Response.json({
35
+ items: [],
36
+ totalResults: 0,
37
+ totalResultsIsEstimate: true,
38
+ currentPage: page,
39
+ totalPages: 1,
40
+ hasMore: false,
41
+ query: query.trim(),
42
+ });
43
+ }
44
+
45
+ try {
46
+ const res = await fetch(`https://api.exa.ai/search`, {
47
+ method: 'POST',
48
+ headers: {
49
+ accept: 'application/json',
50
+ 'content-type': 'application/json',
51
+ 'x-api-key': apiKey,
52
+ },
53
+ body: JSON.stringify({
54
+ query: query.trim(),
55
+ numResults: 10,
56
+ useAutoprompt: true,
57
+ contents: {
58
+ text: {
59
+ maxCharacters: 500,
60
+ },
61
+ },
62
+ }),
63
+ cache: 'no-store',
64
+ });
65
+
66
+ const json = await res.json();
67
+
68
+ if (!res.ok || json.error) {
69
+ const msg = json.error ?? `Exa API error (${res.status})`;
70
+ return Response.json({ error: msg }, { status: res.status });
71
+ }
72
+
73
+ const items: SerperSearchItem[] = (json.results ?? []).map((r: any) => ({
74
+ url: r.url,
75
+ title: r.title,
76
+ description: r.text || r.summary || r.title,
77
+ }));
78
+
79
+ const rawTotal = items.length;
80
+
81
+ return Response.json({
82
+ items,
83
+ totalResults: rawTotal,
84
+ totalResultsIsEstimate: true,
85
+ currentPage: 1,
86
+ totalPages: 1,
87
+ hasMore: false,
88
+ query: query.trim(),
89
+ } satisfies SerperSearchResponse & { totalResultsIsEstimate: boolean });
90
+ } catch (err) {
91
+ const msg = err instanceof Error ? err.message : 'Exa search failed.';
92
+ return Response.json({ error: msg }, { status: 500 });
93
+ }
94
+ }
95
+
96
+ export async function GET() {
97
+ const config = await readConfig();
98
+ const configured = Boolean(config.exaApiKey);
99
+ return Response.json({ configured });
100
+ }
@@ -0,0 +1,147 @@
1
+ import { readConfig } from '@larkup/core/config-store';
2
+
3
+ export const runtime = 'nodejs';
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export interface SerperSearchItem {
7
+ url: string;
8
+ title: string;
9
+ description?: string;
10
+ }
11
+
12
+ export interface SerperSearchResponse {
13
+ items: SerperSearchItem[];
14
+ totalResults: number;
15
+ currentPage: number;
16
+ totalPages: number;
17
+ hasMore: boolean;
18
+ query: string;
19
+ }
20
+
21
+ /**
22
+ * POST { query, page } → paginated Serper.dev Google Search results with total count.
23
+ *
24
+ * Uses the Serper.dev API: https://serper.dev
25
+ * Much simpler than Google CSE — only one env var needed.
26
+ *
27
+ * Required env var:
28
+ * SERPER_API_KEY – from https://serper.dev → API Key tab
29
+ *
30
+ * Serper returns up to 100 results (10 per page, page 1–10).
31
+ */
32
+ export async function POST(req: Request) {
33
+ const config = await readConfig();
34
+ const apiKey = config.serperApiKey;
35
+
36
+ if (!apiKey) {
37
+ return Response.json(
38
+ {
39
+ error: 'Serper search is not configured. Set SERPER_API_KEY in your environment.',
40
+ configured: false,
41
+ },
42
+ { status: 503 },
43
+ );
44
+ }
45
+
46
+ let body: { query?: string; page?: number };
47
+ try {
48
+ body = await req.json();
49
+ } catch {
50
+ return Response.json({ error: 'Invalid request body.' }, { status: 400 });
51
+ }
52
+
53
+ const { query, page = 1 } = body;
54
+ if (!query?.trim()) {
55
+ return Response.json({ error: 'A search query is required.' }, { status: 400 });
56
+ }
57
+
58
+ const clampedPage = Math.max(1, Math.min(page, 10));
59
+
60
+ try {
61
+ const res = await fetch('https://google.serper.dev/search', {
62
+ method: 'POST',
63
+ headers: {
64
+ 'X-API-KEY': apiKey,
65
+ 'Content-Type': 'application/json',
66
+ },
67
+ body: JSON.stringify({
68
+ q: query.trim(),
69
+ num: 10,
70
+ page: clampedPage,
71
+ }),
72
+ cache: 'no-store',
73
+ });
74
+
75
+ const json = (await res.json()) as {
76
+ error?: string;
77
+ /** Serper free plan does NOT include searchInformation — we derive pagination from organic count. */
78
+ searchInformation?: {
79
+ totalResults?: number | string;
80
+ formattedTotalResults?: string;
81
+ timeTakenDisplayed?: number;
82
+ };
83
+ organic?: Array<{
84
+ link?: string;
85
+ title?: string;
86
+ snippet?: string;
87
+ position?: number;
88
+ }>;
89
+ };
90
+
91
+ if (!res.ok || json.error) {
92
+ const msg = json.error ?? `Serper API error (${res.status})`;
93
+ return Response.json({ error: msg }, { status: res.status });
94
+ }
95
+
96
+ const items: SerperSearchItem[] = (json.organic ?? [])
97
+ .filter((r) => r.link)
98
+ .map((r) => ({
99
+ url: r.link!,
100
+ title: r.title ?? r.link!,
101
+ description: r.snippet,
102
+ }));
103
+
104
+ // Serper's free plan omits searchInformation.totalResults.
105
+ // We derive pagination heuristically:
106
+ const gotFullPage = items.length >= 10;
107
+ const hasMore = gotFullPage && clampedPage < 10;
108
+
109
+ const minTotal = (clampedPage - 1) * 10 + items.length;
110
+ // If searchInformation is present (cloud/enterprise plan), use it.
111
+ const siTotal = json.searchInformation?.totalResults;
112
+ const rawTotal =
113
+ siTotal != null
114
+ ? typeof siTotal === 'string'
115
+ ? parseInt(siTotal, 10) || minTotal
116
+ : siTotal
117
+ : minTotal;
118
+
119
+ const totalPages =
120
+ siTotal != null
121
+ ? Math.min(Math.max(1, Math.ceil(Math.min(rawTotal, 100) / 10)), 10)
122
+ : hasMore
123
+ ? 10 // unknown upper bound — show up to 10 pages
124
+ : clampedPage; // last page reached
125
+
126
+ return Response.json({
127
+ items,
128
+ totalResults: rawTotal,
129
+ // Pass along whether totalResults is an estimate
130
+ totalResultsIsEstimate: siTotal == null,
131
+ currentPage: clampedPage,
132
+ totalPages,
133
+ hasMore,
134
+ query: query.trim(),
135
+ } satisfies SerperSearchResponse & { totalResultsIsEstimate: boolean });
136
+ } catch (err) {
137
+ const msg = err instanceof Error ? err.message : 'Serper search failed.';
138
+ return Response.json({ error: msg }, { status: 500 });
139
+ }
140
+ }
141
+
142
+ /** GET → whether Serper search is configured. */
143
+ export async function GET() {
144
+ const config = await readConfig();
145
+ const configured = Boolean(config.serperApiKey);
146
+ return Response.json({ configured });
147
+ }
@@ -0,0 +1,28 @@
1
+ import { searchWeb, isFirecrawlConfigured, FirecrawlError } from '@larkup/scraper/firecrawl';
2
+
3
+ export const runtime = 'nodejs';
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ /** GET → report whether the scraper is configured (drives the setup notice). */
7
+ export async function GET() {
8
+ return Response.json({ configured: await isFirecrawlConfigured() });
9
+ }
10
+
11
+ /** POST { query, limit } → keyword web search returning candidate URLs. */
12
+ export async function POST(req: Request) {
13
+ try {
14
+ const { query, limit } = (await req.json()) as {
15
+ query?: string;
16
+ limit?: number;
17
+ };
18
+ if (!query || !query.trim()) {
19
+ return Response.json({ error: 'A search query is required.' }, { status: 400 });
20
+ }
21
+ const results = await searchWeb(query.trim(), Math.min(limit ?? 10, 25));
22
+ return Response.json({ results });
23
+ } catch (err) {
24
+ const status = err instanceof FirecrawlError ? (err.status ?? 500) : 500;
25
+ const message = err instanceof Error ? err.message : 'Search failed.';
26
+ return Response.json({ error: message }, { status });
27
+ }
28
+ }
@@ -0,0 +1,93 @@
1
+ import { readConfig } from '@larkup/core/config-store';
2
+ import { SerperSearchResponse, SerperSearchItem } from '../google/route';
3
+
4
+ export const runtime = 'nodejs';
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export async function POST(req: Request) {
8
+ const config = await readConfig();
9
+ const apiKey = config.tavilyApiKey;
10
+
11
+ if (!apiKey) {
12
+ return Response.json(
13
+ {
14
+ error: 'Tavily search is not configured. Set TAVILY_API_KEY in your environment.',
15
+ configured: false,
16
+ },
17
+ { status: 503 },
18
+ );
19
+ }
20
+
21
+ let body: { query?: string; page?: number };
22
+ try {
23
+ body = await req.json();
24
+ } catch {
25
+ return Response.json({ error: 'Invalid request body.' }, { status: 400 });
26
+ }
27
+
28
+ const { query, page = 1 } = body;
29
+ if (!query?.trim()) {
30
+ return Response.json({ error: 'A search query is required.' }, { status: 400 });
31
+ }
32
+
33
+ // Tavily does not have a direct "page" param, we'll just do a basic search
34
+ // It returns up to 5-10 results depending on depth. We will ignore pagination for Tavily
35
+ // or return hasMore = false.
36
+
37
+ if (page > 1) {
38
+ return Response.json({
39
+ items: [],
40
+ totalResults: 0,
41
+ totalResultsIsEstimate: true,
42
+ currentPage: page,
43
+ totalPages: 1,
44
+ hasMore: false,
45
+ query: query.trim(),
46
+ });
47
+ }
48
+
49
+ try {
50
+ const res = await fetch(`https://api.tavily.com/search`, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ body: JSON.stringify({ api_key: apiKey, query: query.trim(), search_depth: 'basic' }),
56
+ cache: 'no-store',
57
+ });
58
+
59
+ const json = await res.json();
60
+
61
+ if (!res.ok || json.error) {
62
+ const msg = json.error ?? `Tavily API error (${res.status})`;
63
+ return Response.json({ error: msg }, { status: res.status });
64
+ }
65
+
66
+ const items: SerperSearchItem[] = (json.results ?? []).map((r: any) => ({
67
+ url: r.url,
68
+ title: r.title,
69
+ description: r.content,
70
+ }));
71
+
72
+ const rawTotal = items.length;
73
+
74
+ return Response.json({
75
+ items,
76
+ totalResults: rawTotal,
77
+ totalResultsIsEstimate: true,
78
+ currentPage: 1,
79
+ totalPages: 1,
80
+ hasMore: false,
81
+ query: query.trim(),
82
+ } satisfies SerperSearchResponse & { totalResultsIsEstimate: boolean });
83
+ } catch (err) {
84
+ const msg = err instanceof Error ? err.message : 'Tavily search failed.';
85
+ return Response.json({ error: msg }, { status: 500 });
86
+ }
87
+ }
88
+
89
+ export async function GET() {
90
+ const config = await readConfig();
91
+ const configured = Boolean(config.tavilyApiKey);
92
+ return Response.json({ configured });
93
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ isSearchVerificationProvider,
3
+ verifySearchProvider,
4
+ } from '@larkup/workspace-server/lib/search/provider-verification';
5
+
6
+ export const runtime = 'nodejs';
7
+ export const dynamic = 'force-dynamic';
8
+
9
+ export async function POST(req: Request) {
10
+ try {
11
+ const { provider, apiKey } = await req.json();
12
+
13
+ if (!provider || !apiKey) {
14
+ return Response.json({ error: 'Provider and API key are required' }, { status: 400 });
15
+ }
16
+
17
+ if (!isSearchVerificationProvider(provider)) {
18
+ return Response.json({ error: 'Unsupported provider' }, { status: 400 });
19
+ }
20
+
21
+ await verifySearchProvider(provider, apiKey);
22
+ return Response.json({ success: true });
23
+ } catch (err) {
24
+ const message = err instanceof Error ? err.message : 'Verification failed';
25
+ return Response.json({ error: message }, { status: 400 });
26
+ }
27
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ saveTabularDataset,
3
+ listTabularDatasets,
4
+ deleteTabularDataset,
5
+ } from '@larkup/core/tabular-store';
6
+ import { resolveGroupId, UnknownDataGroupError } from '@larkup/core/groups-store';
7
+
8
+ export const runtime = 'nodejs';
9
+ export const dynamic = 'force-dynamic';
10
+
11
+ /** GET /api/tabular — List all tabular datasets (metadata only). */
12
+ export async function GET() {
13
+ try {
14
+ const datasets = await listTabularDatasets();
15
+ return Response.json({ datasets });
16
+ } catch (err) {
17
+ const message = err instanceof Error ? err.message : 'Failed to list datasets';
18
+ return Response.json({ error: message }, { status: 500 });
19
+ }
20
+ }
21
+
22
+ /** POST /api/tabular — Save a new tabular dataset. */
23
+ export async function POST(req: Request) {
24
+ try {
25
+ const { fileName, rows, groupId } = await req.json();
26
+
27
+ if (!fileName || !Array.isArray(rows) || rows.length === 0) {
28
+ return Response.json(
29
+ { error: 'fileName and non-empty rows array required' },
30
+ { status: 400 },
31
+ );
32
+ }
33
+
34
+ const dataset = await saveTabularDataset(fileName, rows, {
35
+ groupId: await resolveGroupId(typeof groupId === 'string' ? groupId : undefined),
36
+ });
37
+
38
+ return Response.json({
39
+ id: dataset.id,
40
+ fileName: dataset.fileName,
41
+ rowCount: dataset.rowCount,
42
+ columns: dataset.columns,
43
+ summary: dataset.summary,
44
+ });
45
+ } catch (err) {
46
+ const message = err instanceof Error ? err.message : 'Failed to save dataset';
47
+ return Response.json(
48
+ { error: message },
49
+ { status: err instanceof UnknownDataGroupError ? 400 : 500 },
50
+ );
51
+ }
52
+ }
53
+
54
+ /** DELETE /api/tabular?id=... — Delete a tabular dataset. */
55
+ export async function DELETE(req: Request) {
56
+ try {
57
+ const url = new URL(req.url);
58
+ const id = url.searchParams.get('id');
59
+ if (!id) {
60
+ return Response.json({ error: 'id required' }, { status: 400 });
61
+ }
62
+ await deleteTabularDataset(id);
63
+ return Response.json({ ok: true });
64
+ } catch (err) {
65
+ const message = err instanceof Error ? err.message : 'Failed to delete dataset';
66
+ return Response.json({ error: message }, { status: 500 });
67
+ }
68
+ }
@@ -0,0 +1,32 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { SourceDocument } from '@larkup/core/types';
3
+
4
+ /**
5
+ * Fingerprint every source property that can change a grounded text answer.
6
+ * This permits an exact repeat to skip retrieval only while its source scope
7
+ * and Assistant configuration remain unchanged.
8
+ */
9
+ export function assistantSourceScopeFingerprint(
10
+ documents: readonly SourceDocument[],
11
+ context?: { configUpdatedAt?: string },
12
+ ) {
13
+ const hash = createHash('sha256');
14
+ hash.update(context?.configUpdatedAt ?? '');
15
+ for (const document of [...documents].sort((left, right) => left.id.localeCompare(right.id))) {
16
+ hash.update('\0document\0');
17
+ hash.update(
18
+ JSON.stringify({
19
+ id: document.id,
20
+ title: document.title,
21
+ url: document.url,
22
+ source: document.source,
23
+ content: document.content,
24
+ groupId: document.groupId ?? 'default',
25
+ enabled: document.enabled !== false,
26
+ status: document.status,
27
+ createdAt: document.createdAt,
28
+ }),
29
+ );
30
+ }
31
+ return hash.digest('hex');
32
+ }