larkup 0.1.14
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/.env +2 -0
- package/app/actions/vercel.ts +417 -0
- package/app/api/chat/route.ts +192 -0
- package/app/api/chat/status/route.ts +55 -0
- package/app/api/chat/suggestions/route.ts +115 -0
- package/app/api/config/route.ts +73 -0
- package/app/api/config/test/route.ts +67 -0
- package/app/api/config/test-embedding/route.ts +40 -0
- package/app/api/config/test-llm/route.ts +47 -0
- package/app/api/config/test-provider/route.ts +36 -0
- package/app/api/demo/route.ts +176 -0
- package/app/api/deploy/ssh/route.ts +340 -0
- package/app/api/documents/route.ts +90 -0
- package/app/api/firecrawl/local/route.ts +46 -0
- package/app/api/index/route.ts +113 -0
- package/app/api/jobs/[id]/route.ts +44 -0
- package/app/api/jobs/route.ts +116 -0
- package/app/api/parse-file/route.ts +39 -0
- package/app/api/search/google/route.ts +162 -0
- package/app/api/search/route.ts +29 -0
- package/app/api/server/download/route.ts +112 -0
- package/app/api/server/generate/route.ts +46 -0
- package/app/api/server/local/route.ts +42 -0
- package/app/api/servers/route.ts +137 -0
- package/app/api/vector-stores/install/route.ts +65 -0
- package/app/api/vector-stores/status/route.ts +26 -0
- package/app/chat/page.tsx +15 -0
- package/app/configure/page.tsx +53 -0
- package/app/data/page.tsx +15 -0
- package/app/demo/page.tsx +15 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +190 -0
- package/app/index-data/page.tsx +15 -0
- package/app/layout.tsx +51 -0
- package/app/page.tsx +34 -0
- package/app/server/page.tsx +22 -0
- package/app/simple/chat/page.tsx +17 -0
- package/app/simple/docs/page.tsx +21 -0
- package/app/simple/layout.tsx +9 -0
- package/app/simple/settings/page.tsx +627 -0
- package/app/template.tsx +16 -0
- package/app/themes.css +733 -0
- package/components/alerts/generic-alert.tsx +74 -0
- package/components/app-sidebar.tsx +187 -0
- package/components/app-topnav.tsx +195 -0
- package/components/chat/chat-settings-modal.tsx +209 -0
- package/components/chat/chat-workspace.tsx +348 -0
- package/components/chat/knowledge-base-result.tsx +97 -0
- package/components/chat/message-item.tsx +103 -0
- package/components/client-layout-wrapper.tsx +182 -0
- package/components/configure/configure-form.tsx +1683 -0
- package/components/configure/store-fields.tsx +152 -0
- package/components/data/corpus-panel.tsx +499 -0
- package/components/data/data-workspace.tsx +272 -0
- package/components/data/firecrawl-notice.tsx +236 -0
- package/components/data/jobs-panel.tsx +485 -0
- package/components/data/paste-panel.tsx +79 -0
- package/components/data/scrape-panel.tsx +964 -0
- package/components/data/upload-panel.tsx +714 -0
- package/components/demo/demo-workspace.tsx +419 -0
- package/components/global-settings.tsx +286 -0
- package/components/index/index-workspace.tsx +482 -0
- package/components/onboarding/simple-setup.tsx +702 -0
- package/components/onboarding/tech-setup.tsx +121 -0
- package/components/onboarding/welcome-screen.tsx +183 -0
- package/components/page-header.tsx +44 -0
- package/components/server/code-viewer.tsx +87 -0
- package/components/server/deploy-button.tsx +1390 -0
- package/components/server/server-workspace.tsx +392 -0
- package/components/simple/sdk-connect-dialog.tsx +201 -0
- package/components/simple/simple-index-button.tsx +83 -0
- package/components/simple/simple-sidebar.tsx +161 -0
- package/components/theme-customizer-provider.tsx +171 -0
- package/components/theme-switcher.tsx +445 -0
- package/components/ui/alert-dialog.tsx +187 -0
- package/components/ui/alert.tsx +76 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +59 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/checkbox.tsx +28 -0
- package/components/ui/collapsible.tsx +21 -0
- package/components/ui/dialog.tsx +159 -0
- package/components/ui/dropdown-menu.tsx +281 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +20 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/progress.tsx +83 -0
- package/components/ui/provider-icon.tsx +94 -0
- package/components/ui/scroll-area.tsx +55 -0
- package/components/ui/select.tsx +248 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/sonner.tsx +63 -0
- package/components/ui/switch.tsx +32 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +84 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/workspace/delete-server-dialog.tsx +71 -0
- package/components/workspace/server-form-dialog.tsx +103 -0
- package/components/workspace/server-switcher.tsx +163 -0
- package/components/workspace/workspace-provider.tsx +177 -0
- package/components/workspace/workspace-top-bar.tsx +23 -0
- package/components.json +21 -0
- package/lib/error-formatter.ts +15 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +18 -0
- package/package.json +65 -0
- package/postcss.config.mjs +8 -0
- package/public/docker.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/github.svg +3 -0
- package/public/hetzner.svg +1 -0
- package/public/icons/aws.svg +38 -0
- package/public/icons/azure.svg +1 -0
- package/public/icons/chroma.png +0 -0
- package/public/icons/cohere.svg +30 -0
- package/public/icons/digital-ocean.webp +0 -0
- package/public/icons/firecrawl2.png +0 -0
- package/public/icons/gcp.svg +1 -0
- package/public/icons/gemini.svg +1 -0
- package/public/icons/jina.svg +1 -0
- package/public/icons/lancedb2.png +0 -0
- package/public/icons/mistral.svg +469 -0
- package/public/icons/nomic.png +0 -0
- package/public/icons/openai.svg +1 -0
- package/public/icons/pgvector2.png +0 -0
- package/public/icons/pinecone.png +0 -0
- package/public/icons/qdrant.svg +35 -0
- package/public/icons/supabase.png +0 -0
- package/public/icons/vercel copy.svg +1 -0
- package/public/icons/vercel.svg +1 -0
- package/public/icons/voyage-light.png +0 -0
- package/public/icons/weaviate.webp +0 -0
- package/public/logo-light.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo9.png +0 -0
- package/public/vercel.svg +1 -0
- package/tsconfig.json +33 -0
package/.env
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { getActiveServer } from "@larkup/core/workspace";
|
|
6
|
+
|
|
7
|
+
const VERCEL_API = "https://api.vercel.com";
|
|
8
|
+
|
|
9
|
+
interface VercelProject {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
link?: {
|
|
13
|
+
type: string;
|
|
14
|
+
repo?: string;
|
|
15
|
+
repoId?: number;
|
|
16
|
+
org?: string;
|
|
17
|
+
gitCredentialId?: string;
|
|
18
|
+
productionBranch?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface VercelDeployment {
|
|
23
|
+
id: string;
|
|
24
|
+
uid: string;
|
|
25
|
+
name: string;
|
|
26
|
+
url: string;
|
|
27
|
+
readyState: string;
|
|
28
|
+
target?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function vercelFetch(
|
|
32
|
+
path: string,
|
|
33
|
+
token: string,
|
|
34
|
+
options: RequestInit = {},
|
|
35
|
+
) {
|
|
36
|
+
const res = await fetch(`${VERCEL_API}${path}`, {
|
|
37
|
+
...options,
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${token}`,
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
...(options.headers || {}),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
let body: any;
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(text);
|
|
49
|
+
} catch {
|
|
50
|
+
body = { rawText: text };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const msg =
|
|
55
|
+
body?.error?.message ||
|
|
56
|
+
body?.message ||
|
|
57
|
+
`API error occurred: Status ${res.status} Content-Type "${res.headers.get("content-type")}". Body: ${text}`;
|
|
58
|
+
throw new Error(msg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Look up a project by name or ID. Returns null if not found. */
|
|
65
|
+
async function getProject(
|
|
66
|
+
token: string,
|
|
67
|
+
nameOrId: string,
|
|
68
|
+
): Promise<VercelProject | null> {
|
|
69
|
+
try {
|
|
70
|
+
const data = await vercelFetch(
|
|
71
|
+
`/v9/projects/${encodeURIComponent(nameOrId)}`,
|
|
72
|
+
token,
|
|
73
|
+
);
|
|
74
|
+
return data as VercelProject;
|
|
75
|
+
} catch (e: any) {
|
|
76
|
+
if (
|
|
77
|
+
e.message?.toLowerCase().includes("not found") ||
|
|
78
|
+
e.message?.toLowerCase().includes("project not found")
|
|
79
|
+
) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Create a new blank Vercel project. */
|
|
87
|
+
async function createProject(
|
|
88
|
+
token: string,
|
|
89
|
+
name: string,
|
|
90
|
+
): Promise<VercelProject> {
|
|
91
|
+
const data = await vercelFetch(`/v9/projects`, token, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: JSON.stringify({ name, framework: null }),
|
|
94
|
+
});
|
|
95
|
+
return data as VercelProject;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Set an environment variable for the project */
|
|
99
|
+
async function setProjectEnv(
|
|
100
|
+
token: string,
|
|
101
|
+
projectId: string,
|
|
102
|
+
key: string,
|
|
103
|
+
value: string,
|
|
104
|
+
) {
|
|
105
|
+
// Try to set it. We use the /v10/projects/:id/env endpoint.
|
|
106
|
+
// First, check if it exists so we can patch or post
|
|
107
|
+
const envData = await vercelFetch(`/v9/projects/${projectId}/env`, token);
|
|
108
|
+
const existing = envData.envs?.find((e: any) => e.key === key);
|
|
109
|
+
|
|
110
|
+
if (existing) {
|
|
111
|
+
await vercelFetch(`/v9/projects/${projectId}/env/${existing.id}`, token, {
|
|
112
|
+
method: "PATCH",
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
value,
|
|
115
|
+
target: ["production", "preview", "development"],
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
await vercelFetch(`/v10/projects/${projectId}/env`, token, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: JSON.stringify([
|
|
122
|
+
{
|
|
123
|
+
key,
|
|
124
|
+
value,
|
|
125
|
+
target: ["production", "preview", "development"],
|
|
126
|
+
type: "plain",
|
|
127
|
+
},
|
|
128
|
+
]),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getFilesRecursively(dir: string, baseDir: string): any[] {
|
|
134
|
+
let results: any[] = [];
|
|
135
|
+
if (!fs.existsSync(dir)) return results;
|
|
136
|
+
const list = fs.readdirSync(dir);
|
|
137
|
+
for (const file of list) {
|
|
138
|
+
if (file === "node_modules" || file === ".git" || file === "server.log")
|
|
139
|
+
continue;
|
|
140
|
+
const filePath = path.join(dir, file);
|
|
141
|
+
const stat = fs.statSync(filePath);
|
|
142
|
+
if (stat && stat.isDirectory()) {
|
|
143
|
+
results = results.concat(getFilesRecursively(filePath, baseDir));
|
|
144
|
+
} else {
|
|
145
|
+
const data = fs.readFileSync(filePath);
|
|
146
|
+
const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/");
|
|
147
|
+
results.push({
|
|
148
|
+
file: relativePath,
|
|
149
|
+
data: data.toString("base64"),
|
|
150
|
+
encoding: "base64",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Deploy by sending files directly. */
|
|
158
|
+
async function triggerDeploymentWithFiles(
|
|
159
|
+
token: string,
|
|
160
|
+
project: VercelProject,
|
|
161
|
+
files: any[],
|
|
162
|
+
): Promise<VercelDeployment> {
|
|
163
|
+
const body: Record<string, any> = {
|
|
164
|
+
name: project.name,
|
|
165
|
+
target: "production",
|
|
166
|
+
files,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const data = await vercelFetch(`/v13/deployments`, token, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: JSON.stringify(body),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return data as VercelDeployment;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function getServerEnvRequirements(serverId: string) {
|
|
178
|
+
let activeId = serverId;
|
|
179
|
+
if (activeId === "default") {
|
|
180
|
+
const server = await getActiveServer();
|
|
181
|
+
if (server) activeId = server.id;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const cwd = process.cwd();
|
|
185
|
+
|
|
186
|
+
// ── Read the LIVE config (not the stale .env.example on disk).
|
|
187
|
+
// This ensures that switching vector stores in the UI is immediately
|
|
188
|
+
// reflected in the env-vars sheet without requiring a server regeneration.
|
|
189
|
+
let vectorStore: string = "lancedb";
|
|
190
|
+
let storeConfig: Record<string, string> = {};
|
|
191
|
+
const configCandidates = [
|
|
192
|
+
path.join(cwd, ".larkup", "servers", activeId, "config.json"),
|
|
193
|
+
path.join(cwd, ".larkup", "config.json"),
|
|
194
|
+
];
|
|
195
|
+
for (const cfgPath of configCandidates) {
|
|
196
|
+
if (fs.existsSync(cfgPath)) {
|
|
197
|
+
try {
|
|
198
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
199
|
+
vectorStore = cfg.vectorStore ?? "lancedb";
|
|
200
|
+
storeConfig = cfg.storeConfig ?? {};
|
|
201
|
+
break;
|
|
202
|
+
} catch {
|
|
203
|
+
// continue to next candidate
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Map storeConfig fields → env var names.
|
|
209
|
+
// The configure page stores credentials as storeConfig.apiKey, storeConfig.indexName, etc.
|
|
210
|
+
// Mirror the same mapping used in server-runtime.ts so configured values are
|
|
211
|
+
// automatically pre-filled as defaults in the env-vars sheet.
|
|
212
|
+
const storeDefaults: Record<string, string> = {};
|
|
213
|
+
if (vectorStore === "pinecone") {
|
|
214
|
+
if (storeConfig.apiKey) storeDefaults["PINECONE_API_KEY"] = storeConfig.apiKey;
|
|
215
|
+
if (storeConfig.indexName) storeDefaults["PINECONE_INDEX"] = storeConfig.indexName;
|
|
216
|
+
if (storeConfig.namespace) storeDefaults["PINECONE_NAMESPACE"] = storeConfig.namespace;
|
|
217
|
+
if (storeConfig.sparseModel) storeDefaults["PINECONE_SPARSE_MODEL"] = storeConfig.sparseModel;
|
|
218
|
+
if (storeConfig.sparseIndexName) storeDefaults["PINECONE_SPARSE_INDEX"] = storeConfig.sparseIndexName;
|
|
219
|
+
} else {
|
|
220
|
+
if (storeConfig.tableName) storeDefaults["LANCEDB_TABLE"] = storeConfig.tableName;
|
|
221
|
+
if (storeConfig.uri) storeDefaults["LANCEDB_URI"] = storeConfig.uri;
|
|
222
|
+
if (storeConfig.apiKey) storeDefaults["LANCEDB_API_KEY"] = storeConfig.apiKey;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Canonical env-var list — mirrors generateServer() in generate-server.ts.
|
|
226
|
+
// SERVER_API_KEY, PORT, LANCEDB_PATH, LANCEDB_MODE are handled internally
|
|
227
|
+
// and intentionally omitted here.
|
|
228
|
+
const envVarDefs: { key: string; required: boolean; help: string }[] = [
|
|
229
|
+
{
|
|
230
|
+
key: "EMBEDDING_API_KEY",
|
|
231
|
+
required: true,
|
|
232
|
+
help: "API key used to embed incoming queries.",
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
if (vectorStore === "pinecone") {
|
|
237
|
+
envVarDefs.push(
|
|
238
|
+
{ key: "PINECONE_API_KEY", required: true, help: "Pinecone API key." },
|
|
239
|
+
{ key: "PINECONE_INDEX", required: true, help: "Pinecone index name to query." },
|
|
240
|
+
{ key: "PINECONE_NAMESPACE", required: false, help: "Pinecone namespace (default 'default')." },
|
|
241
|
+
{ key: "PINECONE_SPARSE_MODEL", required: false, help: "Pinecone sparse model (for hybrid search)." },
|
|
242
|
+
{ key: "PINECONE_SPARSE_INDEX", required: false, help: "Pinecone sparse index name (for hybrid search)." },
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
// lancedb
|
|
246
|
+
envVarDefs.push(
|
|
247
|
+
{ key: "LANCEDB_TABLE", required: false, help: "Table name holding the embedded chunks (default 'documents')." },
|
|
248
|
+
{ key: "LANCEDB_URI", required: false, help: "LanceDB Cloud database URI (cloud mode)." },
|
|
249
|
+
{ key: "LANCEDB_API_KEY", required: false, help: "LanceDB Cloud API key (cloud mode)." },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Load .env file defaults (lowest priority).
|
|
254
|
+
const rootEnvPath = path.join(cwd, ".env");
|
|
255
|
+
const monoRootEnvPath = path.join(cwd, "../..", ".env");
|
|
256
|
+
const envFileDefaults: Record<string, string> = {};
|
|
257
|
+
for (const envFile of [monoRootEnvPath, rootEnvPath]) {
|
|
258
|
+
if (fs.existsSync(envFile)) {
|
|
259
|
+
const content = fs.readFileSync(envFile, "utf-8");
|
|
260
|
+
for (const line of content.split("\n")) {
|
|
261
|
+
const trimmed = line.trim();
|
|
262
|
+
if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
|
|
263
|
+
const [key, ...rest] = trimmed.split("=");
|
|
264
|
+
let val = rest.join("=").trim();
|
|
265
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
266
|
+
else if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
|
|
267
|
+
envFileDefaults[key.trim()] = val;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Priority: config > storeConfig (configure page) > .env file > empty
|
|
274
|
+
return envVarDefs.map((e) => {
|
|
275
|
+
let defaultValue = storeDefaults[e.key] || envFileDefaults[e.key] || "";
|
|
276
|
+
if (e.key === "EMBEDDING_API_KEY" && !defaultValue) {
|
|
277
|
+
// try to read from config directly
|
|
278
|
+
const cwd = process.cwd();
|
|
279
|
+
const configCandidates = [
|
|
280
|
+
path.join(cwd, ".larkup", "servers", activeId, "config.json"),
|
|
281
|
+
path.join(cwd, ".larkup", "config.json"),
|
|
282
|
+
];
|
|
283
|
+
for (const cfgPath of configCandidates) {
|
|
284
|
+
if (fs.existsSync(cfgPath)) {
|
|
285
|
+
try {
|
|
286
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
287
|
+
if (cfg.embeddingApiKey) defaultValue = cfg.embeddingApiKey;
|
|
288
|
+
break;
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
key: e.key,
|
|
295
|
+
help: e.help,
|
|
296
|
+
required: e.required,
|
|
297
|
+
defaultValue,
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
export async function deployToVercel(
|
|
304
|
+
token: string,
|
|
305
|
+
projectIdOrName: string,
|
|
306
|
+
serverId: string,
|
|
307
|
+
envVars: Record<string, string> = {}
|
|
308
|
+
) {
|
|
309
|
+
let activeId = serverId;
|
|
310
|
+
if (activeId === "default") {
|
|
311
|
+
const server = await getActiveServer();
|
|
312
|
+
if (server) activeId = server.id;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!token || !projectIdOrName) {
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
error: "Vercel token and project ID/name are required.",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!activeId) {
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
error: "Server ID is required to read deployment files.",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
// 1. Find existing project or create one
|
|
331
|
+
let project = await getProject(token, projectIdOrName);
|
|
332
|
+
const wasNew = !project;
|
|
333
|
+
|
|
334
|
+
if (!project) {
|
|
335
|
+
console.log(`Project "${projectIdOrName}" not found. Creating it...`);
|
|
336
|
+
project = await createProject(token, projectIdOrName);
|
|
337
|
+
console.log(`Created project: ${project.id} (${project.name})`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 2. Set environment variables
|
|
341
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
342
|
+
if (value) {
|
|
343
|
+
await setProjectEnv(token, project.id, key, value);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 3. Read the LIVE config to ensure we deploy the latest configuration
|
|
348
|
+
const cwd = process.cwd();
|
|
349
|
+
let config: any = null;
|
|
350
|
+
const configCandidates = [
|
|
351
|
+
path.join(cwd, ".larkup", "servers", activeId, "config.json"),
|
|
352
|
+
path.join(cwd, ".larkup", "config.json"),
|
|
353
|
+
];
|
|
354
|
+
for (const cfgPath of configCandidates) {
|
|
355
|
+
if (fs.existsSync(cfgPath)) {
|
|
356
|
+
try {
|
|
357
|
+
config = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
358
|
+
break;
|
|
359
|
+
} catch {}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!config) {
|
|
364
|
+
return { success: false, error: "Configuration not found. Please save your settings first." };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 4. Generate the server files in memory based on current config
|
|
368
|
+
const { generateServer } = await import("@larkup/core/generator/generate-server");
|
|
369
|
+
const generated = generateServer(config);
|
|
370
|
+
|
|
371
|
+
const files = generated.files.map((f) => ({
|
|
372
|
+
file: f.path,
|
|
373
|
+
data: f.encoding === "base64" ? f.contents : Buffer.from(f.contents).toString("base64"),
|
|
374
|
+
encoding: "base64",
|
|
375
|
+
}));
|
|
376
|
+
|
|
377
|
+
// 5. Attach LanceDB local data folder ONLY if LanceDB local is actually configured
|
|
378
|
+
const isLanceLocal = config.vectorStore === "lancedb" && config.storeConfig?.mode !== "cloud";
|
|
379
|
+
if (isLanceLocal) {
|
|
380
|
+
await setProjectEnv(token, project.id, "LANCEDB_PATH", "./lancedb");
|
|
381
|
+
const lancedbDir = path.join(cwd, ".larkup", "servers", activeId, "lancedb");
|
|
382
|
+
if (fs.existsSync(lancedbDir)) {
|
|
383
|
+
const lancedbFiles = getFilesRecursively(
|
|
384
|
+
lancedbDir,
|
|
385
|
+
path.join(cwd, ".larkup", "servers", activeId)
|
|
386
|
+
);
|
|
387
|
+
files.push(...lancedbFiles);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (files.length === 0) {
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
error: "No files found to deploy in the server directory.",
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 4. Create new deployment with files
|
|
399
|
+
const deployRes = await triggerDeploymentWithFiles(token, project, files);
|
|
400
|
+
|
|
401
|
+
// 5. Build canonical production URL
|
|
402
|
+
const url = `https://${project.name}.vercel.app`;
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
success: true,
|
|
406
|
+
url,
|
|
407
|
+
deploymentId: deployRes?.id,
|
|
408
|
+
projectCreated: wasNew,
|
|
409
|
+
};
|
|
410
|
+
} catch (error: any) {
|
|
411
|
+
console.error("Vercel Deploy Error:", error);
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
error: error.message || "Failed to trigger deployment on Vercel.",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
streamText,
|
|
3
|
+
convertToModelMessages,
|
|
4
|
+
tool,
|
|
5
|
+
stepCountIs,
|
|
6
|
+
type UIMessage,
|
|
7
|
+
} from "ai";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { readConfig } from "@larkup/core/config-store";
|
|
10
|
+
import { readRun } from "@larkup/core/index-store";
|
|
11
|
+
import { refreshServerStatus } from "@larkup/core/generator/server-runtime";
|
|
12
|
+
import { createAdapter } from "@larkup/vector-stores/factory";
|
|
13
|
+
import { embedQuery } from "@larkup/core/indexing/embedder";
|
|
14
|
+
import { runWithServer } from "@larkup/core/workspace";
|
|
15
|
+
import {
|
|
16
|
+
getDefaultChatModel,
|
|
17
|
+
getChatModel,
|
|
18
|
+
} from "@larkup/core/chat-models/registry";
|
|
19
|
+
|
|
20
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
21
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
22
|
+
import { createCohere } from "@ai-sdk/cohere";
|
|
23
|
+
import { createMistral } from "@ai-sdk/mistral";
|
|
24
|
+
import { createDeepSeek } from "@ai-sdk/deepseek";
|
|
25
|
+
import { createGateway } from "@ai-sdk/gateway";
|
|
26
|
+
|
|
27
|
+
export const maxDuration = 60;
|
|
28
|
+
|
|
29
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a helpful research assistant powered by a knowledge base.
|
|
30
|
+
|
|
31
|
+
You have one tool:
|
|
32
|
+
- "searchKnowledgeBase" — searches a private RAG knowledge base.
|
|
33
|
+
|
|
34
|
+
Guidelines:
|
|
35
|
+
- ONLY skip the tool if the user's message is a basic greeting (e.g., "hi", "hello") or simple conversational filler.
|
|
36
|
+
- For ALL OTHER messages (especially questions about facts, personal preferences, instructions, or specific topics), you MUST call the searchKnowledgeBase tool FIRST before answering. Do not assume you cannot answer a question (like "what do I like?") without checking the knowledge base first!
|
|
37
|
+
- Synthesize a clear, well-structured answer based on the retrieved documents.
|
|
38
|
+
- Cite sources inline using markdown links to their URLs when available.
|
|
39
|
+
- Be concise and accurate. Never fabricate sources or facts.
|
|
40
|
+
- Don't reply to the user with a question — try to give the answer unless you truly need clarification.
|
|
41
|
+
- IMPORTANT: If the searchKnowledgeBase tool returns empty results, inform the user that you currently have no knowledge in your database for their specific question. However, you may still attempt to answer general questions using your general knowledge.
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates an AI SDK language model instance based on the provider and model ID.
|
|
46
|
+
*/
|
|
47
|
+
function createChatModel(provider: string, modelId: string, apiKey?: string) {
|
|
48
|
+
const modelName = modelId.includes("/")
|
|
49
|
+
? modelId.split("/").slice(1).join("/")
|
|
50
|
+
: modelId;
|
|
51
|
+
const key = apiKey;
|
|
52
|
+
|
|
53
|
+
switch (provider) {
|
|
54
|
+
case "google":
|
|
55
|
+
return createGoogleGenerativeAI({ apiKey: key })(modelName);
|
|
56
|
+
case "cohere":
|
|
57
|
+
return createCohere({ apiKey: key })(modelName);
|
|
58
|
+
case "mistral":
|
|
59
|
+
return createMistral({ apiKey: key })(modelName);
|
|
60
|
+
case "deepseek":
|
|
61
|
+
return createDeepSeek({ apiKey: key })(modelName);
|
|
62
|
+
case "vercel_ai_gateway":
|
|
63
|
+
return createGateway({ apiKey: key })(modelId);
|
|
64
|
+
case "openai":
|
|
65
|
+
default:
|
|
66
|
+
return createOpenAI({ apiKey: key })(modelName);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Retrieves documents from the knowledge base — either via the running
|
|
72
|
+
* generated server or directly from the local vector store.
|
|
73
|
+
*/
|
|
74
|
+
async function queryKnowledgeBase(
|
|
75
|
+
query: string,
|
|
76
|
+
topK: number,
|
|
77
|
+
serverId: string | null,
|
|
78
|
+
) {
|
|
79
|
+
const doRetrieve = async () => {
|
|
80
|
+
const config = await readConfig();
|
|
81
|
+
|
|
82
|
+
// 1) Try running generated server first
|
|
83
|
+
const server = await refreshServerStatus();
|
|
84
|
+
if (server.running) {
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`${server.endpoint}/query`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ query, topK }),
|
|
90
|
+
signal: AbortSignal.timeout(15_000),
|
|
91
|
+
});
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
if (res.ok && data.hits) {
|
|
94
|
+
return {
|
|
95
|
+
query,
|
|
96
|
+
hits: (data.hits as any[]).map((h: any) => ({
|
|
97
|
+
title: h.title ?? "Untitled",
|
|
98
|
+
url: h.url ?? "",
|
|
99
|
+
score: Number((h.score ?? 0).toFixed(3)),
|
|
100
|
+
text: (h.text ?? "").slice(0, 1200),
|
|
101
|
+
})),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Fall through to direct retrieval
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2) Direct retrieval from local vector store
|
|
110
|
+
const run = await readRun();
|
|
111
|
+
if (!run || run.status !== "completed" || (run.totalChunks ?? 0) === 0) {
|
|
112
|
+
return { query, hits: [] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const vector = await embedQuery(config, query);
|
|
116
|
+
const adapter = await createAdapter(config);
|
|
117
|
+
const hits = await adapter.query(vector, topK, query);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
query,
|
|
121
|
+
hits: hits.map((h) => ({
|
|
122
|
+
title: h.title ?? "Untitled",
|
|
123
|
+
url: h.url ?? "",
|
|
124
|
+
score: Number((h.score ?? 0).toFixed(3)),
|
|
125
|
+
text: (h.text ?? "").slice(0, 1200),
|
|
126
|
+
})),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return serverId ? runWithServer(serverId, doRetrieve) : doRetrieve();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function POST(req: Request) {
|
|
134
|
+
const {
|
|
135
|
+
messages,
|
|
136
|
+
serverId,
|
|
137
|
+
chatModelId: requestedModelId,
|
|
138
|
+
}: {
|
|
139
|
+
messages: UIMessage[];
|
|
140
|
+
serverId?: string;
|
|
141
|
+
chatModelId?: string;
|
|
142
|
+
} = await req.json();
|
|
143
|
+
|
|
144
|
+
const config = await readConfig();
|
|
145
|
+
const provider = config.chatProvider || config.embeddingProvider;
|
|
146
|
+
|
|
147
|
+
// Resolve chat model: explicit request > config > default for provider
|
|
148
|
+
const chatModelId =
|
|
149
|
+
requestedModelId ||
|
|
150
|
+
config.chatModelId ||
|
|
151
|
+
getDefaultChatModel(provider)?.id ||
|
|
152
|
+
"openai/gpt-4o-mini";
|
|
153
|
+
|
|
154
|
+
const chatModelDescriptor = getChatModel(chatModelId);
|
|
155
|
+
const resolvedProvider =
|
|
156
|
+
provider === "vercel_ai_gateway"
|
|
157
|
+
? "vercel_ai_gateway"
|
|
158
|
+
: chatModelDescriptor?.provider || provider;
|
|
159
|
+
|
|
160
|
+
const apiKey = config.chatApiKey || config.embeddingApiKey || undefined;
|
|
161
|
+
console.log("Using API Key for chat:", apiKey ? `${apiKey.substring(0, 10)}...` : "NONE", "Provider:", resolvedProvider);
|
|
162
|
+
const model = createChatModel(resolvedProvider, chatModelId, apiKey);
|
|
163
|
+
|
|
164
|
+
const result = streamText({
|
|
165
|
+
model,
|
|
166
|
+
system: config.systemPrompt || DEFAULT_SYSTEM_PROMPT,
|
|
167
|
+
messages: await convertToModelMessages(messages),
|
|
168
|
+
stopWhen: stepCountIs(5),
|
|
169
|
+
tools: {
|
|
170
|
+
searchKnowledgeBase: tool({
|
|
171
|
+
description:
|
|
172
|
+
"Search the private RAG knowledge base for relevant documents. Use this for factual questions about the indexed content.",
|
|
173
|
+
inputSchema: z.object({
|
|
174
|
+
query: z
|
|
175
|
+
.string()
|
|
176
|
+
.describe("The search query for the knowledge base."),
|
|
177
|
+
}),
|
|
178
|
+
execute: async ({ query }) => {
|
|
179
|
+
return queryKnowledgeBase(query, 5, serverId ?? null);
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return result.toUIMessageStreamResponse({
|
|
186
|
+
onError: (error) => {
|
|
187
|
+
console.error("[chat] stream error:", error);
|
|
188
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
+
return message || "Something went wrong while generating a response.";
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { readConfig } from "@larkup/core/config-store";
|
|
3
|
+
import { readRun } from "@larkup/core/index-store";
|
|
4
|
+
import { runWithServer } from "@larkup/core/workspace";
|
|
5
|
+
import {
|
|
6
|
+
getChatModelsForProvider,
|
|
7
|
+
getDefaultChatModel,
|
|
8
|
+
} from "@larkup/core/chat-models/registry";
|
|
9
|
+
|
|
10
|
+
export const dynamic = "force-dynamic";
|
|
11
|
+
|
|
12
|
+
function withServer<T>(serverId: string | null, fn: () => Promise<T>) {
|
|
13
|
+
return serverId ? runWithServer(serverId, fn) : fn();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GET /api/chat/status — readiness snapshot for the Chat stage.
|
|
18
|
+
*
|
|
19
|
+
* Reports whether there's an index ready to chat against, plus
|
|
20
|
+
* the available chat models for the user's selected provider.
|
|
21
|
+
*/
|
|
22
|
+
export async function GET(req: Request) {
|
|
23
|
+
const serverId = new URL(req.url).searchParams.get("serverId");
|
|
24
|
+
return withServer(serverId, async () => {
|
|
25
|
+
const config = await readConfig();
|
|
26
|
+
const run = await readRun();
|
|
27
|
+
|
|
28
|
+
const indexed = run?.status === "completed" && (run.totalChunks ?? 0) > 0;
|
|
29
|
+
const hasApiKey = !!(config.chatApiKey || config.embeddingApiKey);
|
|
30
|
+
|
|
31
|
+
const blockers: string[] = [];
|
|
32
|
+
if (!hasApiKey) {
|
|
33
|
+
blockers.push(
|
|
34
|
+
"Set an API Key in Settings.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const provider = config.chatProvider || config.embeddingProvider;
|
|
39
|
+
const models = getChatModelsForProvider(provider);
|
|
40
|
+
const defaultModel = getDefaultChatModel(provider);
|
|
41
|
+
const chatModelId =
|
|
42
|
+
config.chatModelId || defaultModel?.id || "openai/gpt-4o-mini";
|
|
43
|
+
|
|
44
|
+
return NextResponse.json({
|
|
45
|
+
ready: hasApiKey,
|
|
46
|
+
indexed,
|
|
47
|
+
blockers,
|
|
48
|
+
provider,
|
|
49
|
+
chatModelId,
|
|
50
|
+
availableModels: models.map((m) => ({ id: m.id, label: m.label, provider: m.provider })),
|
|
51
|
+
suggestions: config.chatSuggestions || [],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|