@larkup/cli 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/dist/index.js ADDED
@@ -0,0 +1,3060 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ __require
4
+ } from "./chunk-3RG5ZIWI.js";
5
+
6
+ // src/index.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/ui/prompts.ts
10
+ import * as p from "@clack/prompts";
11
+
12
+ // src/ui/logger.ts
13
+ import chalk from "chalk";
14
+ var log = {
15
+ /** Standard info message (default color) */
16
+ info: (msg) => process.stdout.write(msg + "\n"),
17
+ /** Success message (green) */
18
+ success: (msg) => process.stdout.write(chalk.green(`\u2713 ${msg}`) + "\n"),
19
+ /** Error message (red) and exit */
20
+ error: (msg) => {
21
+ process.stderr.write(chalk.red(`\u2717 ${msg}`) + "\n");
22
+ process.exit(1);
23
+ },
24
+ /** Warning message (yellow) */
25
+ warn: (msg) => process.stdout.write(chalk.yellow(`! ${msg}`) + "\n"),
26
+ /** Dimmed/secondary text */
27
+ dim: (msg) => process.stdout.write(chalk.dim(msg) + "\n"),
28
+ /** Emphasized text block */
29
+ bold: (msg) => process.stdout.write(chalk.bold(msg) + "\n"),
30
+ /** Inline formatting helpers */
31
+ fmt: {
32
+ dim: chalk.dim,
33
+ bold: chalk.bold,
34
+ green: chalk.green,
35
+ red: chalk.red,
36
+ cyan: chalk.cyan,
37
+ yellow: chalk.yellow
38
+ }
39
+ };
40
+
41
+ // src/ui/prompts.ts
42
+ var prompts = {
43
+ intro: p.intro,
44
+ outro: p.outro,
45
+ text: async (opts) => {
46
+ const result = await p.text(opts);
47
+ if (p.isCancel(result)) {
48
+ log.error("Cancelled.");
49
+ }
50
+ return result;
51
+ },
52
+ select: async (opts) => {
53
+ const result = await p.select(opts);
54
+ if (p.isCancel(result)) {
55
+ log.error("Cancelled.");
56
+ }
57
+ return result;
58
+ },
59
+ confirm: async (opts) => {
60
+ const result = await p.confirm(opts);
61
+ if (p.isCancel(result)) {
62
+ log.error("Cancelled.");
63
+ }
64
+ return result;
65
+ },
66
+ spinner: p.spinner
67
+ };
68
+
69
+ // ../../packages/core/src/workspace.ts
70
+ import { promises as fs } from "fs";
71
+ import path from "path";
72
+ import { randomUUID } from "crypto";
73
+ import { AsyncLocalStorage } from "async_hooks";
74
+
75
+ // ../../packages/core/src/types.ts
76
+ var DEFAULT_CONFIG = {
77
+ projectName: "my-rag",
78
+ embeddingProvider: "openai",
79
+ embeddingApiKey: "",
80
+ embeddingModelId: "openai/text-embedding-3-small",
81
+ indexType: "hybrid",
82
+ chunking: {
83
+ chunkSize: 512,
84
+ chunkOverlap: 64,
85
+ strategy: "recursive"
86
+ },
87
+ vectorStore: "lancedb",
88
+ storeConfig: {
89
+ mode: "local",
90
+ dbPath: "./.larkup/lancedb"
91
+ },
92
+ topK: 5,
93
+ serperApiKey: "",
94
+ scraperProxyServer: "",
95
+ scraperProxyUsername: "",
96
+ scraperProxyPassword: "",
97
+ firecrawlApiKey: "",
98
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
99
+ };
100
+
101
+ // ../../packages/core/src/workspace.ts
102
+ var ROOT = path.join(process.cwd(), ".larkup");
103
+ var SERVERS_DIR = path.join(ROOT, "servers");
104
+ var WORKSPACE_PATH = path.join(ROOT, "workspace.json");
105
+ var BASE_PORT = 8080;
106
+ var EMPTY = { username: null, activeServerId: null, servers: [], mode: null };
107
+ var als = new AsyncLocalStorage();
108
+ function runWithServer(serverId, fn) {
109
+ return als.run({ serverId }, fn);
110
+ }
111
+ var writeChain = Promise.resolve();
112
+ function serialize(fn) {
113
+ const run = writeChain.then(fn, fn);
114
+ writeChain = run.catch(() => {
115
+ });
116
+ return run;
117
+ }
118
+ function serverDir(id) {
119
+ return path.join(SERVERS_DIR, id);
120
+ }
121
+ function relServerPath(id, sub) {
122
+ return `./.larkup/servers/${id}/${sub}`;
123
+ }
124
+ async function exists(p5) {
125
+ try {
126
+ await fs.access(p5);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ async function moveIfExists(from, to) {
133
+ if (!await exists(from)) return;
134
+ await fs.mkdir(path.dirname(to), { recursive: true });
135
+ try {
136
+ await fs.rename(from, to);
137
+ } catch {
138
+ await fs.cp(from, to, { recursive: true });
139
+ await fs.rm(from, { recursive: true, force: true });
140
+ }
141
+ }
142
+ async function readRaw() {
143
+ try {
144
+ const raw = await fs.readFile(WORKSPACE_PATH, "utf8");
145
+ return { ...EMPTY, ...JSON.parse(raw) };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ async function writeRaw(ws) {
151
+ await fs.mkdir(ROOT, { recursive: true });
152
+ await fs.writeFile(WORKSPACE_PATH, JSON.stringify(ws, null, 2), "utf8");
153
+ return ws;
154
+ }
155
+ async function getWorkspace() {
156
+ const existing = await readRaw();
157
+ if (existing) return existing;
158
+ return serialize(migrateOrInit);
159
+ }
160
+ async function migrateOrInit() {
161
+ const again = await readRaw();
162
+ if (again) return again;
163
+ const hasLegacy = await exists(path.join(ROOT, "config.json"));
164
+ if (!hasLegacy) {
165
+ return writeRaw(EMPTY);
166
+ }
167
+ const id = randomUUID();
168
+ const dir = serverDir(id);
169
+ await fs.mkdir(dir, { recursive: true });
170
+ for (const f of [
171
+ "config.json",
172
+ "documents.json",
173
+ "jobs.json",
174
+ "index-run.json",
175
+ "server-local.json"
176
+ ]) {
177
+ await moveIfExists(path.join(ROOT, f), path.join(dir, f));
178
+ }
179
+ await moveIfExists(path.join(ROOT, "lancedb"), path.join(dir, "lancedb"));
180
+ await moveIfExists(
181
+ path.join(ROOT, "generated-server"),
182
+ path.join(dir, "generated-server")
183
+ );
184
+ let name = "Default server";
185
+ try {
186
+ const cfgPath = path.join(dir, "config.json");
187
+ const cfg = JSON.parse(await fs.readFile(cfgPath, "utf8"));
188
+ if (cfg.projectName) name = cfg.projectName;
189
+ if (cfg.vectorStore === "lancedb" && cfg.storeConfig?.mode !== "cloud") {
190
+ cfg.storeConfig = {
191
+ ...cfg.storeConfig,
192
+ dbPath: relServerPath(id, "lancedb")
193
+ };
194
+ }
195
+ await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
196
+ } catch {
197
+ }
198
+ const now = (/* @__PURE__ */ new Date()).toISOString();
199
+ const meta = {
200
+ id,
201
+ name,
202
+ port: BASE_PORT,
203
+ createdAt: now,
204
+ updatedAt: now
205
+ };
206
+ return writeRaw({ username: null, activeServerId: id, servers: [meta], mode: null });
207
+ }
208
+ async function getActiveServer() {
209
+ const ws = await getWorkspace();
210
+ const override = als.getStore()?.serverId;
211
+ const id = override ?? ws.activeServerId;
212
+ if (!id) return null;
213
+ return ws.servers.find((s) => s.id === id) ?? null;
214
+ }
215
+ async function getDataDir() {
216
+ const server = await getActiveServer();
217
+ if (!server) return null;
218
+ const dir = serverDir(server.id);
219
+ await fs.mkdir(dir, { recursive: true });
220
+ return dir;
221
+ }
222
+ async function requireDataDir() {
223
+ const dir = await getDataDir();
224
+ if (dir) return dir;
225
+ const { server } = await createServer("My RAG server");
226
+ return serverDir(server.id);
227
+ }
228
+ function nextPort(ws) {
229
+ const ports = ws.servers.map((s) => s.port);
230
+ return Math.max(BASE_PORT - 1, ...ports) + 1;
231
+ }
232
+ function defaultConfigFor(id, name) {
233
+ return {
234
+ ...DEFAULT_CONFIG,
235
+ projectName: name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "my-rag",
236
+ storeConfig: { mode: "local", dbPath: relServerPath(id, "lancedb") },
237
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
238
+ };
239
+ }
240
+ function createServer(name) {
241
+ return serialize(async () => {
242
+ const ws = await getWorkspace();
243
+ const id = randomUUID();
244
+ const now = (/* @__PURE__ */ new Date()).toISOString();
245
+ const meta = {
246
+ id,
247
+ name: name.trim() || "Untitled server",
248
+ port: nextPort(ws),
249
+ createdAt: now,
250
+ updatedAt: now
251
+ };
252
+ const dir = serverDir(id);
253
+ await fs.mkdir(dir, { recursive: true });
254
+ await fs.writeFile(
255
+ path.join(dir, "config.json"),
256
+ JSON.stringify(defaultConfigFor(id, meta.name), null, 2),
257
+ "utf8"
258
+ );
259
+ const next = {
260
+ ...ws,
261
+ servers: [...ws.servers, meta],
262
+ activeServerId: id
263
+ };
264
+ await writeRaw(next);
265
+ return { workspace: next, server: meta };
266
+ });
267
+ }
268
+ function setActiveServer(id) {
269
+ return serialize(async () => {
270
+ const ws = await getWorkspace();
271
+ if (!ws.servers.some((s) => s.id === id)) return ws;
272
+ return writeRaw({ ...ws, activeServerId: id });
273
+ });
274
+ }
275
+
276
+ // src/commands/init.ts
277
+ async function initCommand(name) {
278
+ const projectName = name ?? "my-rag";
279
+ const { server } = await createServer(projectName);
280
+ log.success(`Created server ${log.fmt.bold(server.name)}`);
281
+ log.dim(` id ${server.id}`);
282
+ log.dim(` port ${server.port} (the generated server listens here)`);
283
+ log.dim(" it is now the active server");
284
+ }
285
+
286
+ // src/commands/servers.ts
287
+ async function listServersCommand() {
288
+ const ws = await getWorkspace();
289
+ if (ws.servers.length === 0) {
290
+ log.dim('No servers yet. Create one with: larkup init "my-rag"');
291
+ return;
292
+ }
293
+ log.bold("Servers");
294
+ for (const s of ws.servers) {
295
+ const active = s.id === ws.activeServerId;
296
+ const marker = active ? log.fmt.green("\u25CF") : log.fmt.dim("\u25CB");
297
+ log.info(`${marker} ${log.fmt.bold(s.name)} ${log.fmt.dim(`:${s.port}`)} ${log.fmt.dim(s.id)}`);
298
+ }
299
+ }
300
+ async function useServerCommand(target) {
301
+ const ws = await getWorkspace();
302
+ const next = ws.servers.find((s) => s.id === target) ?? ws.servers.find((s) => s.name === target);
303
+ if (!next) {
304
+ log.error(`No server matching "${target}".`);
305
+ return;
306
+ }
307
+ await setActiveServer(next.id);
308
+ log.success(`Active server is now ${log.fmt.bold(next.name)}`);
309
+ }
310
+
311
+ // ../../packages/core/src/config-store.ts
312
+ import { promises as fs2 } from "fs";
313
+ import path2 from "path";
314
+ async function readConfig() {
315
+ const dir = await getDataDir();
316
+ if (!dir) return DEFAULT_CONFIG;
317
+ try {
318
+ const raw = await fs2.readFile(path2.join(dir, "config.json"), "utf8");
319
+ const parsed = JSON.parse(raw);
320
+ let migratedEmbeddings = parsed.customEmbeddings;
321
+ let migratedEmbeddingId = parsed.embeddingModelId;
322
+ if (parsed.customEmbedding && !migratedEmbeddings?.length) {
323
+ const legacy = parsed.customEmbedding;
324
+ migratedEmbeddings = [legacy];
325
+ if (migratedEmbeddingId === "custom") {
326
+ migratedEmbeddingId = `custom:${legacy.modelName}`;
327
+ }
328
+ }
329
+ const result = {
330
+ ...DEFAULT_CONFIG,
331
+ ...parsed,
332
+ embeddingModelId: migratedEmbeddingId ?? DEFAULT_CONFIG.embeddingModelId,
333
+ customEmbeddings: migratedEmbeddings,
334
+ chunking: { ...DEFAULT_CONFIG.chunking, ...parsed.chunking },
335
+ storeConfig: { ...parsed.storeConfig }
336
+ };
337
+ delete result["customEmbedding"];
338
+ return result;
339
+ } catch {
340
+ return DEFAULT_CONFIG;
341
+ }
342
+ }
343
+ async function writeConfig(config) {
344
+ const dir = await requireDataDir();
345
+ const next = { ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
346
+ await fs2.writeFile(
347
+ path2.join(dir, "config.json"),
348
+ JSON.stringify(next, null, 2),
349
+ "utf8"
350
+ );
351
+ return next;
352
+ }
353
+
354
+ // ../../packages/core/src/documents-store.ts
355
+ import { promises as fs3 } from "fs";
356
+ import path3 from "path";
357
+ import { randomUUID as randomUUID2 } from "crypto";
358
+ var writeChain2 = Promise.resolve();
359
+ function serialize2(fn) {
360
+ const run = writeChain2.then(fn, fn);
361
+ writeChain2 = run.catch(() => {
362
+ });
363
+ return run;
364
+ }
365
+ async function docsPath(create) {
366
+ const dir = create ? await requireDataDir() : await getDataDir();
367
+ if (!dir) return null;
368
+ return path3.join(dir, "documents.json");
369
+ }
370
+ async function readDocuments() {
371
+ const file = await docsPath(false);
372
+ if (!file) return [];
373
+ try {
374
+ const raw = await fs3.readFile(file, "utf8");
375
+ return JSON.parse(raw);
376
+ } catch {
377
+ return [];
378
+ }
379
+ }
380
+ async function writeAll(docs) {
381
+ const file = await docsPath(true);
382
+ if (!file) return;
383
+ await fs3.writeFile(file, JSON.stringify(docs, null, 2), "utf8");
384
+ }
385
+ function normalize(input) {
386
+ const content = input.content.trim();
387
+ return {
388
+ id: randomUUID2(),
389
+ title: input.title.trim() || "Untitled",
390
+ url: input.url,
391
+ source: input.source,
392
+ content,
393
+ charCount: content.length,
394
+ jobId: input.jobId,
395
+ metadata: input.metadata,
396
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
397
+ };
398
+ }
399
+ function addDocument(input) {
400
+ return serialize2(async () => {
401
+ const docs = await readDocuments();
402
+ const doc = normalize(input);
403
+ docs.push(doc);
404
+ await writeAll(docs);
405
+ return doc;
406
+ });
407
+ }
408
+ async function corpusStats() {
409
+ const docs = await readDocuments();
410
+ const chars = docs.reduce((sum, d) => sum + d.charCount, 0);
411
+ return {
412
+ docCount: docs.length,
413
+ charCount: chars,
414
+ bySource: docs.reduce((acc, d) => {
415
+ acc[d.source] = (acc[d.source] ?? 0) + 1;
416
+ return acc;
417
+ }, {})
418
+ };
419
+ }
420
+
421
+ // ../../packages/core/src/index-store.ts
422
+ import { promises as fs4 } from "fs";
423
+ import path4 from "path";
424
+ var writeChain3 = Promise.resolve();
425
+ function serialize3(fn) {
426
+ const run = writeChain3.then(fn, fn);
427
+ writeChain3 = run.catch(() => {
428
+ });
429
+ return run;
430
+ }
431
+ async function runPath(create) {
432
+ const dir = create ? await requireDataDir() : await getDataDir();
433
+ if (!dir) return null;
434
+ return path4.join(dir, "index-run.json");
435
+ }
436
+ async function readRun() {
437
+ const file = await runPath(false);
438
+ if (!file) return null;
439
+ try {
440
+ const raw = await fs4.readFile(file, "utf8");
441
+ return JSON.parse(raw);
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ function writeRun(run) {
447
+ return serialize3(async () => {
448
+ const file = await runPath(true);
449
+ const next = { ...run, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
450
+ if (file) await fs4.writeFile(file, JSON.stringify(next, null, 2), "utf8");
451
+ return next;
452
+ });
453
+ }
454
+ function patchRun(patch) {
455
+ return serialize3(async () => {
456
+ const current = await readRun();
457
+ if (!current) return null;
458
+ const next = {
459
+ ...current,
460
+ ...patch,
461
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
462
+ };
463
+ const file = await runPath(true);
464
+ if (file) await fs4.writeFile(file, JSON.stringify(next, null, 2), "utf8");
465
+ return next;
466
+ });
467
+ }
468
+
469
+ // ../../packages/vector-stores/src/registry.ts
470
+ var VECTOR_STORES = {
471
+ lancedb: {
472
+ id: "lancedb",
473
+ label: "LanceDB",
474
+ description: "Embedded, file-based vector DB. Runs fully local for dev or against LanceDB Cloud for production.",
475
+ runtime: "both",
476
+ installStatus: "installed",
477
+ docsUrl: "https://lancedb.github.io/lancedb/",
478
+ serverDependencies: {
479
+ "@lancedb/lancedb": "^0.30.0"
480
+ },
481
+ fields: [
482
+ {
483
+ key: "mode",
484
+ label: "Mode",
485
+ type: "select",
486
+ required: true,
487
+ defaultValue: "local",
488
+ help: "Local writes to disk on the toolkit host. Cloud connects to LanceDB Cloud.",
489
+ options: [
490
+ { label: "Local (on-disk)", value: "local" },
491
+ { label: "Cloud", value: "cloud" }
492
+ ]
493
+ },
494
+ {
495
+ key: "dbPath",
496
+ label: "Database path",
497
+ type: "path",
498
+ required: true,
499
+ defaultValue: "./.larkup/lancedb",
500
+ placeholder: "./.larkup/lancedb",
501
+ help: "Directory where the LanceDB tables are stored.",
502
+ showWhen: { key: "mode", equals: ["local"] }
503
+ },
504
+ {
505
+ key: "uri",
506
+ label: "Cloud URI",
507
+ type: "text",
508
+ required: true,
509
+ placeholder: "db://my-database",
510
+ help: "Your LanceDB Cloud database URI.",
511
+ showWhen: { key: "mode", equals: ["cloud"] }
512
+ },
513
+ {
514
+ key: "apiKey",
515
+ label: "API key",
516
+ type: "password",
517
+ required: true,
518
+ secret: true,
519
+ placeholder: "sk_...",
520
+ help: "LanceDB Cloud API key. Stored as an env var in the generated server.",
521
+ showWhen: { key: "mode", equals: ["cloud"] }
522
+ },
523
+ {
524
+ key: "tableName",
525
+ label: "Table name",
526
+ type: "text",
527
+ required: true,
528
+ defaultValue: "documents",
529
+ help: "The table that holds your embedded chunks."
530
+ }
531
+ ]
532
+ },
533
+ pinecone: {
534
+ id: "pinecone",
535
+ label: "Pinecone",
536
+ description: "Fully-managed cloud vector database. Cloud-only \u2014 requires an API key and an existing index.",
537
+ runtime: "cloud",
538
+ installStatus: "installed",
539
+ docsUrl: "https://docs.pinecone.io/",
540
+ serverDependencies: {
541
+ "@pinecone-database/pinecone": "^6.0.0"
542
+ },
543
+ fields: [
544
+ {
545
+ key: "apiKey",
546
+ label: "API key",
547
+ type: "password",
548
+ required: true,
549
+ secret: true,
550
+ placeholder: "pcsk_...",
551
+ help: "Pinecone API key. Stored as an env var in the generated server."
552
+ },
553
+ {
554
+ key: "indexName",
555
+ label: "Index name",
556
+ type: "text",
557
+ required: true,
558
+ placeholder: "rag-index",
559
+ help: "Name of the Pinecone index to upsert into / query. For hybrid/lexical mode this index must use the 'dotproduct' metric."
560
+ },
561
+ {
562
+ key: "namespace",
563
+ label: "Namespace",
564
+ type: "text",
565
+ required: false,
566
+ placeholder: "default",
567
+ help: "Optional namespace to isolate this dataset within the index."
568
+ },
569
+ {
570
+ key: "sparseModel",
571
+ label: "Sparse model",
572
+ type: "text",
573
+ required: true,
574
+ defaultValue: "pinecone-sparse-english-v0",
575
+ help: "Pinecone-hosted sparse model used to generate keyword vectors via the Inference API. Both dense and sparse vectors are stored in the same index.",
576
+ showWhenIndexType: ["lexical", "hybrid"]
577
+ }
578
+ ]
579
+ },
580
+ weaviate: {
581
+ id: "weaviate",
582
+ label: "Weaviate",
583
+ description: "Open-source AI-native vector database with built-in hybrid search, BM25, and multi-tenancy.",
584
+ runtime: "both",
585
+ installStatus: "coming-soon",
586
+ docsUrl: "https://weaviate.io/developers/weaviate",
587
+ serverDependencies: {
588
+ weaviate: "^3.0.0"
589
+ },
590
+ fields: [
591
+ {
592
+ key: "host",
593
+ label: "Host URL",
594
+ type: "text",
595
+ required: true,
596
+ placeholder: "https://my-cluster.weaviate.network",
597
+ help: "Weaviate Cloud URL or self-hosted host address."
598
+ },
599
+ {
600
+ key: "apiKey",
601
+ label: "API key",
602
+ type: "password",
603
+ required: false,
604
+ secret: true,
605
+ placeholder: "weaviate-api-key",
606
+ help: "Weaviate Cloud API key (optional for self-hosted)."
607
+ },
608
+ {
609
+ key: "className",
610
+ label: "Collection name",
611
+ type: "text",
612
+ required: true,
613
+ defaultValue: "Documents",
614
+ help: "The Weaviate collection (class) that holds your embedded chunks."
615
+ }
616
+ ]
617
+ },
618
+ qdrant: {
619
+ id: "qdrant",
620
+ label: "Qdrant",
621
+ description: "High-performance vector search engine with rich filtering, built for production-scale RAG.",
622
+ runtime: "both",
623
+ installStatus: "coming-soon",
624
+ docsUrl: "https://qdrant.tech/documentation/",
625
+ serverDependencies: {
626
+ "@qdrant/js-client-rest": "^1.9.0"
627
+ },
628
+ fields: [
629
+ {
630
+ key: "url",
631
+ label: "Host URL",
632
+ type: "text",
633
+ required: true,
634
+ placeholder: "http://localhost:6333",
635
+ help: "Qdrant server URL (local or Qdrant Cloud endpoint)."
636
+ },
637
+ {
638
+ key: "apiKey",
639
+ label: "API key",
640
+ type: "password",
641
+ required: false,
642
+ secret: true,
643
+ placeholder: "qdrant-api-key",
644
+ help: "Qdrant Cloud API key (leave blank for local)."
645
+ },
646
+ {
647
+ key: "collectionName",
648
+ label: "Collection name",
649
+ type: "text",
650
+ required: true,
651
+ defaultValue: "documents",
652
+ help: "Qdrant collection to upsert into and query."
653
+ }
654
+ ]
655
+ },
656
+ chroma: {
657
+ id: "chroma",
658
+ label: "Chroma",
659
+ description: "Open-source, developer-friendly embedding database. Great for rapid local prototyping.",
660
+ runtime: "both",
661
+ installStatus: "installable",
662
+ docsUrl: "https://docs.trychroma.com/",
663
+ serverDependencies: {
664
+ chromadb: "^1.9.0"
665
+ },
666
+ fields: [
667
+ {
668
+ key: "mode",
669
+ label: "Mode",
670
+ type: "select",
671
+ required: true,
672
+ defaultValue: "server",
673
+ help: "Server connects to a self-hosted Chroma instance. Cloud connects to Chroma Cloud.",
674
+ options: [
675
+ { label: "Server (Self-hosted)", value: "server" },
676
+ { label: "Cloud (Chroma Cloud)", value: "cloud" }
677
+ ]
678
+ },
679
+ {
680
+ key: "host",
681
+ label: "Server URL",
682
+ type: "text",
683
+ required: true,
684
+ placeholder: "http://localhost:8000",
685
+ defaultValue: "http://localhost:8000",
686
+ help: "Chroma server URL (local Docker or remote).",
687
+ showWhen: { key: "mode", equals: ["server"] }
688
+ },
689
+ {
690
+ key: "authToken",
691
+ label: "Auth Token",
692
+ type: "password",
693
+ required: false,
694
+ secret: true,
695
+ placeholder: "chroma-token",
696
+ help: "Optional static auth token for Chroma server.",
697
+ showWhen: { key: "mode", equals: ["server"] }
698
+ },
699
+ {
700
+ key: "apiKey",
701
+ label: "API Key",
702
+ type: "password",
703
+ required: true,
704
+ secret: true,
705
+ placeholder: "sk_...",
706
+ help: "Chroma Cloud API Key.",
707
+ showWhen: { key: "mode", equals: ["cloud"] }
708
+ },
709
+ {
710
+ key: "tenant",
711
+ label: "Tenant",
712
+ type: "text",
713
+ required: true,
714
+ defaultValue: "default_tenant",
715
+ help: "Tenant name for Chroma Cloud.",
716
+ showWhen: { key: "mode", equals: ["cloud"] }
717
+ },
718
+ {
719
+ key: "database",
720
+ label: "Database",
721
+ type: "text",
722
+ required: true,
723
+ defaultValue: "default_database",
724
+ help: "Database name for Chroma Cloud.",
725
+ showWhen: { key: "mode", equals: ["cloud"] }
726
+ },
727
+ {
728
+ key: "collectionName",
729
+ label: "Collection name",
730
+ type: "text",
731
+ required: true,
732
+ defaultValue: "documents",
733
+ help: "Chroma collection to store and retrieve embeddings."
734
+ }
735
+ ]
736
+ },
737
+ pgvector: {
738
+ id: "pgvector",
739
+ label: "pgvector",
740
+ description: "PostgreSQL extension for vector similarity search. Self-host vectors alongside your relational data.",
741
+ runtime: "both",
742
+ installStatus: "coming-soon",
743
+ docsUrl: "https://github.com/pgvector/pgvector",
744
+ serverDependencies: {
745
+ pg: "^8.11.0",
746
+ pgvector: "^0.2.0"
747
+ },
748
+ fields: [
749
+ {
750
+ key: "connectionString",
751
+ label: "Connection string",
752
+ type: "password",
753
+ required: true,
754
+ secret: true,
755
+ placeholder: "postgresql://user:pass@localhost:5432/db",
756
+ help: "Full PostgreSQL connection string."
757
+ },
758
+ {
759
+ key: "tableName",
760
+ label: "Table name",
761
+ type: "text",
762
+ required: true,
763
+ defaultValue: "embeddings",
764
+ help: "The table used to store vector embeddings. Created automatically if absent."
765
+ }
766
+ ]
767
+ },
768
+ supabase: {
769
+ id: "supabase",
770
+ label: "Supabase",
771
+ description: "Postgres-backed vector store via the pgvector extension with Supabase's managed infrastructure.",
772
+ runtime: "cloud",
773
+ installStatus: "coming-soon",
774
+ docsUrl: "https://supabase.com/docs/guides/ai",
775
+ serverDependencies: {
776
+ "@supabase/supabase-js": "^2.43.0"
777
+ },
778
+ fields: [
779
+ {
780
+ key: "url",
781
+ label: "Project URL",
782
+ type: "text",
783
+ required: true,
784
+ placeholder: "https://xyz.supabase.co",
785
+ help: "Your Supabase project URL."
786
+ },
787
+ {
788
+ key: "serviceKey",
789
+ label: "Service role key",
790
+ type: "password",
791
+ required: true,
792
+ secret: true,
793
+ placeholder: "eyJhbGci...",
794
+ help: "Supabase service_role key (not the anon key). Stored as an env var."
795
+ },
796
+ {
797
+ key: "tableName",
798
+ label: "Table name",
799
+ type: "text",
800
+ required: true,
801
+ defaultValue: "documents",
802
+ help: "Supabase table with a vector column for embeddings."
803
+ }
804
+ ]
805
+ }
806
+ };
807
+ var VECTOR_STORE_LIST = Object.values(VECTOR_STORES);
808
+ function getVectorStore(id) {
809
+ return VECTOR_STORES[id];
810
+ }
811
+
812
+ // ../../packages/core/src/embeddings/registry.ts
813
+ var EMBEDDING_MODELS = [
814
+ // ── OpenAI ──────────────────────────────────────────────────────────
815
+ {
816
+ id: "openai/text-embedding-3-small",
817
+ label: "text-embedding-3-small",
818
+ provider: "openai",
819
+ dimensions: 1536,
820
+ maxInputTokens: 8191,
821
+ description: "Fast, cheap, strong general-purpose default."
822
+ },
823
+ {
824
+ id: "openai/text-embedding-3-large",
825
+ label: "text-embedding-3-large",
826
+ provider: "openai",
827
+ dimensions: 3072,
828
+ maxInputTokens: 8191,
829
+ description: "Highest quality OpenAI embeddings, larger vectors."
830
+ },
831
+ {
832
+ id: "openai/text-embedding-ada-002",
833
+ label: "text-embedding-ada-002",
834
+ provider: "openai",
835
+ dimensions: 1536,
836
+ maxInputTokens: 8191,
837
+ description: "Legacy model \u2014 kept for backward compatibility."
838
+ },
839
+ // ── Google ───────────────────────────────────────────────────────────
840
+ {
841
+ id: "google/text-embedding-004",
842
+ label: "text-embedding-004",
843
+ provider: "google",
844
+ dimensions: 768,
845
+ maxInputTokens: 2048,
846
+ description: "Compact, high-quality embeddings from Google."
847
+ },
848
+ {
849
+ id: "google/gemini-embedding-exp-03-07",
850
+ label: "gemini-embedding-exp-03-07",
851
+ provider: "google",
852
+ dimensions: 3072,
853
+ maxInputTokens: 8192,
854
+ description: "State-of-the-art Gemini embeddings with flexible dimensions."
855
+ },
856
+ // ── Cohere ───────────────────────────────────────────────────────────
857
+ {
858
+ id: "cohere/embed-english-v3.0",
859
+ label: "embed-english-v3.0",
860
+ provider: "cohere",
861
+ dimensions: 1024,
862
+ maxInputTokens: 512,
863
+ description: "Tuned for retrieval; great for English-heavy corpora."
864
+ },
865
+ {
866
+ id: "cohere/embed-multilingual-v3.0",
867
+ label: "embed-multilingual-v3.0",
868
+ provider: "cohere",
869
+ dimensions: 1024,
870
+ maxInputTokens: 512,
871
+ description: "100+ language multilingual retrieval embeddings."
872
+ },
873
+ // ── Voyage AI ────────────────────────────────────────────────────────
874
+ {
875
+ id: "voyage/voyage-3",
876
+ label: "voyage-3",
877
+ provider: "voyage",
878
+ dimensions: 1024,
879
+ maxInputTokens: 32e3,
880
+ description: "Voyage's best general-purpose retrieval model."
881
+ },
882
+ {
883
+ id: "voyage/voyage-3-lite",
884
+ label: "voyage-3-lite",
885
+ provider: "voyage",
886
+ dimensions: 512,
887
+ maxInputTokens: 32e3,
888
+ description: "Optimized for latency and cost with strong performance."
889
+ },
890
+ {
891
+ id: "voyage/voyage-code-3",
892
+ label: "voyage-code-3",
893
+ provider: "voyage",
894
+ dimensions: 1024,
895
+ maxInputTokens: 32e3,
896
+ description: "Specialized for code retrieval tasks."
897
+ },
898
+ // ── Mistral ──────────────────────────────────────────────────────────
899
+ {
900
+ id: "mistral/mistral-embed",
901
+ label: "mistral-embed",
902
+ provider: "mistral",
903
+ dimensions: 1024,
904
+ maxInputTokens: 8192,
905
+ description: "Mistral's efficient embedding model for retrieval."
906
+ },
907
+ // ── Jina AI ──────────────────────────────────────────────────────────
908
+ {
909
+ id: "jina/jina-embeddings-v3",
910
+ label: "jina-embeddings-v3",
911
+ provider: "jina",
912
+ dimensions: 1024,
913
+ maxInputTokens: 8192,
914
+ description: "Multilingual, task-specific embeddings from Jina AI."
915
+ },
916
+ {
917
+ id: "jina/jina-clip-v2",
918
+ label: "jina-clip-v2",
919
+ provider: "jina",
920
+ dimensions: 1024,
921
+ maxInputTokens: 8192,
922
+ description: "Multimodal embeddings for text and images."
923
+ },
924
+ // ── Nomic ────────────────────────────────────────────────────────────
925
+ {
926
+ id: "nomic/nomic-embed-text-v1.5",
927
+ label: "nomic-embed-text-v1.5",
928
+ provider: "nomic",
929
+ dimensions: 768,
930
+ maxInputTokens: 8192,
931
+ description: "Open-source, highly capable text embeddings."
932
+ }
933
+ ];
934
+ function getEmbeddingModel(id) {
935
+ return EMBEDDING_MODELS.find((m) => m.id === id);
936
+ }
937
+
938
+ // src/lib/scope.ts
939
+ async function inServerScope(serverId, fn) {
940
+ if (serverId) {
941
+ const found = await runWithServer(serverId, () => getActiveServer());
942
+ if (!found) log.error(`No server with id "${serverId}".`);
943
+ return runWithServer(serverId, fn);
944
+ }
945
+ return fn();
946
+ }
947
+ async function requireActive() {
948
+ const server = await getActiveServer();
949
+ if (!server) {
950
+ log.error('No active server. Create one first with: larkup init "my-rag"');
951
+ }
952
+ return server;
953
+ }
954
+
955
+ // src/commands/config.ts
956
+ async function configCommand(options) {
957
+ await inServerScope(options.server, async () => {
958
+ const server = await requireActive();
959
+ const config = await readConfig();
960
+ const stats = await corpusStats();
961
+ const run = await readRun();
962
+ const store = getVectorStore(config.vectorStore);
963
+ const model = getEmbeddingModel(config.embeddingModelId);
964
+ log.bold(server.name);
965
+ log.info(` project ${config.projectName}`);
966
+ log.info(` model ${model?.label ?? config.embeddingModelId}`);
967
+ log.info(` store ${store?.label ?? config.vectorStore}`);
968
+ log.info(` index ${config.indexType}`);
969
+ log.info(` chunking ${config.chunking.chunkSize}/${config.chunking.chunkOverlap} (${config.chunking.strategy})`);
970
+ log.info(` top-k ${config.topK}`);
971
+ log.info(` corpus ${stats.docCount} docs \xB7 ${stats.charCount.toLocaleString()} chars`);
972
+ log.info(` indexed ${run?.status === "completed" ? `yes (${run.totalChunks} chunks)` : "no"}`);
973
+ });
974
+ }
975
+
976
+ // src/commands/add-doc.ts
977
+ import { promises as fs5 } from "fs";
978
+ import path5 from "path";
979
+ async function addDocCommand(options) {
980
+ await inServerScope(options.server, async () => {
981
+ await requireActive();
982
+ let content;
983
+ let title = options.title;
984
+ if (options.file) {
985
+ const abs = path5.isAbsolute(options.file) ? options.file : path5.join(process.cwd(), options.file);
986
+ try {
987
+ content = await fs5.readFile(abs, "utf8");
988
+ } catch {
989
+ log.error(`Could not read file: ${abs}`);
990
+ }
991
+ if (!title) title = path5.basename(options.file);
992
+ } else if (options.text) {
993
+ content = options.text;
994
+ } else {
995
+ log.error("Provide --file <path> or --text <string>.");
996
+ }
997
+ if (!content.trim()) log.error("The document content is empty.");
998
+ const doc = await addDocument({
999
+ title: title ?? "Untitled",
1000
+ content,
1001
+ source: options.file ? "upload" : "paste",
1002
+ url: options.url
1003
+ });
1004
+ log.success(`Added "${doc.title}" (${doc.charCount.toLocaleString()} chars)`);
1005
+ const stats = await corpusStats();
1006
+ log.dim(` corpus now has ${stats.docCount} document(s)`);
1007
+ });
1008
+ }
1009
+
1010
+ // ../../packages/core/src/indexing/indexer.ts
1011
+ import { randomUUID as randomUUID3 } from "crypto";
1012
+
1013
+ // ../../packages/core/src/indexing/chunker.ts
1014
+ var CHARS_PER_TOKEN = 4;
1015
+ function tokensToChars(tokens) {
1016
+ return Math.max(1, Math.round(tokens * CHARS_PER_TOKEN));
1017
+ }
1018
+ var RECURSIVE_SEPARATORS = ["\n\n", "\n", ". ", "? ", "! ", " "];
1019
+ function splitRecursive(text4, maxChars) {
1020
+ if (text4.length <= maxChars) return [text4];
1021
+ for (const sep of RECURSIVE_SEPARATORS) {
1022
+ if (!text4.includes(sep)) continue;
1023
+ const parts = text4.split(sep);
1024
+ const out = [];
1025
+ let buf = "";
1026
+ for (const part of parts) {
1027
+ const piece = buf ? buf + sep + part : part;
1028
+ if (piece.length <= maxChars) {
1029
+ buf = piece;
1030
+ } else {
1031
+ if (buf) out.push(buf);
1032
+ if (part.length > maxChars) out.push(...splitRecursive(part, maxChars));
1033
+ else buf = part;
1034
+ }
1035
+ }
1036
+ if (buf) out.push(buf);
1037
+ if (out.length > 1) return out;
1038
+ }
1039
+ return hardSlice(text4, maxChars);
1040
+ }
1041
+ function splitSentences(text4, maxChars) {
1042
+ const sentences = text4.match(/[^.!?]+[.!?]+[\s]*|[^.!?]+$/g) ?? [text4];
1043
+ const out = [];
1044
+ let buf = "";
1045
+ for (const s of sentences) {
1046
+ const piece = buf + s;
1047
+ if (piece.length <= maxChars) buf = piece;
1048
+ else {
1049
+ if (buf) out.push(buf.trim());
1050
+ buf = s.length > maxChars ? "" : s;
1051
+ if (s.length > maxChars) out.push(...hardSlice(s, maxChars));
1052
+ }
1053
+ }
1054
+ if (buf.trim()) out.push(buf.trim());
1055
+ return out;
1056
+ }
1057
+ function hardSlice(text4, maxChars) {
1058
+ const out = [];
1059
+ for (let i = 0; i < text4.length; i += maxChars) {
1060
+ out.push(text4.slice(i, i + maxChars));
1061
+ }
1062
+ return out;
1063
+ }
1064
+ function applyOverlap(chunks, overlapChars) {
1065
+ if (overlapChars <= 0 || chunks.length <= 1) return chunks;
1066
+ return chunks.map((chunk, i) => {
1067
+ if (i === 0) return chunk;
1068
+ const tail = chunks[i - 1].slice(-overlapChars);
1069
+ return `${tail} ${chunk}`.trim();
1070
+ });
1071
+ }
1072
+ function chunkDocument(doc, params) {
1073
+ const maxChars = tokensToChars(params.chunkSize);
1074
+ const overlapChars = tokensToChars(params.chunkOverlap);
1075
+ const text4 = doc.content.trim();
1076
+ if (!text4) return [];
1077
+ let raw;
1078
+ switch (params.strategy) {
1079
+ case "sentence":
1080
+ raw = splitSentences(text4, maxChars);
1081
+ break;
1082
+ case "fixed":
1083
+ raw = hardSlice(text4, maxChars);
1084
+ break;
1085
+ case "recursive":
1086
+ default:
1087
+ raw = splitRecursive(text4, maxChars);
1088
+ break;
1089
+ }
1090
+ const withOverlap = applyOverlap(
1091
+ raw.map((c) => c.trim()).filter(Boolean),
1092
+ overlapChars
1093
+ );
1094
+ return withOverlap.map((textChunk, index) => ({
1095
+ id: `${doc.id}#${index}`,
1096
+ documentId: doc.id,
1097
+ index,
1098
+ text: textChunk,
1099
+ title: doc.title,
1100
+ url: doc.url,
1101
+ source: doc.source,
1102
+ charCount: textChunk.length,
1103
+ metadata: doc.metadata
1104
+ }));
1105
+ }
1106
+ function chunkCorpus(docs, params) {
1107
+ return docs.flatMap((d) => chunkDocument(d, params));
1108
+ }
1109
+
1110
+ // ../../packages/core/src/indexing/embedder.ts
1111
+ import { embed, embedMany } from "ai";
1112
+
1113
+ // ../../packages/core/src/embeddings/providers.ts
1114
+ import { createOpenAI } from "@ai-sdk/openai";
1115
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1116
+ import { createDeepSeek } from "@ai-sdk/deepseek";
1117
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
1118
+ import { createCohere } from "@ai-sdk/cohere";
1119
+ import { createMistral } from "@ai-sdk/mistral";
1120
+ import { createGateway } from "@ai-sdk/gateway";
1121
+ function getAIModel(config) {
1122
+ let modelName = config.embeddingModelId;
1123
+ if (modelName.includes("/")) {
1124
+ modelName = modelName.split("/")[1];
1125
+ }
1126
+ if (config.embeddingModelId.startsWith("custom:")) {
1127
+ const customName = config.embeddingModelId.slice("custom:".length);
1128
+ const custom = (config.customEmbeddings ?? []).find(
1129
+ (m) => m.modelName === customName
1130
+ );
1131
+ if (custom) {
1132
+ const customProvider = createOpenAICompatible({
1133
+ name: "custom_provider",
1134
+ baseURL: custom.baseUrl,
1135
+ apiKey: custom.apiKey || config.embeddingApiKey || void 0
1136
+ });
1137
+ return customProvider.embeddingModel(custom.modelName);
1138
+ }
1139
+ }
1140
+ if (config.embeddingProvider === "deepseek") {
1141
+ const deepseek = createDeepSeek({
1142
+ apiKey: config.embeddingApiKey || void 0
1143
+ });
1144
+ return deepseek.embeddingModel(modelName);
1145
+ }
1146
+ if (config.embeddingProvider === "google") {
1147
+ const google = createGoogleGenerativeAI({
1148
+ apiKey: config.embeddingApiKey || void 0
1149
+ });
1150
+ return google.embedding(modelName);
1151
+ }
1152
+ if (config.embeddingProvider === "cohere") {
1153
+ const cohere = createCohere({
1154
+ apiKey: config.embeddingApiKey || void 0
1155
+ });
1156
+ return cohere.embedding(modelName);
1157
+ }
1158
+ if (config.embeddingProvider === "mistral") {
1159
+ const mistral = createMistral({
1160
+ apiKey: config.embeddingApiKey || void 0
1161
+ });
1162
+ return mistral.embedding(modelName);
1163
+ }
1164
+ if (config.embeddingProvider === "vercel_ai_gateway") {
1165
+ const gateway = createGateway({
1166
+ apiKey: config.embeddingApiKey || void 0
1167
+ });
1168
+ return gateway.embedding(config.embeddingModelId);
1169
+ }
1170
+ const openai = createOpenAI({
1171
+ apiKey: config.embeddingApiKey || void 0
1172
+ });
1173
+ return openai.embedding(modelName);
1174
+ }
1175
+
1176
+ // ../../packages/core/src/indexing/embedder.ts
1177
+ async function embedTexts(config, texts) {
1178
+ if (texts.length === 0) return { embeddings: [], dimensions: 0 };
1179
+ const model = getAIModel(config);
1180
+ const { embeddings } = await embedMany({
1181
+ model,
1182
+ values: texts
1183
+ });
1184
+ return {
1185
+ embeddings,
1186
+ dimensions: embeddings[0]?.length ?? 0
1187
+ };
1188
+ }
1189
+ async function embedQuery(config, text4) {
1190
+ const model = getAIModel(config);
1191
+ const { embedding } = await embed({ model, value: text4 });
1192
+ return embedding;
1193
+ }
1194
+ function expectedDimensions(config) {
1195
+ if (config.embeddingModelId.startsWith("custom:")) {
1196
+ const customName = config.embeddingModelId.slice("custom:".length);
1197
+ const custom = (config.customEmbeddings ?? []).find(
1198
+ (m) => m.modelName === customName
1199
+ );
1200
+ if (custom) return custom.dimensions;
1201
+ }
1202
+ return getEmbeddingModel(config.embeddingModelId)?.dimensions ?? 0;
1203
+ }
1204
+
1205
+ // ../../packages/vector-stores/src/factory.ts
1206
+ async function createAdapter(config, onRateLimit) {
1207
+ switch (config.vectorStore) {
1208
+ case "pinecone": {
1209
+ const { PineconeAdapter } = await import("./pinecone-YZHFLCKI.js");
1210
+ return new PineconeAdapter({
1211
+ apiKey: config.storeConfig.apiKey,
1212
+ indexName: config.storeConfig.indexName,
1213
+ namespace: config.storeConfig.namespace,
1214
+ sparseModel: config.storeConfig.sparseModel,
1215
+ indexType: config.indexType,
1216
+ onRateLimit
1217
+ });
1218
+ }
1219
+ case "chroma": {
1220
+ const { ChromaAdapter } = await import("./chroma-GIDEKUIH.js");
1221
+ return new ChromaAdapter({
1222
+ mode: config.storeConfig.mode,
1223
+ host: config.storeConfig.host,
1224
+ authToken: config.storeConfig.authToken,
1225
+ apiKey: config.storeConfig.apiKey,
1226
+ tenant: config.storeConfig.tenant,
1227
+ database: config.storeConfig.database,
1228
+ collectionName: config.storeConfig.collectionName,
1229
+ indexType: config.indexType
1230
+ });
1231
+ }
1232
+ case "lancedb":
1233
+ default: {
1234
+ const { LanceDBAdapter } = await import("./lancedb-E2JLQERV.js");
1235
+ return new LanceDBAdapter({
1236
+ mode: config.storeConfig.mode,
1237
+ dbPath: config.storeConfig.dbPath,
1238
+ uri: config.storeConfig.uri,
1239
+ apiKey: config.storeConfig.apiKey,
1240
+ tableName: config.storeConfig.tableName
1241
+ });
1242
+ }
1243
+ }
1244
+ }
1245
+
1246
+ // ../../packages/core/src/indexing/indexer.ts
1247
+ var EMBED_BATCH = 64;
1248
+ async function createRun(config) {
1249
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1250
+ const run = {
1251
+ id: randomUUID3(),
1252
+ status: "chunking",
1253
+ embeddingModelId: config.embeddingModelId,
1254
+ vectorStore: config.vectorStore,
1255
+ indexType: config.indexType,
1256
+ totalChunks: 0,
1257
+ processedChunks: 0,
1258
+ docCount: 0,
1259
+ dimensions: expectedDimensions(config),
1260
+ startedAt: now,
1261
+ updatedAt: now
1262
+ };
1263
+ return writeRun(run);
1264
+ }
1265
+ async function runIndexer(runId, config, previousRun = null) {
1266
+ const started = Date.now();
1267
+ try {
1268
+ let docs = await readDocuments();
1269
+ if (previousRun && previousRun.status === "completed") {
1270
+ docs = docs.filter((d) => d.createdAt > previousRun.startedAt);
1271
+ }
1272
+ if (docs.length === 0) {
1273
+ if (previousRun) {
1274
+ await patchRun({
1275
+ status: "completed",
1276
+ docCount: 0,
1277
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1278
+ durationMs: Date.now() - started
1279
+ });
1280
+ } else {
1281
+ await patchRun({
1282
+ status: "failed",
1283
+ error: "The corpus is empty. Load documents before indexing.",
1284
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1285
+ });
1286
+ }
1287
+ return;
1288
+ }
1289
+ await patchRun({ status: "chunking", docCount: docs.length });
1290
+ const chunks = chunkCorpus(docs, config.chunking);
1291
+ if (chunks.length === 0) {
1292
+ if (previousRun) {
1293
+ await patchRun({
1294
+ status: "completed",
1295
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1296
+ durationMs: Date.now() - started
1297
+ });
1298
+ } else {
1299
+ await patchRun({
1300
+ status: "failed",
1301
+ error: "No chunks were produced from the corpus.",
1302
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1303
+ });
1304
+ }
1305
+ return;
1306
+ }
1307
+ await patchRun({ totalChunks: chunks.length, status: "embedding" });
1308
+ const adapter = await createAdapter(config, async (waitSecs, attempt) => {
1309
+ await patchRun({
1310
+ warning: `Sparse model rate-limited \u2014 pausing ${waitSecs}s (retry ${attempt}/${3})\u2026`
1311
+ });
1312
+ });
1313
+ let dimensions = expectedDimensions(config);
1314
+ let processed = 0;
1315
+ let initialized = false;
1316
+ let currentDelayMs = 0;
1317
+ for (let i = 0; i < chunks.length; i += EMBED_BATCH) {
1318
+ const batch = chunks.slice(i, i + EMBED_BATCH);
1319
+ if (currentDelayMs > 0) {
1320
+ await new Promise((r) => setTimeout(r, currentDelayMs));
1321
+ }
1322
+ await patchRun({ status: "embedding" });
1323
+ let attempt = 1;
1324
+ let batchEmbeddings = [];
1325
+ let batchDimensions = dimensions;
1326
+ while (true) {
1327
+ try {
1328
+ const { embeddings, dimensions: dim } = await embedTexts(
1329
+ config,
1330
+ batch.map((c) => c.text)
1331
+ );
1332
+ batchEmbeddings = embeddings;
1333
+ if (dim) batchDimensions = dim;
1334
+ break;
1335
+ } catch (err) {
1336
+ const is429 = err?.status === 429 || err?.statusCode === 429 || String(err?.message ?? "").includes("429") || String(err?.message ?? "").includes("rate limit") || String(err?.message ?? "").toLowerCase().includes("too many requests");
1337
+ if (is429 && attempt <= 3) {
1338
+ const waitSecs = attempt * 10;
1339
+ await patchRun({
1340
+ warning: `Dense model rate-limited \u2014 pausing ${waitSecs}s (retry ${attempt}/${3})\u2026`
1341
+ });
1342
+ await new Promise((r) => setTimeout(r, waitSecs * 1e3));
1343
+ currentDelayMs = Math.max(currentDelayMs, 2e3);
1344
+ attempt++;
1345
+ } else {
1346
+ throw err;
1347
+ }
1348
+ }
1349
+ }
1350
+ dimensions = batchDimensions;
1351
+ if (!initialized) {
1352
+ await adapter.init(dimensions);
1353
+ if (!previousRun) {
1354
+ await adapter.reset();
1355
+ }
1356
+ initialized = true;
1357
+ await patchRun({ dimensions });
1358
+ }
1359
+ const records = batch.map((c, j) => ({
1360
+ id: c.id,
1361
+ vector: batchEmbeddings[j],
1362
+ text: c.text,
1363
+ title: c.title,
1364
+ url: c.url,
1365
+ source: c.source,
1366
+ documentId: c.documentId,
1367
+ chunkIndex: c.index,
1368
+ metadata: c.metadata
1369
+ }));
1370
+ await patchRun({ status: "upserting" });
1371
+ let upsertAttempt = 1;
1372
+ while (true) {
1373
+ try {
1374
+ await adapter.upsert(records);
1375
+ break;
1376
+ } catch (err) {
1377
+ const is429 = err?.status === 429 || err?.statusCode === 429 || String(err?.message ?? "").includes("429") || String(err?.message ?? "").includes("rate limit") || String(err?.message ?? "").toLowerCase().includes("too many requests") || String(err?.message ?? "").includes("RESOURCE_EXHAUSTED");
1378
+ if (is429 && upsertAttempt <= 3) {
1379
+ const waitSecs = upsertAttempt * 10;
1380
+ await patchRun({
1381
+ warning: `Vector store rate-limited \u2014 pausing ${waitSecs}s (retry ${upsertAttempt}/${3})\u2026`
1382
+ });
1383
+ await new Promise((r) => setTimeout(r, waitSecs * 1e3));
1384
+ currentDelayMs = Math.max(currentDelayMs, 2e3);
1385
+ upsertAttempt++;
1386
+ } else {
1387
+ throw err;
1388
+ }
1389
+ }
1390
+ }
1391
+ processed += batch.length;
1392
+ await patchRun({ processedChunks: processed, status: "embedding", warning: void 0 });
1393
+ }
1394
+ await patchRun({
1395
+ status: "completed",
1396
+ processedChunks: processed,
1397
+ dimensions,
1398
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1399
+ durationMs: Date.now() - started
1400
+ });
1401
+ } catch (err) {
1402
+ const message = err instanceof Error ? err.message : "Indexing failed unexpectedly.";
1403
+ await patchRun({
1404
+ status: "failed",
1405
+ error: message,
1406
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1407
+ durationMs: Date.now() - started
1408
+ });
1409
+ }
1410
+ }
1411
+
1412
+ // src/lib/keys.ts
1413
+ import * as p2 from "@clack/prompts";
1414
+ async function ensureApiKey(config, keyType = "embedding") {
1415
+ const envKey = keyType === "embedding" ? process.env.OPENAI_API_KEY : process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
1416
+ const currentKey = keyType === "embedding" ? config.embeddingApiKey : config.chatApiKey || config.embeddingApiKey;
1417
+ if (envKey && !currentKey) {
1418
+ return;
1419
+ }
1420
+ if (!currentKey) {
1421
+ log.warn(`Your ${keyType} API key is not configured.`);
1422
+ const key = await p2.text({
1423
+ message: `Please enter your ${config.embeddingProvider} API key:`,
1424
+ placeholder: "sk-..."
1425
+ });
1426
+ if (p2.isCancel(key)) {
1427
+ log.error("Canceled.");
1428
+ process.exit(1);
1429
+ }
1430
+ if (key && typeof key === "string" && key.trim()) {
1431
+ if (keyType === "embedding") {
1432
+ config.embeddingApiKey = key.trim();
1433
+ } else {
1434
+ config.chatApiKey = key.trim();
1435
+ }
1436
+ await writeConfig(config);
1437
+ log.success(`Saved API key to local configuration.`);
1438
+ } else {
1439
+ log.error("API Key is required to proceed.");
1440
+ process.exit(1);
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ // src/commands/index-cmd.ts
1446
+ async function indexCommand(options) {
1447
+ await inServerScope(options.server, async () => {
1448
+ await requireActive();
1449
+ const config = await readConfig();
1450
+ const docs = await readDocuments();
1451
+ if (docs.length === 0) {
1452
+ log.error("Corpus is empty. Add documents first: larkup add-doc --file <path>");
1453
+ }
1454
+ if (!getEmbeddingModel(config.embeddingModelId)) {
1455
+ log.error(`Unknown embedding model "${config.embeddingModelId}". Set it in the Configure stage.`);
1456
+ }
1457
+ await ensureApiKey(config, "embedding");
1458
+ log.info(log.fmt.cyan(`Indexing ${docs.length} document(s) into ${config.vectorStore}\u2026`));
1459
+ const run = await createRun(config);
1460
+ const s = prompts.spinner();
1461
+ s.start("Starting indexer...");
1462
+ let done = false;
1463
+ const timer = setInterval(async () => {
1464
+ const r = await readRun();
1465
+ if (!r || done) return;
1466
+ if (r.totalChunks > 0) {
1467
+ s.message(`${r.status.padEnd(10)} ${r.processedChunks}/${r.totalChunks} chunks`);
1468
+ } else {
1469
+ s.message(`${r.status}\u2026`);
1470
+ }
1471
+ }, 400);
1472
+ try {
1473
+ await runIndexer(run.id, config);
1474
+ } catch (e) {
1475
+ done = true;
1476
+ clearInterval(timer);
1477
+ s.stop("Indexing failed.");
1478
+ log.error(e.message || "Indexing failed");
1479
+ }
1480
+ done = true;
1481
+ clearInterval(timer);
1482
+ const final = await readRun();
1483
+ if (final?.status === "completed") {
1484
+ s.stop(`Indexed ${final.totalChunks} chunks (${final.dimensions}-dim) in ${((final.durationMs ?? 0) / 1e3).toFixed(1)}s`);
1485
+ log.success("Indexing complete!");
1486
+ } else {
1487
+ s.stop("Indexing failed.");
1488
+ log.error(final?.error ?? "Indexing failed.");
1489
+ }
1490
+ });
1491
+ }
1492
+
1493
+ // src/commands/generate.ts
1494
+ import { promises as fs7 } from "fs";
1495
+ import path7 from "path";
1496
+
1497
+ // ../../packages/core/src/generator/generate-server.ts
1498
+ function lang(path9) {
1499
+ if (path9.endsWith(".json")) return "json";
1500
+ if (path9.endsWith(".mjs") || path9.endsWith(".js")) return "javascript";
1501
+ if (path9.endsWith(".md")) return "markdown";
1502
+ if (path9.endsWith("Dockerfile")) return "dockerfile";
1503
+ if (path9.endsWith(".yml") || path9.endsWith(".yaml")) return "yaml";
1504
+ return "text";
1505
+ }
1506
+ function lancedbStore() {
1507
+ return `import * as lancedb from "@lancedb/lancedb"
1508
+ import path from "node:path"
1509
+
1510
+ const MODE = process.env.LANCEDB_MODE || "local"
1511
+ // On Vercel (and other serverless platforms), /var/task is read-only.
1512
+ // Fall back to /tmp which is writable, though ephemeral per invocation.
1513
+ const IS_SERVERLESS = !!process.env.VERCEL || !!process.env.AWS_LAMBDA_FUNCTION_NAME
1514
+ const DEFAULT_DB_PATH = IS_SERVERLESS ? "/tmp/lancedb" : "./.larkup/lancedb"
1515
+ const DB_PATH = process.env.LANCEDB_PATH || DEFAULT_DB_PATH
1516
+ const URI = process.env.LANCEDB_URI || ""
1517
+ const API_KEY = process.env.LANCEDB_API_KEY || ""
1518
+ const TABLE = process.env.LANCEDB_TABLE || "documents"
1519
+
1520
+ if (IS_SERVERLESS && MODE !== "cloud") {
1521
+ console.warn("[LanceDB] Running on serverless \u2014 using /tmp/lancedb. Data will NOT persist between invocations. Use LanceDB Cloud for production.")
1522
+ }
1523
+
1524
+ let _table = null
1525
+
1526
+ async function table() {
1527
+ if (_table) return _table
1528
+ let conn
1529
+ if (MODE === "cloud") {
1530
+ if (!URI || !API_KEY) {
1531
+ throw new Error("LanceDB Cloud needs LANCEDB_URI and LANCEDB_API_KEY.")
1532
+ }
1533
+ conn = await lancedb.connect(URI, { apiKey: API_KEY })
1534
+ } else {
1535
+ const abs = path.isAbsolute(DB_PATH) ? DB_PATH : path.join(process.cwd(), DB_PATH)
1536
+ conn = await lancedb.connect(abs)
1537
+ }
1538
+
1539
+ _table = await conn.openTable(TABLE)
1540
+ return _table
1541
+ }
1542
+
1543
+ export async function query(vector, topK) {
1544
+ const t = await table()
1545
+ const rows = await t.search(vector).limit(topK).toArray()
1546
+ return rows.map((row) => ({
1547
+ id: row.id,
1548
+ score: typeof row._distance === "number" ? 1 / (1 + row._distance) : 0,
1549
+ text: row.text,
1550
+ title: row.title,
1551
+ url: row.url || undefined,
1552
+ documentId: row.documentId,
1553
+ }))
1554
+ }
1555
+
1556
+ export async function list({ page = 1, limit = 20 } = {}) {
1557
+ const t = await table()
1558
+ // Fetch all rows to get total count, then slice for the page.
1559
+ // We explicitly exclude the "vector" column to avoid memory bloat and Rust panics on full scans.
1560
+ const allRows = await t.query().select(["id", "text", "title", "url", "documentId"]).toArray()
1561
+ const total = allRows.length
1562
+ const start = (page - 1) * limit
1563
+ const pageRows = allRows.slice(start, start + limit)
1564
+ return {
1565
+ documents: pageRows.map((row) => ({
1566
+ id: row.id,
1567
+ text: row.text,
1568
+ title: row.title,
1569
+ url: row.url || undefined,
1570
+ documentId: row.documentId,
1571
+ })),
1572
+ total,
1573
+ page,
1574
+ limit,
1575
+ totalPages: Math.ceil(total / limit),
1576
+ }
1577
+ }
1578
+
1579
+ export async function get(id) {
1580
+ const t = await table()
1581
+ const rows = await t.query().filter(\`id = '\${id}'\`).limit(1).toArray()
1582
+ if (!rows.length) return null;
1583
+ const row = rows[0];
1584
+ return { id: row.id, text: row.text, title: row.title, url: row.url || undefined, documentId: row.documentId }
1585
+ }
1586
+
1587
+ export async function add(docs) {
1588
+ const t = await table()
1589
+ await t.add(docs)
1590
+ return { success: true }
1591
+ }
1592
+
1593
+ export async function remove(id) {
1594
+ const t = await table()
1595
+ await t.delete(\`id = '\${id}'\`)
1596
+ return { success: true }
1597
+ }
1598
+
1599
+ export async function update(id, doc) {
1600
+ const t = await table()
1601
+ await t.delete(\`id = '\${id}'\`)
1602
+ await t.add([doc])
1603
+ return { success: true }
1604
+ }
1605
+ `;
1606
+ }
1607
+ function pineconeStore() {
1608
+ return `import { Pinecone } from "@pinecone-database/pinecone"
1609
+
1610
+ const API_KEY = process.env.PINECONE_API_KEY || ""
1611
+ const INDEX = process.env.PINECONE_INDEX || ""
1612
+ const NAMESPACE = process.env.PINECONE_NAMESPACE || "default"
1613
+
1614
+ if (!API_KEY) throw new Error("PINECONE_API_KEY is required.")
1615
+ if (!INDEX) throw new Error("PINECONE_INDEX is required.")
1616
+
1617
+ const pc = new Pinecone({ apiKey: API_KEY })
1618
+ const ns = pc.index(INDEX).namespace(NAMESPACE)
1619
+
1620
+ export async function query(vector, topK) {
1621
+ const res = await ns.query({ vector, topK, includeMetadata: true })
1622
+ return (res.matches ?? []).map((m) => {
1623
+ const meta = m.metadata ?? {}
1624
+ return {
1625
+ id: m.id,
1626
+ score: m.score ?? 0,
1627
+ text: meta.text ?? "",
1628
+ title: meta.title ?? "Untitled",
1629
+ url: meta.url || undefined,
1630
+ documentId: meta.documentId ?? "",
1631
+ }
1632
+ })
1633
+ }
1634
+
1635
+ export async function list({ page = 1, limit = 20 } = {}) {
1636
+ // Collect all IDs via Pinecone's cursor-based pagination
1637
+ const allIds = []
1638
+ let paginationToken = undefined
1639
+ do {
1640
+ const res = await ns.listPaginated({ limit: 100, paginationToken })
1641
+ if (res.vectors) allIds.push(...res.vectors.map(v => v.id))
1642
+ paginationToken = res.pagination?.next
1643
+ } while (paginationToken)
1644
+
1645
+ const total = allIds.length
1646
+ const start = (page - 1) * limit
1647
+ const pageIds = allIds.slice(start, start + limit)
1648
+
1649
+ if (pageIds.length === 0) {
1650
+ return { documents: [], total, page, limit, totalPages: Math.ceil(total / limit) }
1651
+ }
1652
+
1653
+ const fetched = await ns.fetch(pageIds)
1654
+ const documents = Object.values(fetched.records).map(r => {
1655
+ const meta = r.metadata ?? {}
1656
+ return {
1657
+ id: r.id,
1658
+ text: meta.text ?? "",
1659
+ title: meta.title ?? "Untitled",
1660
+ url: meta.url || undefined,
1661
+ documentId: meta.documentId ?? "",
1662
+ }
1663
+ })
1664
+ return { documents, total, page, limit, totalPages: Math.ceil(total / limit) }
1665
+ }
1666
+
1667
+ export async function get(id) {
1668
+ const fetched = await ns.fetch([id])
1669
+ const r = fetched.records[id]
1670
+ if (!r) return null;
1671
+ const meta = r.metadata ?? {}
1672
+ return {
1673
+ id: r.id,
1674
+ text: meta.text ?? "",
1675
+ title: meta.title ?? "Untitled",
1676
+ url: meta.url || undefined,
1677
+ documentId: meta.documentId ?? "",
1678
+ }
1679
+ }
1680
+
1681
+ export async function add(docs) {
1682
+ const vectors = docs.map(d => ({
1683
+ id: d.id,
1684
+ values: d.vector,
1685
+ metadata: {
1686
+ text: d.text,
1687
+ title: d.title,
1688
+ url: d.url || "",
1689
+ documentId: d.documentId,
1690
+ }
1691
+ }))
1692
+ await ns.upsert(vectors)
1693
+ return { success: true }
1694
+ }
1695
+
1696
+ export async function remove(id) {
1697
+ await ns.deleteOne(id)
1698
+ return { success: true }
1699
+ }
1700
+
1701
+ export async function update(id, doc) {
1702
+ await ns.update({
1703
+ id,
1704
+ values: doc.vector,
1705
+ setMetadata: {
1706
+ text: doc.text,
1707
+ title: doc.title,
1708
+ url: doc.url || "",
1709
+ documentId: doc.documentId,
1710
+ }
1711
+ })
1712
+ return { success: true }
1713
+ }
1714
+ `;
1715
+ }
1716
+ function embedSource(config) {
1717
+ let imports = `import { embed } from "ai"
1718
+ `;
1719
+ let init = ``;
1720
+ if (config.embeddingModelId.startsWith("custom:")) {
1721
+ const customName = config.embeddingModelId.slice("custom:".length);
1722
+ const custom = (config.customEmbeddings ?? []).find(
1723
+ (m) => m.modelName === customName
1724
+ );
1725
+ imports += `import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
1726
+ `;
1727
+ init = `const provider = createOpenAICompatible({
1728
+ name: "custom_provider",
1729
+ baseURL: process.env.EMBEDDING_BASE_URL || ${JSON.stringify(custom?.baseUrl || "")},
1730
+ apiKey: process.env.EMBEDDING_API_KEY
1731
+ })
1732
+ const MODEL = provider.embedding(process.env.EMBEDDING_MODEL || ${JSON.stringify(custom?.modelName || "")})`;
1733
+ } else if (config.embeddingProvider === "deepseek") {
1734
+ imports += `import { createDeepSeek } from "@ai-sdk/deepseek"
1735
+ `;
1736
+ init = `const provider = createDeepSeek({
1737
+ apiKey: process.env.EMBEDDING_API_KEY
1738
+ })
1739
+ const MODEL = provider.embedding(process.env.EMBEDDING_MODEL || ${JSON.stringify(config.embeddingModelId)})`;
1740
+ } else if (config.embeddingProvider === "google") {
1741
+ imports += `import { createGoogleGenerativeAI } from "@ai-sdk/google"
1742
+ `;
1743
+ init = `const provider = createGoogleGenerativeAI({
1744
+ apiKey: process.env.EMBEDDING_API_KEY
1745
+ })
1746
+ const MODEL = provider.embedding(process.env.EMBEDDING_MODEL || ${JSON.stringify(config.embeddingModelId)})`;
1747
+ } else if (config.embeddingProvider === "cohere") {
1748
+ imports += `import { createCohere } from "@ai-sdk/cohere"
1749
+ `;
1750
+ init = `const provider = createCohere({
1751
+ apiKey: process.env.EMBEDDING_API_KEY
1752
+ })
1753
+ const MODEL = provider.embedding(process.env.EMBEDDING_MODEL || ${JSON.stringify(config.embeddingModelId)})`;
1754
+ } else if (config.embeddingProvider === "mistral") {
1755
+ imports += `import { createMistral } from "@ai-sdk/mistral"
1756
+ `;
1757
+ init = `const provider = createMistral({
1758
+ apiKey: process.env.EMBEDDING_API_KEY
1759
+ })
1760
+ const MODEL = provider.embedding(process.env.EMBEDDING_MODEL || ${JSON.stringify(config.embeddingModelId)})`;
1761
+ } else if (config.embeddingProvider === "vercel_ai_gateway") {
1762
+ imports += `import { createGateway } from "@ai-sdk/gateway"
1763
+ `;
1764
+ init = `const gateway = createGateway({
1765
+ apiKey: process.env.EMBEDDING_API_KEY
1766
+ })
1767
+ const MODEL = gateway.embedding(process.env.EMBEDDING_MODEL || ${JSON.stringify(config.embeddingModelId)})`;
1768
+ } else {
1769
+ imports += `import { createOpenAI } from "@ai-sdk/openai"
1770
+ `;
1771
+ init = `const provider = createOpenAI({
1772
+ apiKey: process.env.EMBEDDING_API_KEY
1773
+ })
1774
+ const modelName = process.env.EMBEDDING_MODEL || ${JSON.stringify(config.embeddingModelId)};
1775
+ const MODEL = provider.embedding(modelName.includes("/") ? modelName.split("/")[1] : modelName)`;
1776
+ }
1777
+ return `${imports}
1778
+ ${init}
1779
+
1780
+ export async function embedQuery(text) {
1781
+ const { embedding } = await embed({ model: MODEL, value: text })
1782
+ return embedding
1783
+ }
1784
+ `;
1785
+ }
1786
+ function serverSource(config) {
1787
+ return `import { createServer } from "node:http"
1788
+ import { embedQuery } from "./embed.mjs"
1789
+ import * as store from "./store.mjs"
1790
+ import fs from "node:fs"
1791
+ import path from "node:path"
1792
+ import * as cheerio from "cheerio"
1793
+
1794
+ const PORT = process.env.PORT || 8080
1795
+ const DEFAULT_TOP_K = Number(process.env.TOP_K || ${config.topK})
1796
+
1797
+ console.log('[${config.projectName}] SERVER_API_KEY:', process.env.SERVER_API_KEY ? 'SET ('+process.env.SERVER_API_KEY.length+' chars, starts with: '+process.env.SERVER_API_KEY.slice(0,8)+'...)' : 'NOT SET (open access)')
1798
+
1799
+ function send(res, status, body) {
1800
+ const json = JSON.stringify(body)
1801
+ res.writeHead(status, {
1802
+ "Content-Type": "application/json",
1803
+ "Access-Control-Allow-Origin": "*",
1804
+ "Access-Control-Allow-Headers": "Content-Type",
1805
+ "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
1806
+ })
1807
+ res.end(json)
1808
+ }
1809
+
1810
+ async function readBody(req) {
1811
+ const chunks = []
1812
+ for await (const c of req) chunks.push(c)
1813
+ if (chunks.length === 0) return {}
1814
+ try {
1815
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"))
1816
+ } catch {
1817
+ return {}
1818
+ }
1819
+ }
1820
+
1821
+ const server = createServer(async (req, res) => {
1822
+ if (req.method === "OPTIONS") return send(res, 204, {})
1823
+
1824
+ const url = new URL(req.url, \`http://\${req.headers.host}\`)
1825
+
1826
+ if (req.method === "GET" && url.pathname === "/favicon2.ico") {
1827
+ try {
1828
+ const filepath = path.join(process.cwd(), "public", "favicon2.ico")
1829
+ if (fs.existsSync(filepath)) {
1830
+ res.writeHead(200, { "Content-Type": "image/x-icon" })
1831
+ return res.end(fs.readFileSync(filepath))
1832
+ }
1833
+ const rootFilepath = path.join(process.cwd(), "favicon2.ico")
1834
+ if (fs.existsSync(rootFilepath)) {
1835
+ res.writeHead(200, { "Content-Type": "image/x-icon" })
1836
+ return res.end(fs.readFileSync(rootFilepath))
1837
+ }
1838
+ } catch {}
1839
+ }
1840
+
1841
+ const expectedKey = process.env.SERVER_API_KEY
1842
+
1843
+ if (expectedKey) {
1844
+ const auth = req.headers.authorization
1845
+ const isPublic = url.pathname === "/" || url.pathname === "/health" || url.pathname === "/openapi.json" || url.pathname === "/reference"
1846
+ if (!isPublic) {
1847
+ if (!auth) {
1848
+ return send(res, 401, { error: "Missing Authorization header. Expected Bearer token." })
1849
+ }
1850
+ const token = auth.replace(/^Bearer\\s+/i, "").trim()
1851
+ if (token !== expectedKey.trim()) {
1852
+ console.error('[AUTH] Token mismatch. Got:', token.slice(0,20), '| Expected starts with:', expectedKey.trim().slice(0,20))
1853
+ return send(res, 403, { error: "Invalid API key." })
1854
+ }
1855
+ }
1856
+ }
1857
+
1858
+ if (req.method === "GET" && url.pathname === "/health") {
1859
+ return send(res, 200, { ok: true, service: ${JSON.stringify(config.projectName)} })
1860
+ }
1861
+
1862
+ if (req.method === "GET" && url.pathname === "/") {
1863
+ res.writeHead(200, { "Content-Type": "text/html" })
1864
+ return res.end(\`<!DOCTYPE html>
1865
+ <html lang="en">
1866
+ <head>
1867
+ <meta charset="utf-8" />
1868
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1869
+ <title>\${${JSON.stringify(config.projectName)}} \u2014 RAG Server</title>
1870
+ <link rel="icon" href="/favicon2.ico" type="image/x-icon" />
1871
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1872
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1873
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
1874
+ <style>
1875
+ *{margin:0;padding:0;box-sizing:border-box}
1876
+ body{font-family:'Inter',system-ui,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#F2EDE8;color:#18181b;overflow:hidden;position:relative}
1877
+ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.28 0 0 0 0 0.23 0 0 0 0 0.16 0 0 0 0.55 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");background-size:512px 512px;background-repeat:repeat;opacity:0.79;mix-blend-mode:overlay}
1878
+ .bg-mesh{position:fixed;inset:0;z-index:0;background-image:radial-gradient(ellipse 55% 45% at 8% 4%,rgba(0,0,0,0.12),transparent 70%),radial-gradient(ellipse 45% 40% at 94% 92%,rgba(220,160,120,0.15),transparent 65%),radial-gradient(ellipse 70% 55% at 50% 50%,rgba(0,0,0,0.05),transparent 80%)}
1879
+ .container{position:relative;z-index:1;text-align:center;max-width:800px;padding:2rem;}
1880
+ .badge{display:inline-flex;align-items:center;gap:6px;padding:6px 16px;border-radius:100px;font-size:0.75rem;font-weight:500;color:#000;background:rgba(255,255,255,0.8);border:1px solid rgba(0,0,0,0.1);margin-bottom:2rem;backdrop-filter:blur(4px);transition:colors .2s}
1881
+ .badge:hover{background:#fff}
1882
+ .badge .dot{width:6px;height:6px;border-radius:50%;background:#10b981;box-shadow:0 0 6px rgba(16,185,129,.4);animation:pulse 2s ease-in-out infinite}
1883
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
1884
+ h1{font-size:3.5rem;font-weight:500;letter-spacing:-0.02em;color:#000;margin-bottom:1.5rem;line-height:1.05;}
1885
+ .subtitle{font-size:1.125rem;color:#52525b;line-height:1.6;margin-bottom:3rem;max-width:600px;margin-left:auto;margin-right:auto;}
1886
+ .actions{display:flex;flex-wrap:wrap;gap:1rem;justify-content:center}
1887
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:0.75rem 1.5rem;font-size:0.875rem;font-weight:500;text-decoration:none;transition:all .2s ease;cursor:pointer;border:none;}
1888
+ .btn-primary{background:#000000;color:#fff;box-shadow:0 2px 12px rgba(0,0,0,.15)}
1889
+ .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 20px rgba(0,0,0,.25)}
1890
+ .btn-outline{background:transparent;color:#000;border:1px solid rgba(0,0,0,.12);}
1891
+ .btn-outline:hover{background:rgba(255,255,255,0.5);border-color:rgba(0,0,0,.2);transform:translateY(-1px);}
1892
+ .footer{margin-top:4rem;font-size:.875rem;color:#71717a}
1893
+ .footer a{color:#000;text-decoration:underline;text-underline-offset:2px;}
1894
+ .footer a:hover{color:#333;}
1895
+ .icon{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
1896
+ @media (max-width: 640px){ h1{font-size:2.5rem;} }
1897
+ </style>
1898
+ </head>
1899
+ <body>
1900
+ <div class="bg-mesh"></div>
1901
+ <div class="container">
1902
+ <div class="badge"><span class="dot"></span> Server Online</div>
1903
+ <h1>\${${JSON.stringify(config.projectName)}}</h1>
1904
+ <p class="subtitle">Your RAG retrieval server is live and ready to accept queries.<br/>Powered by <strong style="color:#000000">Larkup</strong></p>
1905
+ <div class="actions">
1906
+ <a href="/reference" class="btn btn-primary">
1907
+ <svg class="icon" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
1908
+ API Reference
1909
+ </a>
1910
+ <a href="https://github.com/BuddyHere-AI" target="_blank" rel="noopener" class="btn btn-outline">
1911
+ <svg class="icon" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
1912
+ Larkup Team
1913
+ </a>
1914
+ <a href="mailto:contact@larkup.de" class="btn btn-outline">
1915
+ <svg class="icon" viewBox="0 0 24 24"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
1916
+ Contact
1917
+ </a>
1918
+ </div>
1919
+ <div class="footer">Built with <a href="https://github.com/BuddyHere-AI/buddy-rag" target="_blank" rel="noopener">larkup</a> \xB7 v1.0</div>
1920
+ </div>
1921
+ </body>
1922
+ </html>\`)
1923
+ }
1924
+
1925
+ if (req.method === "POST" && url.pathname === "/query") {
1926
+ try {
1927
+ const { query: q, topK } = await readBody(req)
1928
+ if (!q || typeof q !== "string") {
1929
+ return send(res, 400, { error: "Body must include a 'query' string." })
1930
+ }
1931
+ const vector = await embedQuery(q)
1932
+ const hits = await store.query(vector, Number(topK) || DEFAULT_TOP_K)
1933
+ return send(res, 200, { query: q, hits })
1934
+ } catch (err) {
1935
+ return send(res, 500, { error: String(err?.message || err) })
1936
+ }
1937
+ }
1938
+
1939
+ if (req.method === "GET" && url.pathname === "/documents") {
1940
+ try {
1941
+ const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10) || 1)
1942
+ const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10) || 20))
1943
+ const result = await store.list({ page, limit })
1944
+ return send(res, 200, result)
1945
+ } catch (err) {
1946
+ return send(res, 500, { error: String(err?.message || err) })
1947
+ }
1948
+ }
1949
+
1950
+ if (req.method === "GET" && url.pathname.match(/^\\/documents\\/[^/]+$/) && !url.pathname.endsWith("/documents")) {
1951
+ try {
1952
+ const id = url.pathname.split("/").pop()
1953
+ const doc = await store.get(id)
1954
+ if (!doc) return send(res, 404, { error: "Document not found" })
1955
+ return send(res, 200, doc)
1956
+ } catch (err) {
1957
+ return send(res, 500, { error: String(err?.message || err) })
1958
+ }
1959
+ }
1960
+
1961
+ if (req.method === "POST" && url.pathname === "/documents") {
1962
+ try {
1963
+ const doc = await readBody(req)
1964
+ if (!doc.text) return send(res, 400, { error: "Missing text" })
1965
+ const vector = await embedQuery(doc.text)
1966
+ const id = doc.id || Math.random().toString(36).slice(2)
1967
+ await store.add([{ id, vector, text: doc.text, title: doc.title || "Untitled", url: doc.url, documentId: doc.documentId || id }])
1968
+ return send(res, 200, { success: true, id })
1969
+ } catch (err) {
1970
+ return send(res, 500, { error: String(err?.message || err) })
1971
+ }
1972
+ }
1973
+
1974
+ if (req.method === "PUT" && url.pathname.startsWith("/documents/")) {
1975
+ try {
1976
+ const id = url.pathname.split("/").pop()
1977
+ const doc = await readBody(req)
1978
+ if (!doc.text) return send(res, 400, { error: "Missing text" })
1979
+ const vector = await embedQuery(doc.text)
1980
+ await store.update(id, { id, vector, text: doc.text, title: doc.title || "Untitled", url: doc.url, documentId: doc.documentId || id })
1981
+ return send(res, 200, { success: true })
1982
+ } catch (err) {
1983
+ return send(res, 500, { error: String(err?.message || err) })
1984
+ }
1985
+ }
1986
+
1987
+ if (req.method === "DELETE" && url.pathname.startsWith("/documents/")) {
1988
+ try {
1989
+ const id = url.pathname.split("/").pop()
1990
+ await store.remove(id)
1991
+ return send(res, 200, { success: true })
1992
+ } catch (err) {
1993
+ return send(res, 500, { error: String(err?.message || err) })
1994
+ }
1995
+ }
1996
+
1997
+ if (req.method === "POST" && url.pathname === "/scrape") {
1998
+ try {
1999
+ const { url: targetUrl } = await readBody(req)
2000
+ if (!targetUrl || typeof targetUrl !== "string") {
2001
+ return send(res, 400, { error: "Body must include a 'url' string." })
2002
+ }
2003
+ const fetchRes = await fetch(targetUrl)
2004
+ if (!fetchRes.ok) return send(res, 502, { error: \`Failed to fetch \${targetUrl}: \${fetchRes.status}\` })
2005
+ const html = await fetchRes.text()
2006
+ const $ = cheerio.load(html)
2007
+ $("script, style, nav, footer, header, iframe, noscript").remove()
2008
+ const rawText = $("body").text().replace(/\\s+/g, " ").trim()
2009
+ if (!rawText) return send(res, 422, { error: "No text content extracted from URL." })
2010
+
2011
+ const title = $("title").text().trim() || new URL(targetUrl).hostname
2012
+ const CHUNK_SIZE = 1000
2013
+ const chunks = []
2014
+ for (let i = 0; i < rawText.length; i += CHUNK_SIZE) {
2015
+ chunks.push(rawText.slice(i, i + CHUNK_SIZE))
2016
+ }
2017
+
2018
+ const documentId = Math.random().toString(36).slice(2)
2019
+ for (const [idx, chunk] of chunks.entries()) {
2020
+ const id = \`\${documentId}-\${idx}\`
2021
+ const vector = await embedQuery(chunk)
2022
+ await store.add([{ id, vector, text: chunk, title: \`\${title} (part \${idx + 1})\`, url: targetUrl, documentId }])
2023
+ }
2024
+
2025
+ return send(res, 200, { success: true, documentId, chunks: chunks.length })
2026
+ } catch (err) {
2027
+ return send(res, 500, { error: String(err?.message || err) })
2028
+ }
2029
+ }
2030
+
2031
+ if (req.method === "GET" && url.pathname === "/openapi.json") {
2032
+ return send(res, 200, {
2033
+ openapi: "3.1.0",
2034
+ info: { title: "Larkup Server", version: "1.0.0" },
2035
+ security: [{ bearerAuth: [] }],
2036
+ paths: {
2037
+ "/query": {
2038
+ post: {
2039
+ summary: "Query the RAG knowledge base",
2040
+ security: [{ bearerAuth: [] }],
2041
+ requestBody: {
2042
+ content: { "application/json": { schema: { type: "object", properties: { query: { type: "string" }, topK: { type: "number" } } } } }
2043
+ },
2044
+ responses: { "200": { description: "Successful response" } }
2045
+ }
2046
+ },
2047
+ "/documents": {
2048
+ get: {
2049
+ summary: "List documents (paginated)",
2050
+ security: [{ bearerAuth: [] }],
2051
+ parameters: [
2052
+ { name: "page", in: "query", required: false, schema: { type: "integer", default: 1, minimum: 1 }, description: "Page number (1-indexed)" },
2053
+ { name: "limit", in: "query", required: false, schema: { type: "integer", default: 20, minimum: 1, maximum: 100 }, description: "Items per page" }
2054
+ ],
2055
+ responses: {
2056
+ "200": {
2057
+ description: "Paginated list of documents",
2058
+ content: {
2059
+ "application/json": {
2060
+ schema: {
2061
+ type: "object",
2062
+ properties: {
2063
+ documents: { type: "array", items: { type: "object" } },
2064
+ total: { type: "integer", description: "Total number of documents" },
2065
+ page: { type: "integer", description: "Current page" },
2066
+ limit: { type: "integer", description: "Items per page" },
2067
+ totalPages: { type: "integer", description: "Total number of pages" }
2068
+ }
2069
+ }
2070
+ }
2071
+ }
2072
+ }
2073
+ }
2074
+ },
2075
+ post: {
2076
+ summary: "Add a new document",
2077
+ security: [{ bearerAuth: [] }],
2078
+ requestBody: {
2079
+ content: { "application/json": { schema: { type: "object", properties: { text: { type: "string" }, title: { type: "string" }, url: { type: "string" }, documentId: { type: "string" } } } } }
2080
+ },
2081
+ responses: { "200": { description: "Successful response" } }
2082
+ }
2083
+ },
2084
+ "/documents/{id}": {
2085
+ get: {
2086
+ summary: "Get a single document by ID",
2087
+ security: [{ bearerAuth: [] }],
2088
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2089
+ responses: {
2090
+ "200": { description: "Document object" },
2091
+ "404": { description: "Document not found" }
2092
+ }
2093
+ },
2094
+ put: {
2095
+ summary: "Update a document",
2096
+ security: [{ bearerAuth: [] }],
2097
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2098
+ requestBody: {
2099
+ content: { "application/json": { schema: { type: "object", properties: { text: { type: "string" }, title: { type: "string" }, url: { type: "string" }, documentId: { type: "string" } } } } }
2100
+ },
2101
+ responses: { "200": { description: "Successful response" } }
2102
+ },
2103
+ delete: {
2104
+ summary: "Delete a document",
2105
+ security: [{ bearerAuth: [] }],
2106
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2107
+ responses: { "200": { description: "Successful response" } }
2108
+ }
2109
+ },
2110
+ "/scrape": {
2111
+ post: {
2112
+ summary: "Scrape a URL and ingest into the corpus",
2113
+ security: [{ bearerAuth: [] }],
2114
+ requestBody: {
2115
+ content: { "application/json": { schema: { type: "object", required: ["url"], properties: { url: { type: "string", description: "URL to scrape" } } } } }
2116
+ },
2117
+ responses: {
2118
+ "200": { description: "Scrape successful", content: { "application/json": { schema: { type: "object", properties: { success: { type: "boolean" }, documentId: { type: "string" }, chunks: { type: "integer" } } } } } },
2119
+ "502": { description: "Failed to fetch the target URL" }
2120
+ }
2121
+ }
2122
+ },
2123
+ "/health": {
2124
+ get: {
2125
+ summary: "Health check",
2126
+ responses: { "200": { description: "OK" } }
2127
+ }
2128
+ }
2129
+ },
2130
+ components: {
2131
+ securitySchemes: {
2132
+ bearerAuth: {
2133
+ type: "http",
2134
+ scheme: "bearer"
2135
+ }
2136
+ }
2137
+ }
2138
+ })
2139
+ }
2140
+
2141
+ if (req.method === "GET" && url.pathname === "/reference") {
2142
+ const defaultToken = process.env.SERVER_API_KEY || ""
2143
+ res.writeHead(200, { "Content-Type": "text/html" })
2144
+ return res.end(\`
2145
+ <!DOCTYPE html>
2146
+ <html>
2147
+ <head>
2148
+ <title>API Reference</title>
2149
+ <meta charset="utf-8" />
2150
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2151
+ <link rel="icon" href="/favicon2.ico" type="image/x-icon" />
2152
+ </head>
2153
+ <body>
2154
+ <script id="api-reference" data-url="/openapi.json"></script>
2155
+ <script>
2156
+ const configuration = {
2157
+ "theme": "kepler",
2158
+ "hideClientButton": false,
2159
+ "showSidebar": true,
2160
+ "showDeveloperTools": "localhost",
2161
+ "showToolbar": "localhost",
2162
+ "operationTitleSource": "summary",
2163
+ "persistAuth": true,
2164
+ "telemetry": true,
2165
+ "externalUrls": {
2166
+ "dashboardUrl": "https://dashboard.scalar.com",
2167
+ "registryUrl": "https://registry.scalar.com",
2168
+ "proxyUrl": "https://proxy.scalar.com",
2169
+ "apiBaseUrl": "https://api.scalar.com"
2170
+ },
2171
+ "default": false,
2172
+ "layout": "modern",
2173
+ "isEditable": false,
2174
+ "isLoading": false,
2175
+ "hideModels": false,
2176
+ "documentDownloadType": "both",
2177
+ "hideTestRequestButton": false,
2178
+ "hideSearch": false,
2179
+ "showOperationId": false,
2180
+ "hideDarkModeToggle": false,
2181
+ "withDefaultFonts": true,
2182
+ "defaultOpenFirstTag": true,
2183
+ "defaultOpenAllTags": false,
2184
+ "expandAllModelSections": false,
2185
+ "expandAllResponses": false,
2186
+ "orderSchemaPropertiesBy": "alpha",
2187
+ "orderRequiredPropertiesFirst": true,
2188
+ "_integration": "html",
2189
+ "modelsSectionLabel": "Models",
2190
+ "slug": "api-1",
2191
+ "title": "API #1",
2192
+ "authentication": {
2193
+ "preferredSecurityScheme": "bearerAuth",
2194
+ "http": {
2195
+ "basic": { "username": "", "password": "" },
2196
+ "bearer": { "token": "\${defaultToken}" }
2197
+ }
2198
+ }
2199
+ };
2200
+ document.getElementById('api-reference').dataset.configuration = JSON.stringify(configuration);
2201
+ </script>
2202
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
2203
+ </body>
2204
+ </html>
2205
+ \`)
2206
+ }
2207
+
2208
+ return send(res, 404, { error: "Not found" })
2209
+ })
2210
+
2211
+ server.listen(PORT, () => {
2212
+ console.log(\`[${config.projectName}] RAG server listening on :\${PORT}\`)
2213
+ })
2214
+ `;
2215
+ }
2216
+ function demoSource() {
2217
+ return `const BASE = process.env.RAG_SERVER_URL || "http://localhost:8080"
2218
+
2219
+ const question = process.argv.slice(2).join(" ") || "What is this corpus about?"
2220
+
2221
+ const res = await fetch(\`\${BASE}/query\`, {
2222
+ method: "POST",
2223
+ headers: { "Content-Type": "application/json" },
2224
+ body: JSON.stringify({ query: question }),
2225
+ })
2226
+
2227
+ const data = await res.json()
2228
+ if (!res.ok) {
2229
+ console.error("Error:", data.error)
2230
+ process.exit(1)
2231
+ }
2232
+
2233
+ console.log(\`\\nQuery: \${data.query}\\n\`)
2234
+ for (const [i, hit] of data.hits.entries()) {
2235
+ console.log(\`#\${i + 1} [\${hit.score.toFixed(3)}] \${hit.title}\`)
2236
+ if (hit.url) console.log(\` \${hit.url}\`)
2237
+ console.log(\` \${hit.text.slice(0, 160).replace(/\\n/g, " ")}...\\n\`)
2238
+ }
2239
+ `;
2240
+ }
2241
+ function dockerfile() {
2242
+ return `FROM node:22-slim
2243
+
2244
+ WORKDIR /app
2245
+
2246
+ COPY package.json ./
2247
+ RUN npm install --omit=dev
2248
+
2249
+ COPY . .
2250
+
2251
+ ENV PORT=8080
2252
+ EXPOSE 8080
2253
+
2254
+ CMD ["node", "server.mjs"]
2255
+ `;
2256
+ }
2257
+ function dockerignore() {
2258
+ return `node_modules
2259
+ npm-debug.log
2260
+ .env
2261
+ .git
2262
+ .DS_Store
2263
+ `;
2264
+ }
2265
+ function dockerCompose(projectName, usesLocalLance) {
2266
+ const volume = usesLocalLance ? ` volumes:
2267
+ - ./.larkup:/app/.larkup
2268
+ ` : "";
2269
+ return `services:
2270
+ ${projectName}:
2271
+ build: .
2272
+ container_name: ${projectName}
2273
+ restart: unless-stopped
2274
+ ports:
2275
+ - "8080:8080"
2276
+ env_file:
2277
+ - .env
2278
+ ${volume}`;
2279
+ }
2280
+ function vercelJson() {
2281
+ return `{
2282
+ "$schema": "https://openapi.vercel.sh/vercel.json",
2283
+ "builds": [{ "src": "server.mjs", "use": "@vercel/node" }],
2284
+ "routes": [
2285
+ { "handle": "filesystem" },
2286
+ { "src": "/(.*)", "dest": "server.mjs" }
2287
+ ]
2288
+ }
2289
+ `;
2290
+ }
2291
+ function envExample(server) {
2292
+ return server.envVars.map((e) => `# ${e.help}${e.required ? "" : " (optional)"}
2293
+ ${e.key}=`).join("\n");
2294
+ }
2295
+ function readme(config, server) {
2296
+ const store = getVectorStore(config.vectorStore);
2297
+ const deps = Object.entries(server.dependencies).map(([k, v]) => `- \`${k}@${v}\``).join("\n");
2298
+ return `# ${config.projectName}
2299
+
2300
+ A lightweight RAG retrieval server generated by **larkup**.
2301
+
2302
+ - **Vector store:** ${store.label}
2303
+ - **Embedding model:** ${config.embeddingModelId}
2304
+ - **Index type:** ${config.indexType}
2305
+ - **Default top-k:** ${config.topK}
2306
+
2307
+ ## Dependencies
2308
+ ${deps}
2309
+
2310
+ ## Run locally
2311
+ \`\`\`bash
2312
+ npm install
2313
+ cp .env.example .env
2314
+ node server.mjs
2315
+ \`\`\`
2316
+
2317
+ Then query it:
2318
+ \`\`\`bash
2319
+ node demo.mjs "your question here"
2320
+ # or
2321
+ curl -X POST http://localhost:8080/query \\
2322
+ -H "Content-Type: application/json" \\
2323
+ -H "Authorization: Bearer <your-key>" \\
2324
+ -d '{"query":"your question","topK":${config.topK}}'
2325
+ \`\`\`
2326
+
2327
+ ## Deploy
2328
+
2329
+ ### Docker / VPS
2330
+ \`\`\`bash
2331
+ docker compose up -d --build
2332
+ \`\`\`
2333
+
2334
+ ### Vercel
2335
+ \`\`\`bash
2336
+ vercel deploy
2337
+ \`\`\`
2338
+
2339
+ ## API
2340
+ - \`GET /health\` \u2192 \`{ ok: true }\`
2341
+ - \`GET /reference\` \u2192 Scalar API docs UI
2342
+ - \`POST /query\` \u2192 \`{ query, hits: [{ id, score, title, url, text, documentId }] }\`
2343
+ - \`GET /documents\` \u2192 list all documents (paginated)
2344
+ - \`GET /documents/:id\` \u2192 get a single document
2345
+ - \`POST /documents\` \u2192 add a document
2346
+ - \`PUT /documents/:id\` \u2192 update a document
2347
+ - \`DELETE /documents/:id\` \u2192 delete a document
2348
+ - \`POST /scrape\` \u2192 scrape a URL and ingest into the corpus
2349
+ `;
2350
+ }
2351
+ function generateServer(config) {
2352
+ const store = getVectorStore(config.vectorStore);
2353
+ const model = getEmbeddingModel(config.embeddingModelId);
2354
+ const usesLocalLance = config.vectorStore === "lancedb" && config.storeConfig.mode !== "cloud";
2355
+ const dependencies = {
2356
+ ai: "^6.0.0",
2357
+ cheerio: "^1.0.0",
2358
+ ...store.serverDependencies
2359
+ };
2360
+ const envVars = [
2361
+ {
2362
+ key: "EMBEDDING_API_KEY",
2363
+ required: true,
2364
+ help: "API key used to embed incoming queries."
2365
+ },
2366
+ {
2367
+ key: "PORT",
2368
+ required: false,
2369
+ help: "Port to listen on (default 8080)."
2370
+ },
2371
+ {
2372
+ key: "SERVER_API_KEY",
2373
+ required: false,
2374
+ help: "Bearer token to secure your server endpoints. If set, requests must include 'Authorization: Bearer <key>'."
2375
+ },
2376
+ {
2377
+ key: "TOP_K",
2378
+ required: false,
2379
+ help: `Default number of documents to retrieve (default ${config.topK}).`
2380
+ }
2381
+ ];
2382
+ if (config.vectorStore === "pinecone") {
2383
+ envVars.push(
2384
+ { key: "PINECONE_API_KEY", required: true, help: "Pinecone API key." },
2385
+ {
2386
+ key: "PINECONE_INDEX",
2387
+ required: true,
2388
+ help: "Pinecone index name to query."
2389
+ },
2390
+ {
2391
+ key: "PINECONE_NAMESPACE",
2392
+ required: false,
2393
+ help: "Pinecone namespace (default 'default')."
2394
+ },
2395
+ {
2396
+ key: "PINECONE_SPARSE_MODEL",
2397
+ required: false,
2398
+ help: "Pinecone sparse model (for hybrid search)."
2399
+ },
2400
+ {
2401
+ key: "PINECONE_SPARSE_INDEX",
2402
+ required: false,
2403
+ help: "Pinecone sparse index name (for hybrid search)."
2404
+ }
2405
+ );
2406
+ } else {
2407
+ envVars.push(
2408
+ {
2409
+ key: "LANCEDB_MODE",
2410
+ required: false,
2411
+ help: "'local' (on-disk) or 'cloud' (default 'local')."
2412
+ },
2413
+ {
2414
+ key: "LANCEDB_PATH",
2415
+ required: false,
2416
+ help: "On-disk path to the LanceDB tables (local mode)."
2417
+ },
2418
+ {
2419
+ key: "LANCEDB_TABLE",
2420
+ required: false,
2421
+ help: "Table name holding the embedded chunks (default 'documents')."
2422
+ },
2423
+ {
2424
+ key: "LANCEDB_URI",
2425
+ required: false,
2426
+ help: "LanceDB Cloud database URI (cloud mode)."
2427
+ },
2428
+ {
2429
+ key: "LANCEDB_API_KEY",
2430
+ required: false,
2431
+ help: "LanceDB Cloud API key (cloud mode)."
2432
+ }
2433
+ );
2434
+ }
2435
+ const server = {
2436
+ projectName: config.projectName,
2437
+ files: [],
2438
+ dependencies,
2439
+ envVars
2440
+ };
2441
+ const pkg = {
2442
+ name: config.projectName,
2443
+ version: "1.0.0",
2444
+ private: true,
2445
+ type: "module",
2446
+ description: `Lightweight RAG server (${store.label}) generated by larkup`,
2447
+ scripts: {
2448
+ start: "node server.mjs",
2449
+ demo: "node demo.mjs"
2450
+ },
2451
+ dependencies,
2452
+ engines: { node: ">=20" }
2453
+ };
2454
+ const generatedEnv = server.envVars.map((e) => {
2455
+ let val = "";
2456
+ if (e.key === "EMBEDDING_API_KEY") val = config.embeddingApiKey || "";
2457
+ if (e.key === "PINECONE_API_KEY") val = config.storeConfig.apiKey || "";
2458
+ if (e.key === "PINECONE_INDEX") val = config.storeConfig.indexName || "";
2459
+ if (e.key === "PINECONE_NAMESPACE")
2460
+ val = config.storeConfig.namespace || "";
2461
+ if (e.key === "LANCEDB_URI") val = config.storeConfig.uri || "";
2462
+ if (e.key === "LANCEDB_API_KEY") val = config.storeConfig.apiKey || "";
2463
+ return `${e.key}=${val}`;
2464
+ }).join("\n");
2465
+ const files = [
2466
+ { path: "package.json", contents: JSON.stringify(pkg, null, 2) + "\n" },
2467
+ { path: "server.mjs", contents: serverSource(config) },
2468
+ { path: "embed.mjs", contents: embedSource(config) },
2469
+ {
2470
+ path: "store.mjs",
2471
+ contents: config.vectorStore === "pinecone" ? pineconeStore() : lancedbStore()
2472
+ },
2473
+ { path: "demo.mjs", contents: demoSource() },
2474
+ { path: "Dockerfile", contents: dockerfile() },
2475
+ { path: ".dockerignore", contents: dockerignore() },
2476
+ {
2477
+ path: "docker-compose.yml",
2478
+ contents: dockerCompose(config.projectName, usesLocalLance)
2479
+ },
2480
+ { path: "vercel.json", contents: vercelJson() },
2481
+ { path: ".env.example", contents: envExample(server) },
2482
+ { path: ".env", contents: generatedEnv },
2483
+ { path: ".gitignore", contents: "node_modules\n.env\n.DS_Store\n" },
2484
+ { path: "README.md", contents: readme(config, server) }
2485
+ ].map((f) => ({ ...f, language: lang(f.path) }));
2486
+ try {
2487
+ const fs8 = __require("fs");
2488
+ const path9 = __require("path");
2489
+ const faviconPath = path9.resolve(process.cwd(), "public/favicon2.ico");
2490
+ if (fs8.existsSync(faviconPath)) {
2491
+ files.push({
2492
+ path: "public/favicon2.ico",
2493
+ contents: fs8.readFileSync(faviconPath).toString("base64"),
2494
+ language: "ico",
2495
+ encoding: "base64"
2496
+ });
2497
+ } else {
2498
+ const webFaviconPath = path9.resolve(
2499
+ process.cwd(),
2500
+ "apps/web/public/favicon2.ico"
2501
+ );
2502
+ if (fs8.existsSync(webFaviconPath)) {
2503
+ files.push({
2504
+ path: "public/favicon2.ico",
2505
+ contents: fs8.readFileSync(webFaviconPath).toString("base64"),
2506
+ language: "ico",
2507
+ encoding: "base64"
2508
+ });
2509
+ }
2510
+ }
2511
+ } catch (e) {
2512
+ }
2513
+ void model;
2514
+ server.files = files;
2515
+ return server;
2516
+ }
2517
+
2518
+ // ../../packages/core/src/generator/server-runtime.ts
2519
+ import { promises as fs6 } from "fs";
2520
+ import path6 from "path";
2521
+ import { spawn, exec } from "child_process";
2522
+ import { promisify } from "util";
2523
+ var execAsync = promisify(exec);
2524
+ var FALLBACK_PORT = 8080;
2525
+ async function resolvePort() {
2526
+ const server = await getActiveServer();
2527
+ return server?.port ?? FALLBACK_PORT;
2528
+ }
2529
+ function emptyState(port) {
2530
+ return {
2531
+ running: false,
2532
+ port,
2533
+ endpoint: `http://localhost:${port}`
2534
+ };
2535
+ }
2536
+ async function outDir(create) {
2537
+ const dir = create ? await requireDataDir() : await getDataDir();
2538
+ if (!dir) return null;
2539
+ return path6.join(dir, "generated-server");
2540
+ }
2541
+ async function statePath(create) {
2542
+ const dir = create ? await requireDataDir() : await getDataDir();
2543
+ if (!dir) return null;
2544
+ return path6.join(dir, "server-local.json");
2545
+ }
2546
+ async function readServerState() {
2547
+ const port = await resolvePort();
2548
+ const file = await statePath(false);
2549
+ if (!file) return emptyState(port);
2550
+ try {
2551
+ const raw = await fs6.readFile(file, "utf8");
2552
+ return { ...emptyState(port), ...JSON.parse(raw), port };
2553
+ } catch {
2554
+ return emptyState(port);
2555
+ }
2556
+ }
2557
+ async function writeState(state) {
2558
+ const file = await statePath(true);
2559
+ if (file) await fs6.writeFile(file, JSON.stringify(state, null, 2), "utf8");
2560
+ return state;
2561
+ }
2562
+ function pidAlive(pid) {
2563
+ if (!pid) return false;
2564
+ try {
2565
+ process.kill(pid, 0);
2566
+ return true;
2567
+ } catch {
2568
+ return false;
2569
+ }
2570
+ }
2571
+ async function emitToDisk(config) {
2572
+ const server = generateServer(config);
2573
+ const dir = await outDir(true);
2574
+ if (!dir) throw new Error("No active server to emit to.");
2575
+ await fs6.mkdir(dir, { recursive: true });
2576
+ for (const file of server.files) {
2577
+ const dest = path6.join(dir, file.path);
2578
+ await fs6.mkdir(path6.dirname(dest), { recursive: true });
2579
+ if (file.encoding === "base64") {
2580
+ await fs6.writeFile(dest, Buffer.from(file.contents, "base64"));
2581
+ } else {
2582
+ await fs6.writeFile(dest, file.contents, "utf8");
2583
+ }
2584
+ }
2585
+ return dir;
2586
+ }
2587
+ async function isHealthy(endpoint) {
2588
+ try {
2589
+ const res = await fetch(`${endpoint}/health`, {
2590
+ signal: AbortSignal.timeout(3e3)
2591
+ });
2592
+ return res.ok;
2593
+ } catch {
2594
+ return false;
2595
+ }
2596
+ }
2597
+ async function refreshServerStatus() {
2598
+ const state = await readServerState();
2599
+ if (!state.startedAt) return state;
2600
+ const alive = pidAlive(state.pid) && await isHealthy(state.endpoint);
2601
+ if (alive !== state.running) {
2602
+ return writeState({ ...state, running: alive });
2603
+ }
2604
+ return state;
2605
+ }
2606
+
2607
+ // src/commands/generate.ts
2608
+ async function generateCommand(options) {
2609
+ await inServerScope(options.server, async () => {
2610
+ await requireActive();
2611
+ const config = await readConfig();
2612
+ if (options.out) {
2613
+ const dir = path7.isAbsolute(options.out) ? options.out : path7.join(process.cwd(), options.out);
2614
+ const server2 = generateServer(config);
2615
+ await fs7.mkdir(dir, { recursive: true });
2616
+ for (const f of server2.files) {
2617
+ const dest = path7.join(dir, f.path);
2618
+ await fs7.mkdir(path7.dirname(dest), { recursive: true });
2619
+ await fs7.writeFile(dest, f.contents, "utf8");
2620
+ }
2621
+ log.success(`Generated ${server2.files.length} files \u2192 ${dir}`);
2622
+ } else {
2623
+ const dir = await emitToDisk(config);
2624
+ const server2 = generateServer(config);
2625
+ log.success(`Generated ${server2.files.length} files \u2192 ${dir}`);
2626
+ }
2627
+ const server = generateServer(config);
2628
+ const deps = Object.entries(server.dependencies).map(([k, v]) => `${k}@${v}`).join(", ");
2629
+ log.dim(` deps: ${deps}`);
2630
+ log.dim(" run it locally with: larkup serve");
2631
+ });
2632
+ }
2633
+
2634
+ // src/commands/serve.ts
2635
+ import path8 from "path";
2636
+ import { spawn as spawn2 } from "child_process";
2637
+ async function serveCommand(options) {
2638
+ await inServerScope(options.server, async () => {
2639
+ const server = await requireActive();
2640
+ const config = await readConfig();
2641
+ const isLocalLance = config.vectorStore === "lancedb" && config.storeConfig.mode !== "cloud";
2642
+ const dir = await emitToDisk(config);
2643
+ log.info(log.fmt.cyan("Installing minimal server dependencies\u2026"));
2644
+ await new Promise((resolve, reject) => {
2645
+ const child2 = spawn2("npm", ["install", "--omit=dev"], { cwd: dir, stdio: "inherit" });
2646
+ child2.on(
2647
+ "exit",
2648
+ (code) => code === 0 ? resolve() : reject(new Error(`npm install exited with code ${code}`))
2649
+ );
2650
+ child2.on("error", reject);
2651
+ });
2652
+ const env = {
2653
+ ...process.env,
2654
+ PORT: String(server.port),
2655
+ TOP_K: String(config.topK)
2656
+ };
2657
+ if (isLocalLance) {
2658
+ const dbPath = config.storeConfig.dbPath || "./.larkup/lancedb";
2659
+ env.LANCEDB_MODE = "local";
2660
+ env.LANCEDB_PATH = path8.isAbsolute(dbPath) ? dbPath : path8.join(process.cwd(), dbPath);
2661
+ env.LANCEDB_TABLE = config.storeConfig.tableName || "documents";
2662
+ }
2663
+ log.success(`Serving ${log.fmt.bold(config.projectName)} on http://localhost:${server.port}`);
2664
+ log.dim(" POST /query \xB7 GET /health \xB7 Ctrl+C to stop");
2665
+ const child = spawn2("node", ["server.mjs"], {
2666
+ cwd: dir,
2667
+ stdio: "inherit",
2668
+ env
2669
+ });
2670
+ await new Promise((resolve) => {
2671
+ child.on("exit", () => resolve());
2672
+ process.on("SIGINT", () => child.kill("SIGINT"));
2673
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
2674
+ });
2675
+ });
2676
+ }
2677
+
2678
+ // src/commands/query.ts
2679
+ async function queryCommand(question, options) {
2680
+ await inServerScope(options.server, async () => {
2681
+ await requireActive();
2682
+ if (!question) log.error('Usage: larkup query "your question" [--topK 5]');
2683
+ const config = await readConfig();
2684
+ const run = await readRun();
2685
+ if (!run || run.status !== "completed" || (run.totalChunks ?? 0) === 0) {
2686
+ log.error("No index to query. Build one first: larkup index");
2687
+ }
2688
+ await ensureApiKey(config, "embedding");
2689
+ const topK = Math.min(
2690
+ Math.max(Number(options.topK) || config.topK, 1),
2691
+ 20
2692
+ );
2693
+ const started = Date.now();
2694
+ const vector = await embedQuery(config, question);
2695
+ const adapter = await createAdapter(config);
2696
+ const hits = await adapter.query(vector, topK, question);
2697
+ const took = Date.now() - started;
2698
+ log.info(`
2699
+ ${log.fmt.bold("Query:")} ${question}`);
2700
+ log.dim(`${hits.length} result(s) \xB7 ${took}ms
2701
+ `);
2702
+ if (hits.length === 0) {
2703
+ log.dim("No matches. Try rephrasing or indexing more content.");
2704
+ return;
2705
+ }
2706
+ hits.forEach((hit, i) => {
2707
+ const pct = Math.round(Math.min(Math.max(hit.score, 0), 1) * 100);
2708
+ log.info(`${log.fmt.cyan(`#${i + 1}`)} ${log.fmt.bold(hit.title || "Untitled")} ${log.fmt.dim(`(${pct}%)`)}`);
2709
+ if (hit.url) log.dim(` ${hit.url}`);
2710
+ const snippet = hit.text.slice(0, 200).replace(/\s+/g, " ").trim();
2711
+ log.info(` ${snippet}${hit.text.length > 200 ? "\u2026" : ""}
2712
+ `);
2713
+ });
2714
+ });
2715
+ }
2716
+
2717
+ // src/commands/chat.ts
2718
+ import { streamText, tool, stepCountIs } from "ai";
2719
+ import { z } from "zod";
2720
+ import * as p3 from "@clack/prompts";
2721
+
2722
+ // ../../packages/core/src/chat-models/registry.ts
2723
+ var CHAT_MODELS = [
2724
+ // ── OpenAI ──────────────────────────────────────────────────────────
2725
+ {
2726
+ id: "openai/gpt-4o-mini",
2727
+ label: "GPT-4o Mini",
2728
+ provider: "openai",
2729
+ isDefault: true,
2730
+ description: "Fast and affordable. Great for most chat use cases."
2731
+ },
2732
+ {
2733
+ id: "openai/gpt-4o",
2734
+ label: "GPT-4o",
2735
+ provider: "openai",
2736
+ description: "Most capable OpenAI model. Higher cost."
2737
+ },
2738
+ {
2739
+ id: "openai/gpt-4.1-mini",
2740
+ label: "GPT-4.1 Mini",
2741
+ provider: "openai",
2742
+ description: "Latest mini model with improved instruction following."
2743
+ },
2744
+ {
2745
+ id: "openai/gpt-4.1-nano",
2746
+ label: "GPT-4.1 Nano",
2747
+ provider: "openai",
2748
+ description: "Smallest and fastest GPT-4.1 variant."
2749
+ },
2750
+ // ── Google ──────────────────────────────────────────────────────────
2751
+ {
2752
+ id: "google/gemini-2.0-flash",
2753
+ label: "Gemini 2.0 Flash",
2754
+ provider: "google",
2755
+ isDefault: true,
2756
+ description: "Fast, affordable Gemini model. Great default."
2757
+ },
2758
+ {
2759
+ id: "google/gemini-2.5-flash",
2760
+ label: "Gemini 2.5 Flash",
2761
+ provider: "google",
2762
+ description: "Latest Gemini flash with improved reasoning."
2763
+ },
2764
+ {
2765
+ id: "google/gemini-2.5-pro",
2766
+ label: "Gemini 2.5 Pro",
2767
+ provider: "google",
2768
+ description: "Most capable Gemini model."
2769
+ },
2770
+ // ── Cohere ──────────────────────────────────────────────────────────
2771
+ {
2772
+ id: "cohere/command-r",
2773
+ label: "Command R",
2774
+ provider: "cohere",
2775
+ isDefault: true,
2776
+ description: "Optimized for RAG and tool use."
2777
+ },
2778
+ {
2779
+ id: "cohere/command-r-plus",
2780
+ label: "Command R+",
2781
+ provider: "cohere",
2782
+ description: "Most capable Cohere model for complex tasks."
2783
+ },
2784
+ // ── Mistral ─────────────────────────────────────────────────────────
2785
+ {
2786
+ id: "mistral/mistral-small-latest",
2787
+ label: "Mistral Small",
2788
+ provider: "mistral",
2789
+ isDefault: true,
2790
+ description: "Fast and efficient. Good for most tasks."
2791
+ },
2792
+ {
2793
+ id: "mistral/mistral-large-latest",
2794
+ label: "Mistral Large",
2795
+ provider: "mistral",
2796
+ description: "Mistral's most capable model."
2797
+ },
2798
+ // ── DeepSeek ────────────────────────────────────────────────────────
2799
+ {
2800
+ id: "deepseek/deepseek-chat",
2801
+ label: "DeepSeek Chat",
2802
+ provider: "deepseek",
2803
+ isDefault: true,
2804
+ description: "DeepSeek's primary chat model."
2805
+ },
2806
+ // ── Vercel AI Gateway ───────────────────────────────────────────────
2807
+ {
2808
+ id: "openai/gpt-4o-mini",
2809
+ label: "GPT-4o Mini (via Gateway)",
2810
+ provider: "vercel_ai_gateway",
2811
+ isDefault: true,
2812
+ description: "OpenAI GPT-4o Mini routed through Vercel AI Gateway."
2813
+ },
2814
+ {
2815
+ id: "openai/gpt-4o",
2816
+ label: "GPT-4o (via Gateway)",
2817
+ provider: "vercel_ai_gateway",
2818
+ description: "OpenAI GPT-4o routed through Vercel AI Gateway."
2819
+ },
2820
+ {
2821
+ id: "google/gemini-2.0-flash",
2822
+ label: "Gemini 2.0 Flash (via Gateway)",
2823
+ provider: "vercel_ai_gateway",
2824
+ description: "Google Gemini routed through Vercel AI Gateway."
2825
+ },
2826
+ {
2827
+ id: "cohere/command-r-plus",
2828
+ label: "Command R+ (via Gateway)",
2829
+ provider: "vercel_ai_gateway",
2830
+ description: "Cohere Command R+ routed through Vercel AI Gateway."
2831
+ },
2832
+ {
2833
+ id: "mistral/mistral-large-latest",
2834
+ label: "Mistral Large (via Gateway)",
2835
+ provider: "vercel_ai_gateway",
2836
+ description: "Mistral Large routed through Vercel AI Gateway."
2837
+ },
2838
+ {
2839
+ id: "anthropic/claude-3-5-sonnet-20240620",
2840
+ label: "Claude 3.5 Sonnet (via Gateway)",
2841
+ provider: "vercel_ai_gateway",
2842
+ description: "Anthropic Claude 3.5 Sonnet via Vercel AI Gateway."
2843
+ },
2844
+ {
2845
+ id: "anthropic/claude-3-haiku-20240307",
2846
+ label: "Claude 3 Haiku (via Gateway)",
2847
+ provider: "vercel_ai_gateway",
2848
+ description: "Anthropic Claude 3 Haiku via Vercel AI Gateway."
2849
+ }
2850
+ ];
2851
+ function getDefaultChatModel(provider) {
2852
+ return CHAT_MODELS.find((m) => m.provider === provider && m.isDefault) ?? CHAT_MODELS.find((m) => m.provider === provider);
2853
+ }
2854
+ function getChatModel(id) {
2855
+ return CHAT_MODELS.find((m) => m.id === id);
2856
+ }
2857
+
2858
+ // src/commands/chat.ts
2859
+ import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
2860
+ import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
2861
+ import { createCohere as createCohere2 } from "@ai-sdk/cohere";
2862
+ import { createMistral as createMistral2 } from "@ai-sdk/mistral";
2863
+ import { createDeepSeek as createDeepSeek2 } from "@ai-sdk/deepseek";
2864
+ import { createGateway as createGateway2 } from "@ai-sdk/gateway";
2865
+ var SYSTEM_PROMPT = `You are a helpful research assistant powered by a knowledge base.
2866
+ You have one tool:
2867
+ - "searchKnowledgeBase" \u2014 searches a private RAG knowledge base.
2868
+
2869
+ Guidelines:
2870
+ - For factual questions about the knowledge base content, call searchKnowledgeBase.
2871
+ - Synthesize a clear, well-structured answer based on the retrieved documents.
2872
+ - Cite sources inline when available.
2873
+ - Be concise and accurate. Never fabricate sources or facts.
2874
+ - For casual conversation, respond naturally without using the tool.
2875
+ `;
2876
+ function createChatModel(provider, modelId, apiKey) {
2877
+ const modelName = modelId.includes("/") ? modelId.split("/").slice(1).join("/") : modelId;
2878
+ switch (provider) {
2879
+ case "google":
2880
+ return createGoogleGenerativeAI2({ apiKey })(modelName);
2881
+ case "cohere":
2882
+ return createCohere2({ apiKey })(modelName);
2883
+ case "mistral":
2884
+ return createMistral2({ apiKey })(modelName);
2885
+ case "deepseek":
2886
+ return createDeepSeek2({ apiKey })(modelName);
2887
+ case "vercel_ai_gateway":
2888
+ return createGateway2({ apiKey })(modelId);
2889
+ case "openai":
2890
+ default:
2891
+ return createOpenAI2({ apiKey })(modelName);
2892
+ }
2893
+ }
2894
+ async function queryKnowledgeBase(query, topK) {
2895
+ const config = await readConfig();
2896
+ const server = await refreshServerStatus();
2897
+ if (server.running) {
2898
+ try {
2899
+ const res = await fetch(`${server.endpoint}/query`, {
2900
+ method: "POST",
2901
+ headers: { "Content-Type": "application/json" },
2902
+ body: JSON.stringify({ query, topK }),
2903
+ signal: AbortSignal.timeout(15e3)
2904
+ });
2905
+ const data = await res.json();
2906
+ if (res.ok && data.hits) {
2907
+ return { query, hits: data.hits };
2908
+ }
2909
+ } catch {
2910
+ }
2911
+ }
2912
+ const run = await readRun();
2913
+ if (!run || run.status !== "completed" || (run.totalChunks ?? 0) === 0) {
2914
+ return { query, hits: [] };
2915
+ }
2916
+ const vector = await embedQuery(config, query);
2917
+ const adapter = await createAdapter(config);
2918
+ const hits = await adapter.query(vector, topK, query);
2919
+ return { query, hits };
2920
+ }
2921
+ async function chatCommand(options) {
2922
+ await inServerScope(options.server, async () => {
2923
+ const server = await requireActive();
2924
+ const config = await readConfig();
2925
+ const provider = config.embeddingProvider;
2926
+ const chatModelId = options.model || config.chatModelId || getDefaultChatModel(provider)?.id || "openai/gpt-4o-mini";
2927
+ const descriptor = getChatModel(chatModelId);
2928
+ const resolvedProvider = descriptor?.provider || provider;
2929
+ await ensureApiKey(config, "chat");
2930
+ log.info(log.fmt.cyan(`Starting chat session... (Type 'exit' to quit)`));
2931
+ log.dim(`Server: ${server.name} | Model: ${chatModelId}`);
2932
+ const aiModel = createChatModel(resolvedProvider, chatModelId, config.chatApiKey || config.embeddingApiKey);
2933
+ const messages = [];
2934
+ while (true) {
2935
+ const input = await p3.text({
2936
+ message: log.fmt.bold("You:"),
2937
+ placeholder: "Ask something..."
2938
+ });
2939
+ if (p3.isCancel(input)) {
2940
+ log.info("Goodbye.");
2941
+ process.exit(0);
2942
+ }
2943
+ const text4 = input.trim();
2944
+ if (!text4) continue;
2945
+ if (text4.toLowerCase() === "exit" || text4.toLowerCase() === "quit") {
2946
+ log.info("Goodbye.");
2947
+ break;
2948
+ }
2949
+ messages.push({ role: "user", content: text4 });
2950
+ try {
2951
+ process.stdout.write(log.fmt.green(log.fmt.bold("Buddy: ")));
2952
+ const result = streamText({
2953
+ model: aiModel,
2954
+ system: SYSTEM_PROMPT,
2955
+ messages,
2956
+ stopWhen: stepCountIs(5),
2957
+ tools: {
2958
+ searchKnowledgeBase: tool({
2959
+ description: "Search the private knowledge base.",
2960
+ inputSchema: z.object({ query: z.string() }),
2961
+ execute: async ({ query }) => {
2962
+ log.dim(`
2963
+ [Tool: searching knowledge base for "${query}"]`);
2964
+ const res = await queryKnowledgeBase(query, 5);
2965
+ log.dim(`[Tool: found ${res.hits.length} results]
2966
+ `);
2967
+ return res;
2968
+ }
2969
+ })
2970
+ }
2971
+ });
2972
+ let fullResponse = "";
2973
+ for await (const chunk of result.textStream) {
2974
+ process.stdout.write(log.fmt.green(chunk));
2975
+ fullResponse += chunk;
2976
+ }
2977
+ process.stdout.write("\n\n");
2978
+ messages.push({ role: "assistant", content: fullResponse });
2979
+ } catch (e) {
2980
+ log.error(`
2981
+ Chat error: ${e.message}`);
2982
+ messages.pop();
2983
+ }
2984
+ }
2985
+ });
2986
+ }
2987
+
2988
+ // src/commands/settings.ts
2989
+ import * as p4 from "@clack/prompts";
2990
+ async function settingsCommand(options) {
2991
+ await inServerScope(options.server, async () => {
2992
+ await requireActive();
2993
+ const config = await readConfig();
2994
+ log.info(log.fmt.bold("\n--- CLI Settings ---"));
2995
+ const providers = Array.from(new Set(CHAT_MODELS.map((m) => m.provider)));
2996
+ const selectedProvider = await p4.select({
2997
+ message: "Select AI Provider for Chat:",
2998
+ initialValue: config.embeddingProvider,
2999
+ options: providers.map((p5) => ({ label: p5, value: p5 }))
3000
+ });
3001
+ if (p4.isCancel(selectedProvider)) {
3002
+ log.info("Cancelled.");
3003
+ return;
3004
+ }
3005
+ const availableModels = CHAT_MODELS.filter((m) => m.provider === selectedProvider);
3006
+ const selectedModel = await p4.select({
3007
+ message: "Select Chat Model:",
3008
+ initialValue: config.chatModelId || availableModels[0]?.id,
3009
+ options: availableModels.map((m) => ({ label: m.label, value: m.id }))
3010
+ });
3011
+ if (p4.isCancel(selectedModel)) {
3012
+ log.info("Cancelled.");
3013
+ return;
3014
+ }
3015
+ await writeConfig({ ...config, chatModelId: selectedModel });
3016
+ log.success(`Settings saved. Chat model set to ${selectedModel}.`);
3017
+ });
3018
+ }
3019
+
3020
+ // src/index.ts
3021
+ var program = new Command();
3022
+ program.name("larkup").description("Build, index, and serve a RAG pipeline from the terminal.").version("0.1.3");
3023
+ program.command("init [name]").description("Create a new RAG server (workspace project)").action(async (name) => {
3024
+ prompts.intro(log.fmt.bold("larkup init"));
3025
+ await initCommand(name);
3026
+ prompts.outro("Done!");
3027
+ });
3028
+ program.command("servers").alias("list").description("List all servers (\u25CF = active)").action(async () => {
3029
+ await listServersCommand();
3030
+ });
3031
+ program.command("use <serverId>").description("Switch the active server").action(async (serverId) => {
3032
+ await useServerCommand(serverId);
3033
+ });
3034
+ program.command("config").description("Show the active server's configuration + status").option("--server <id>", "Target a specific server instead of the active one").action(async (options) => {
3035
+ await configCommand(options);
3036
+ });
3037
+ program.command("add-doc").description("Add a document to the corpus").option("--file <path>", "Add a document from a file").option("--text <string>", "Add a document from inline text").option("--title <string>", "Document title").option("--url <string>", "Document URL").option("--server <id>", "Target a specific server instead of the active one").action(async (options) => {
3038
+ await addDocCommand(options);
3039
+ });
3040
+ program.command("index").description("Chunk \u2192 embed \u2192 store the corpus").option("--server <id>", "Target a specific server instead of the active one").action(async (options) => {
3041
+ await indexCommand(options);
3042
+ });
3043
+ program.command("generate").description("Emit the deployable RAG server to disk").option("--out <dir>", "Output directory").option("--server <id>", "Target a specific server instead of the active one").action(async (options) => {
3044
+ await generateCommand(options);
3045
+ });
3046
+ program.command("serve").description("Run the generated server locally (foreground)").option("--server <id>", "Target a specific server instead of the active one").action(async (options) => {
3047
+ await serveCommand(options);
3048
+ });
3049
+ program.command("query <question>").description("Retrieve top-k chunks").option("--topK <number>", "Number of results to return").option("--server <id>", "Target a specific server instead of the active one").action(async (question, options) => {
3050
+ await queryCommand(question, options);
3051
+ });
3052
+ program.command("chat").description("Chat with your knowledge base in the terminal").option("--model <string>", "Specify the chat model ID").option("--server <id>", "Target a specific server instead of the active one").action(async (options) => {
3053
+ await chatCommand(options);
3054
+ });
3055
+ program.command("settings").description("Configure CLI settings (e.g., chat models)").option("--server <id>", "Target a specific server instead of the active one").action(async (options) => {
3056
+ await settingsCommand(options);
3057
+ });
3058
+ program.parseAsync(process.argv).catch((err) => {
3059
+ log.error(err instanceof Error ? err.message : String(err));
3060
+ });