@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.
@@ -0,0 +1,163 @@
1
+ import "./chunk-3RG5ZIWI.js";
2
+
3
+ // ../../packages/vector-stores/src/adapters/lancedb.ts
4
+ import path from "path";
5
+ import fs from "fs/promises";
6
+ import * as lancedb from "@lancedb/lancedb";
7
+ var LanceDBAdapter = class {
8
+ constructor(config) {
9
+ this.config = config;
10
+ this.tableName = config.tableName?.trim() || "documents";
11
+ }
12
+ config;
13
+ conn = null;
14
+ table = null;
15
+ tableName;
16
+ async connect() {
17
+ if (this.conn) return this.conn;
18
+ if (this.config.mode === "cloud") {
19
+ if (!this.config.uri || !this.config.apiKey) {
20
+ throw new Error("LanceDB Cloud requires both a URI and an API key.");
21
+ }
22
+ if (!this.config.uri.startsWith("db://")) {
23
+ throw new Error(
24
+ "LanceDB Cloud URI must start with 'db://' (e.g. db://my-database)."
25
+ );
26
+ }
27
+ this.conn = await lancedb.connect(this.config.uri, {
28
+ apiKey: this.config.apiKey
29
+ });
30
+ } else {
31
+ const dir = this.config.dbPath?.trim() || "./.larkup/lancedb";
32
+ let abs = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
33
+ try {
34
+ await fs.mkdir(abs, { recursive: true });
35
+ } catch (err) {
36
+ if (path.isAbsolute(dir)) {
37
+ const fallbackDir = "." + dir;
38
+ abs = path.join(process.cwd(), fallbackDir);
39
+ try {
40
+ await fs.mkdir(abs, { recursive: true });
41
+ } catch (fallbackErr) {
42
+ throw new Error(
43
+ `Could not create directory for LanceDB at "${abs}": ${fallbackErr.message}`
44
+ );
45
+ }
46
+ } else {
47
+ throw new Error(
48
+ `Could not create directory for LanceDB at "${abs}": ${err.message}`
49
+ );
50
+ }
51
+ }
52
+ this.conn = await lancedb.connect(abs);
53
+ }
54
+ return this.conn;
55
+ }
56
+ async init() {
57
+ const conn = await this.connect();
58
+ const names = await conn.tableNames();
59
+ if (names.includes(this.tableName)) {
60
+ this.table = await conn.openTable(this.tableName);
61
+ }
62
+ }
63
+ async reset() {
64
+ const conn = await this.connect();
65
+ const names = await conn.tableNames();
66
+ if (names.includes(this.tableName)) {
67
+ await conn.dropTable(this.tableName);
68
+ }
69
+ this.table = null;
70
+ }
71
+ toRow(r) {
72
+ return {
73
+ id: r.id,
74
+ vector: r.vector,
75
+ text: r.text,
76
+ title: r.title,
77
+ url: r.url ?? "",
78
+ source: r.source,
79
+ documentId: r.documentId,
80
+ chunkIndex: r.chunkIndex,
81
+ metadata: r.metadata ? JSON.stringify(r.metadata) : void 0
82
+ };
83
+ }
84
+ async upsert(records) {
85
+ if (records.length === 0) return;
86
+ const conn = await this.connect();
87
+ const rows = records.map((r) => this.toRow(r));
88
+ if (!this.table) {
89
+ const names = await conn.tableNames();
90
+ if (names.includes(this.tableName)) {
91
+ this.table = await conn.openTable(this.tableName);
92
+ await this.table.add(rows);
93
+ } else {
94
+ this.table = await conn.createTable(this.tableName, rows);
95
+ }
96
+ } else {
97
+ await this.table.add(rows);
98
+ }
99
+ }
100
+ async count() {
101
+ if (!this.table) {
102
+ await this.init();
103
+ if (!this.table) return 0;
104
+ }
105
+ try {
106
+ return await this.table.countRows();
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ async query(vector, topK) {
112
+ if (!this.table) {
113
+ await this.init();
114
+ if (!this.table) return [];
115
+ }
116
+ const rows = await this.table.search(vector).limit(topK).toArray();
117
+ return rows.map((row) => ({
118
+ id: row.id,
119
+ // Convert L2 distance → a 0..1 similarity-ish score for display.
120
+ score: typeof row._distance === "number" ? 1 / (1 + row._distance) : 0,
121
+ text: row.text,
122
+ title: row.title,
123
+ url: row.url || void 0,
124
+ documentId: row.documentId,
125
+ metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : void 0
126
+ }));
127
+ }
128
+ async testConnection(dimensions) {
129
+ if (this.config.mode !== "cloud") {
130
+ try {
131
+ await this.connect();
132
+ return;
133
+ } catch (err) {
134
+ throw new Error(`Failed to connect to local LanceDB: ${err.message}`);
135
+ }
136
+ }
137
+ let conn;
138
+ try {
139
+ conn = await this.connect();
140
+ } catch (err) {
141
+ throw new Error(`Failed to connect to LanceDB Cloud: ${err.message}`);
142
+ }
143
+ try {
144
+ await conn.tableNames();
145
+ } catch (err) {
146
+ const msg = err.message ?? "";
147
+ if (msg.includes("401") || msg.includes("Unauthorized") || msg.includes("403") || msg.includes("Forbidden")) {
148
+ throw new Error(
149
+ "Invalid LanceDB Cloud API key. Please check your credentials in the LanceDB Cloud dashboard."
150
+ );
151
+ }
152
+ if (msg.includes("404") || msg.includes("Not Found")) {
153
+ throw new Error(
154
+ `LanceDB Cloud database not found. Please verify your URI (e.g. "db://my-database").`
155
+ );
156
+ }
157
+ throw new Error(`Failed to reach LanceDB Cloud: ${msg}`);
158
+ }
159
+ }
160
+ };
161
+ export {
162
+ LanceDBAdapter
163
+ };
@@ -0,0 +1,315 @@
1
+ import "./chunk-3RG5ZIWI.js";
2
+
3
+ // ../../packages/vector-stores/src/adapters/pinecone.ts
4
+ import {
5
+ Pinecone
6
+ } from "@pinecone-database/pinecone";
7
+ var SPARSE_BATCH = 48;
8
+ var BASE_INTER_BATCH_DELAY_MS = 7e3;
9
+ var RATE_LIMIT_WAIT_MS = 65e3;
10
+ var RATE_LIMIT_MAX_RETRIES = 3;
11
+ var RRF_K = 60;
12
+ function sleep(ms) {
13
+ return new Promise((r) => setTimeout(r, ms));
14
+ }
15
+ function is429(err) {
16
+ const e = err;
17
+ return e?.status === 429 || e?.statusCode === 429 || String(e?.message ?? "").includes("429") || String(e?.message ?? "").includes("RESOURCE_EXHAUSTED");
18
+ }
19
+ var PineconeAdapter = class {
20
+ constructor(config) {
21
+ this.config = config;
22
+ if (!config.apiKey) throw new Error("Pinecone requires an API key.");
23
+ if (!config.indexName) throw new Error("Pinecone requires an index name.");
24
+ this.indexName = config.indexName.trim();
25
+ this.namespace = config.namespace?.trim() || "default";
26
+ }
27
+ config;
28
+ client = null;
29
+ index = null;
30
+ indexName;
31
+ namespace;
32
+ currentInterBatchDelayMs = 0;
33
+ getClient() {
34
+ if (!this.client) {
35
+ this.client = new Pinecone({ apiKey: this.config.apiKey });
36
+ }
37
+ return this.client;
38
+ }
39
+ get needsSparse() {
40
+ return this.config.indexType === "lexical" || this.config.indexType === "hybrid";
41
+ }
42
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
43
+ async init(dimensions) {
44
+ const pc = this.getClient();
45
+ const { indexes } = await pc.listIndexes();
46
+ const exists = (indexes ?? []).some((i) => i.name === this.indexName);
47
+ if (!exists) {
48
+ const metric = this.needsSparse ? "dotproduct" : "cosine";
49
+ await pc.createIndex({
50
+ name: this.indexName,
51
+ dimension: dimensions,
52
+ metric,
53
+ spec: { serverless: { cloud: "aws", region: "us-east-1" } },
54
+ waitUntilReady: true
55
+ });
56
+ }
57
+ this.index = pc.index(this.indexName);
58
+ }
59
+ ns() {
60
+ if (!this.index) this.index = this.getClient().index(this.indexName);
61
+ return this.index.namespace(this.namespace);
62
+ }
63
+ async reset() {
64
+ try {
65
+ await this.ns().deleteAll();
66
+ } catch {
67
+ }
68
+ }
69
+ // ── Sparse vector generation with rate-limit handling ─────────────────────
70
+ /**
71
+ * Call Pinecone Inference API for one batch of texts, retrying on 429.
72
+ * Before each retry-sleep the `onRateLimit` hook is called so the indexer
73
+ * can patch the run store with a warning that the UI polls.
74
+ */
75
+ async embedSparseWithRetry(texts) {
76
+ const pc = this.getClient();
77
+ for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES + 1; attempt++) {
78
+ try {
79
+ const result = await pc.inference.embed({
80
+ model: this.config.sparseModel,
81
+ inputs: texts,
82
+ parameters: { input_type: "passage", truncate: "END" }
83
+ });
84
+ return result.data.map((emb) => {
85
+ const s = emb;
86
+ return {
87
+ indices: s.sparseIndices ?? [],
88
+ values: s.sparseValues ?? []
89
+ };
90
+ });
91
+ } catch (err) {
92
+ if (is429(err) && attempt <= RATE_LIMIT_MAX_RETRIES) {
93
+ const waitSecs = Math.round(RATE_LIMIT_WAIT_MS / 1e3);
94
+ await this.config.onRateLimit?.(waitSecs, attempt);
95
+ await sleep(RATE_LIMIT_WAIT_MS);
96
+ this.currentInterBatchDelayMs = Math.max(
97
+ this.currentInterBatchDelayMs,
98
+ BASE_INTER_BATCH_DELAY_MS
99
+ );
100
+ continue;
101
+ }
102
+ throw err;
103
+ }
104
+ }
105
+ throw new Error("Sparse embedding failed after max retries.");
106
+ }
107
+ async buildSparseVectors(texts) {
108
+ const result = [];
109
+ for (let i = 0; i < texts.length; i += SPARSE_BATCH) {
110
+ const batch = texts.slice(i, i + SPARSE_BATCH);
111
+ const vecs = await this.embedSparseWithRetry(batch);
112
+ result.push(...vecs);
113
+ if (i + SPARSE_BATCH < texts.length && this.currentInterBatchDelayMs > 0) {
114
+ await sleep(this.currentInterBatchDelayMs);
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+ async buildQuerySparseVector(text) {
120
+ const pc = this.getClient();
121
+ for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES + 1; attempt++) {
122
+ try {
123
+ const result = await pc.inference.embed({
124
+ model: this.config.sparseModel,
125
+ inputs: [text],
126
+ parameters: { input_type: "query", truncate: "END" }
127
+ });
128
+ const s = result.data[0];
129
+ return {
130
+ indices: s.sparseIndices ?? [],
131
+ values: s.sparseValues ?? []
132
+ };
133
+ } catch (err) {
134
+ if (is429(err) && attempt <= RATE_LIMIT_MAX_RETRIES) {
135
+ await sleep(RATE_LIMIT_WAIT_MS);
136
+ continue;
137
+ }
138
+ throw err;
139
+ }
140
+ }
141
+ throw new Error("Sparse query embedding failed after max retries.");
142
+ }
143
+ // ── Upsert ─────────────────────────────────────────────────────────────────
144
+ async upsert(records) {
145
+ if (records.length === 0) return;
146
+ const sanitizeMeta = (meta) => {
147
+ if (!meta) return {};
148
+ const safe = {};
149
+ for (const [k, v] of Object.entries(meta)) {
150
+ if (v === null || v === void 0) continue;
151
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
152
+ safe[k] = v;
153
+ } else if (Array.isArray(v) && v.every((item) => typeof item === "string")) {
154
+ safe[k] = v;
155
+ } else {
156
+ safe[k] = JSON.stringify(v);
157
+ }
158
+ }
159
+ return safe;
160
+ };
161
+ const metaOf = (r) => ({
162
+ ...sanitizeMeta(r.metadata),
163
+ text: r.text,
164
+ title: r.title,
165
+ url: r.url ?? "",
166
+ source: r.source,
167
+ documentId: r.documentId,
168
+ chunkIndex: r.chunkIndex
169
+ });
170
+ if (!this.needsSparse || !this.config.sparseModel) {
171
+ await this.ns().upsert({
172
+ records: records.map((r) => ({
173
+ id: r.id,
174
+ values: r.vector,
175
+ metadata: metaOf(r)
176
+ }))
177
+ });
178
+ return;
179
+ }
180
+ const sparseVecs = await this.buildSparseVectors(
181
+ records.map((r) => r.text)
182
+ );
183
+ await this.ns().upsert({
184
+ records: records.map((r, i) => ({
185
+ id: r.id,
186
+ values: r.vector,
187
+ sparseValues: sparseVecs[i],
188
+ metadata: metaOf(r)
189
+ }))
190
+ });
191
+ }
192
+ // ── Query ──────────────────────────────────────────────────────────────────
193
+ async query(vector, topK, queryText) {
194
+ const canHybrid = this.needsSparse && this.config.sparseModel && queryText;
195
+ if (!canHybrid) {
196
+ return this.denseQuery(vector, topK);
197
+ }
198
+ const fetchN = Math.min(topK * 2, 100);
199
+ const [denseHits, sparseHits] = await Promise.all([
200
+ this.denseQuery(vector, fetchN),
201
+ this.sparseQuery(vector, queryText, fetchN)
202
+ ]);
203
+ return rrfMerge(denseHits, sparseHits, topK);
204
+ }
205
+ async denseQuery(vector, topK) {
206
+ const res = await this.ns().query({ vector, topK, includeMetadata: true });
207
+ return (res.matches ?? []).map(hitFromMatch);
208
+ }
209
+ /**
210
+ * Sparse-weighted query for hybrid search.
211
+ *
212
+ * Pinecone's dotproduct index ALWAYS requires a non-empty dense `vector`
213
+ * array — even when the call is primarily sparse. Passing `[]` triggers the
214
+ * "You must enter an array of RecordValues" error. We pass the real dense
215
+ * query vector here; Pinecone's scoring blends both dimensions and the RRF
216
+ * merge downstream takes care of the final ranking.
217
+ */
218
+ async sparseQuery(denseVector, queryText, topK) {
219
+ const sparseVector = await this.buildQuerySparseVector(queryText);
220
+ const res = await this.ns().query({
221
+ vector: denseVector,
222
+ sparseVector,
223
+ topK,
224
+ includeMetadata: true
225
+ });
226
+ return (res.matches ?? []).map(hitFromMatch);
227
+ }
228
+ // ── Count ──────────────────────────────────────────────────────────────────
229
+ async count() {
230
+ try {
231
+ const stats = await this.ns().describeIndexStats();
232
+ return stats.namespaces?.[this.namespace]?.recordCount ?? stats.totalRecordCount ?? 0;
233
+ } catch {
234
+ return null;
235
+ }
236
+ }
237
+ // ── Connection test ────────────────────────────────────────────────────────
238
+ async testConnection(dimensions) {
239
+ const pc = this.getClient();
240
+ let indexList;
241
+ try {
242
+ indexList = await pc.listIndexes();
243
+ } catch (err) {
244
+ if (err.message?.toLowerCase().includes("api key") || err.name === "PineconeAuthorizationError" || err.status === 401) {
245
+ throw new Error("Invalid Pinecone API key.");
246
+ }
247
+ throw new Error(`Failed to connect to Pinecone: ${err.message}`);
248
+ }
249
+ const indexModel = (indexList.indexes ?? []).find(
250
+ (i) => i.name === this.indexName
251
+ );
252
+ if (!indexModel) {
253
+ throw new Error(
254
+ `Index "${this.indexName}" does not exist in your Pinecone project. Please create it first.`
255
+ );
256
+ }
257
+ if (indexModel.dimension !== dimensions) {
258
+ throw new Error(
259
+ `Dimension mismatch: Index "${this.indexName}" has dimension ${indexModel.dimension}, but the selected embedding model requires dimension ${dimensions}.`
260
+ );
261
+ }
262
+ if (this.needsSparse) {
263
+ if (!this.config.sparseModel) {
264
+ throw new Error(
265
+ "Hybrid/lexical search requires a sparse model to be selected."
266
+ );
267
+ }
268
+ const metric = indexModel.metric ?? indexModel.spec?.metric;
269
+ if (metric && metric !== "dotproduct") {
270
+ throw new Error(
271
+ `Hybrid/lexical requires your Pinecone index to use the "dotproduct" metric, but "${this.indexName}" uses "${metric}". Please delete and recreate the index with metric = dotproduct.`
272
+ );
273
+ }
274
+ }
275
+ }
276
+ };
277
+ function hitFromMatch(m) {
278
+ const meta = m.metadata ?? {};
279
+ const {
280
+ text,
281
+ title,
282
+ url,
283
+ source,
284
+ documentId,
285
+ chunkIndex,
286
+ ...customMetadata
287
+ } = meta;
288
+ return {
289
+ id: m.id,
290
+ score: m.score ?? 0,
291
+ text: text ?? "",
292
+ title: title ?? "Untitled",
293
+ url: url || void 0,
294
+ documentId: documentId ?? "",
295
+ metadata: Object.keys(customMetadata).length > 0 ? customMetadata : void 0
296
+ };
297
+ }
298
+ function rrfMerge(denseHits, sparseHits, topK) {
299
+ const scores = /* @__PURE__ */ new Map();
300
+ const addList = (hits) => hits.forEach((hit, rank) => {
301
+ const contrib = 1 / (RRF_K + rank + 1);
302
+ const prev = scores.get(hit.id);
303
+ if (prev) {
304
+ prev.score += contrib;
305
+ } else {
306
+ scores.set(hit.id, { hit, score: contrib });
307
+ }
308
+ });
309
+ addList(denseHits);
310
+ addList(sparseHits);
311
+ return [...scores.values()].sort((a, b) => b.score - a.score).slice(0, topK).map(({ hit, score }) => ({ ...hit, score }));
312
+ }
313
+ export {
314
+ PineconeAdapter
315
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@larkup/cli",
3
+ "version": "0.1.14",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "bin": {
9
+ "larkup": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
+ "larkup": "tsx src/index.ts"
18
+ },
19
+ "dependencies": {
20
+ "@ai-sdk/cohere": "^3.0.39",
21
+ "@ai-sdk/deepseek": "^2.0.39",
22
+ "@ai-sdk/gateway": "^3.0.133",
23
+ "@ai-sdk/google": "^3.0.83",
24
+ "@ai-sdk/mistral": "^3.0.40",
25
+ "@ai-sdk/openai": "^3.0.68",
26
+ "@ai-sdk/openai-compatible": "^2.0.51",
27
+ "@clack/prompts": "^0.10.1",
28
+ "@lancedb/lancedb": "^0.30.0",
29
+ "@larkup/core": "workspace:*",
30
+ "@larkup/vector-stores": "workspace:*",
31
+ "@pinecone-database/pinecone": "^7.2.0",
32
+ "ai": "^6.0.197",
33
+ "chalk": "^5.6.2",
34
+ "chromadb": "^1.10.5",
35
+ "cli-table3": "^0.6.5",
36
+ "commander": "^13.1.0",
37
+ "zod": "^4.4.3"
38
+ },
39
+ "devDependencies": {
40
+ "tsup": "^8.0.0",
41
+ "tsx": "^4.22.4",
42
+ "typescript": "5.7.3"
43
+ }
44
+ }