@larkup/cli 0.2.27 → 0.2.29

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.
@@ -3,7 +3,7 @@ import {
3
3
  getAnalyticsSummary,
4
4
  readUsageEvents,
5
5
  trackUsageEvent
6
- } from "./chunk-JZ3UCEGF.js";
6
+ } from "./chunk-T2XKBURE.js";
7
7
  import "./chunk-QGM4M3NI.js";
8
8
  export {
9
9
  estimateCost,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getToolById,
3
3
  invalidateRegistryCache
4
- } from "./chunk-5UWKP64Q.js";
4
+ } from "./chunk-YUC4VMEE.js";
5
5
  import {
6
6
  __require
7
7
  } from "./chunk-QGM4M3NI.js";
@@ -27,7 +27,8 @@ function serializePackageMutation(fn) {
27
27
  return run;
28
28
  }
29
29
  function getToolsDir() {
30
- return path.join(process.cwd(), ".larkup", "tools");
30
+ const dataDir = process.env.LARKUP_DATA_DIR?.trim();
31
+ return path.join(dataDir ? path.resolve(dataDir) : path.join(process.cwd(), ".larkup"), "tools");
31
32
  }
32
33
  function getManifestPath() {
33
34
  return path.join(getToolsDir(), "installed.json");
@@ -451,7 +452,7 @@ async function uninstallTool(toolId) {
451
452
  }
452
453
  }
453
454
  invalidateRegistryCache();
454
- const { unloadTool: unloadTool2 } = await import("./tool-loader-PEQZZWTB.js");
455
+ const { unloadTool: unloadTool2 } = await import("./tool-loader-2WHU3H77.js");
455
456
  unloadTool2(toolId);
456
457
  } finally {
457
458
  uninstallingTools.delete(toolId);
@@ -575,7 +576,7 @@ function unloadTool(toolId) {
575
576
  moduleCache.delete(toolId);
576
577
  }
577
578
  async function hasCapability(capability) {
578
- const { getToolsWithCapability } = await import("./tool-registry-VJYLRXKJ.js");
579
+ const { getToolsWithCapability } = await import("./tool-registry-JS5NUNN6.js");
579
580
  const tools = await getToolsWithCapability(capability);
580
581
  for (const t of tools) {
581
582
  const installed = await getInstalledTool(t.id);
@@ -55,12 +55,17 @@ var DEFAULT_CONFIG = {
55
55
  };
56
56
 
57
57
  // ../../packages/core/src/project-store.ts
58
- var LARKUP_ROOT = path.join(process.cwd(), ".larkup");
59
- var PROJECTS_DIR = path.join(LARKUP_ROOT, "projects");
60
58
  var BASE_PORT = 8080;
61
59
  var EMPTY_WORKSPACE = { activeProjectId: null, projects: [] };
62
60
  var projectScope = new AsyncLocalStorage();
63
61
  var writeChain = Promise.resolve();
62
+ function getLarkupDataDir() {
63
+ const configured = process.env.LARKUP_DATA_DIR?.trim();
64
+ return configured ? path.resolve(configured) : path.join(process.cwd(), ".larkup");
65
+ }
66
+ function projectsDir() {
67
+ return path.join(getLarkupDataDir(), "projects");
68
+ }
64
69
  function serialize(operation) {
65
70
  const run = writeChain.then(operation, operation);
66
71
  writeChain = run.catch(() => void 0);
@@ -68,12 +73,12 @@ function serialize(operation) {
68
73
  }
69
74
  async function readWorkspaceFile() {
70
75
  try {
71
- const entries = await fs.readdir(PROJECTS_DIR, { withFileTypes: true });
76
+ const entries = await fs.readdir(projectsDir(), { withFileTypes: true });
72
77
  const projects = await Promise.all(
73
78
  entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
74
79
  try {
75
80
  return JSON.parse(
76
- await fs.readFile(path.join(PROJECTS_DIR, entry.name, "project.json"), "utf8")
81
+ await fs.readFile(path.join(projectsDir(), entry.name, "project.json"), "utf8")
77
82
  );
78
83
  } catch {
79
84
  return null;
@@ -91,7 +96,7 @@ async function readWorkspaceFile() {
91
96
  }
92
97
  }
93
98
  async function writeWorkspaceFile(workspace) {
94
- await fs.mkdir(PROJECTS_DIR, { recursive: true });
99
+ await fs.mkdir(projectsDir(), { recursive: true });
95
100
  await Promise.all(
96
101
  workspace.projects.map(async (project) => {
97
102
  const persisted = { ...project, active: project.id === workspace.activeProjectId };
@@ -122,13 +127,14 @@ function slugify(name) {
122
127
  return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "my-larkup";
123
128
  }
124
129
  function initialConfig(project) {
130
+ const configuredDataRoot = process.env.LARKUP_DATA_DIR?.trim();
125
131
  return {
126
132
  ...DEFAULT_CONFIG,
127
133
  projectName: slugify(project.name),
128
134
  storeConfig: {
129
135
  ...DEFAULT_CONFIG.storeConfig,
130
136
  mode: "local",
131
- dbPath: `./.larkup/projects/${project.id}/index`
137
+ dbPath: configuredDataRoot ? path.join(getLarkupDataDir(), "projects", project.id, "index") : `./.larkup/projects/${project.id}/index`
132
138
  },
133
139
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
134
140
  };
@@ -137,7 +143,7 @@ function runWithProject(projectId, operation) {
137
143
  return projectScope.run({ projectId }, operation);
138
144
  }
139
145
  function projectDir(projectId) {
140
- return path.join(PROJECTS_DIR, projectId);
146
+ return path.join(projectsDir(), projectId);
141
147
  }
142
148
  async function getProjectWorkspace() {
143
149
  return readWorkspaceFile();
@@ -177,21 +183,35 @@ function createProject(name) {
177
183
  const dir = projectDir(project.id);
178
184
  await fs.mkdir(path.join(dir, "releases"), { recursive: true });
179
185
  await Promise.all([
180
- fs.writeFile(path.join(dir, "project.json"), JSON.stringify({ ...project, active: true }, null, 2), "utf8"),
186
+ fs.writeFile(
187
+ path.join(dir, "project.json"),
188
+ JSON.stringify({ ...project, active: true }, null, 2),
189
+ "utf8"
190
+ ),
181
191
  fs.writeFile(
182
192
  path.join(dir, "config.json"),
183
193
  JSON.stringify(initialConfig(project), null, 2),
184
194
  "utf8"
185
195
  ),
186
- fs.writeFile(path.join(dir, "groups.json"), JSON.stringify([{
187
- id: "default",
188
- name: "Default",
189
- description: "Sources added without a specific group.",
190
- icon: "\u{1F4DA}",
191
- assistantEnabled: true,
192
- createdAt: now,
193
- updatedAt: now
194
- }], null, 2), "utf8"),
196
+ fs.writeFile(
197
+ path.join(dir, "groups.json"),
198
+ JSON.stringify(
199
+ [
200
+ {
201
+ id: "default",
202
+ name: "Default",
203
+ description: "Sources added without a specific group.",
204
+ icon: "\u{1F4DA}",
205
+ assistantEnabled: true,
206
+ createdAt: now,
207
+ updatedAt: now
208
+ }
209
+ ],
210
+ null,
211
+ 2
212
+ ),
213
+ "utf8"
214
+ ),
195
215
  fs.writeFile(path.join(dir, "documents.json"), "[]\n", "utf8"),
196
216
  fs.writeFile(path.join(dir, "deployments.json"), "[]\n", "utf8"),
197
217
  fs.writeFile(path.join(dir, "automations.json"), "[]\n", "utf8"),
@@ -214,11 +234,12 @@ function setActiveProject(projectId) {
214
234
  });
215
235
  }
216
236
  async function resetLocalProjects(confirmedPath) {
217
- if (confirmedPath !== PROJECTS_DIR) {
218
- throw new Error(`Confirmation must exactly match ${PROJECTS_DIR}.`);
237
+ const target = projectsDir();
238
+ if (confirmedPath !== target) {
239
+ throw new Error(`Confirmation must exactly match ${target}.`);
219
240
  }
220
241
  await serialize(async () => {
221
- await fs.rm(PROJECTS_DIR, { recursive: true, force: true });
242
+ await fs.rm(target, { recursive: true, force: true });
222
243
  });
223
244
  }
224
245
 
@@ -7010,6 +7031,7 @@ async function getAnalyticsSummary(days, runtime = "all") {
7010
7031
 
7011
7032
  export {
7012
7033
  DEFAULT_CONFIG,
7034
+ getLarkupDataDir,
7013
7035
  runWithProject,
7014
7036
  getProjectWorkspace,
7015
7037
  getActiveProject,
@@ -0,0 +1,776 @@
1
+ // ../../packages/marketplace/src/tool-registry.ts
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
4
+
5
+ // ../../packages/marketplace/src/types.ts
6
+ var DEFAULT_HUB_URL = process.env.LARKUP_HUB_URL ?? "https://hub.larkup.de";
7
+ var STORAGE_WARNING_THRESHOLDS = [
8
+ 1 * 1024 * 1024 * 1024,
9
+ // 1 GB
10
+ 5 * 1024 * 1024 * 1024,
11
+ // 5 GB
12
+ 10 * 1024 * 1024 * 1024
13
+ // 10 GB
14
+ ];
15
+
16
+ // ../../packages/marketplace/src/db/builtin-tools.ts
17
+ var BUILTIN_TOOLS = [
18
+ {
19
+ manifestVersion: "3.0",
20
+ kind: "tool",
21
+ id: "video-intelligence",
22
+ name: "Video Intelligence",
23
+ description: "Help your AI understand your videos, so you can ask questions and get answers from the right moments.",
24
+ longDescription: "Turn videos into knowledge your AI can understand. It listens to what is said, reads what appears on screen, and helps you find the moments that matter.",
25
+ category: "media",
26
+ version: "0.2.2",
27
+ pricing: "free",
28
+ emoji: "\u{1F3AC}",
29
+ icon: "Film",
30
+ packageName: "@larkup/tool-video-intelligence",
31
+ installSize: "~120 KB",
32
+ requiresSandbox: false,
33
+ author: "Larkup",
34
+ capabilities: [
35
+ "video-indexing",
36
+ "video-indexing-brief",
37
+ "agentic-video-indexing",
38
+ "cloud-video-runtime",
39
+ "timestamped-transcription",
40
+ "ocr",
41
+ "object-detection",
42
+ "anonymous-object-tracking",
43
+ "usage-metering"
44
+ ],
45
+ entrypoints: { server: ".", agent: ".", ui: "./ui" },
46
+ runtime: {
47
+ protocolVersion: "1",
48
+ defaultMode: "managed-cloud",
49
+ healthPath: "/v1/health",
50
+ modes: [
51
+ {
52
+ id: "managed-cloud",
53
+ label: "Larkup Cloud",
54
+ description: "Index with managed private GPU capacity and your Larkup Cloud allowance.",
55
+ requiresGpu: true
56
+ },
57
+ {
58
+ id: "local",
59
+ label: "Local runtime",
60
+ icon: "/docker.png",
61
+ description: "Run the application locally on your computer.",
62
+ endpointConfigKey: "localRuntimeUrl",
63
+ credentialConfigKey: "localRuntimeApiKey",
64
+ image: "ghcr.io/larkup-ai/video-intelligence:0.1.0",
65
+ composeFile: "compose.yaml",
66
+ setupNotice: "The local application and its models download to this computer. The initial download can be large."
67
+ },
68
+ {
69
+ id: "custom-remote",
70
+ label: "Custom runtime",
71
+ description: "Connect a compatible Video Intelligence deployment you operate.",
72
+ endpointConfigKey: "customRuntimeUrl",
73
+ credentialConfigKey: "customRuntimeApiKey",
74
+ requiresGpu: true
75
+ }
76
+ ]
77
+ },
78
+ billing: {
79
+ model: "hybrid",
80
+ entitlementVersion: "1",
81
+ meters: [
82
+ { id: "source-minute", unit: "minute", description: "Minutes of source video." },
83
+ {
84
+ id: "thorough-inspection-minute",
85
+ unit: "minute",
86
+ description: "Minutes inspected with Thorough agent coverage."
87
+ },
88
+ { id: "gpu-second", unit: "second", description: "GPU worker seconds." },
89
+ { id: "ocr-frame", unit: "frame", description: "Frames processed by OCR." }
90
+ ]
91
+ },
92
+ usage: {
93
+ label: "Usage this month",
94
+ visibleWhenRuntimeMode: ["managed-cloud"],
95
+ visualization: { usedKey: "sourceMinutesUsed", limitKey: "sourceMinutesLimit" },
96
+ support: {
97
+ contactLabel: "Request more usage",
98
+ description: "Each installation receives Cloud access automatically. Request more video-processing usage when your allowance is low.",
99
+ userIdConfigKey: "cloudInstallationId"
100
+ },
101
+ fields: [
102
+ { key: "sourceMinutesUsed", label: "Used", format: "minutes" },
103
+ { key: "sourceMinutesLimit", label: "Included", format: "minutes" },
104
+ { key: "activeJobs", label: "Active jobs", format: "number" }
105
+ ]
106
+ },
107
+ ui: {
108
+ isolation: "sandboxed-iframe",
109
+ surfaces: [
110
+ {
111
+ id: "video-indexing-brief",
112
+ slot: "data-indexing",
113
+ title: "Let your AI understand this video",
114
+ entrypoint: "./ui",
115
+ appliesTo: ["video"],
116
+ description: "Choose how deeply to analyze it and optionally point it to what matters most.",
117
+ estimate: {
118
+ modeField: "indexingMode",
119
+ variants: [
120
+ {
121
+ value: "fast",
122
+ analyzedFramesPerSourceMinute: 5,
123
+ ocrFramesPerSourceMinute: 3,
124
+ processingSecondsPerSourceMinute: 8,
125
+ maxProcessingSecondsPerSourceMinute: 15,
126
+ fixedOverheadSeconds: 90,
127
+ maxFixedOverheadSeconds: 180,
128
+ creditsPerSourceMinute: 1
129
+ },
130
+ {
131
+ value: "balanced",
132
+ analyzedFramesPerSourceMinute: 12,
133
+ ocrFramesPerSourceMinute: 8,
134
+ processingSecondsPerSourceMinute: 16,
135
+ maxProcessingSecondsPerSourceMinute: 30,
136
+ fixedOverheadSeconds: 120,
137
+ maxFixedOverheadSeconds: 240,
138
+ creditsPerSourceMinute: 2
139
+ },
140
+ {
141
+ value: "thorough",
142
+ analyzedFramesPerSourceMinute: 30,
143
+ ocrFramesPerSourceMinute: 20,
144
+ processingSecondsPerSourceMinute: 32,
145
+ maxProcessingSecondsPerSourceMinute: 60,
146
+ fixedOverheadSeconds: 180,
147
+ maxFixedOverheadSeconds: 360,
148
+ creditsPerSourceMinute: 4
149
+ }
150
+ ]
151
+ },
152
+ form: {
153
+ submitLabel: "Start indexing",
154
+ fields: [
155
+ {
156
+ key: "goal",
157
+ type: "textarea",
158
+ label: "What should your AI look for? (optional)",
159
+ placeholder: "For example: find the final score, the moment the package is dropped, or every mention of pricing."
160
+ },
161
+ {
162
+ key: "indexingMode",
163
+ type: "select",
164
+ label: "Coverage",
165
+ defaultValue: "balanced",
166
+ options: [
167
+ {
168
+ label: "Fast",
169
+ value: "fast",
170
+ description: "Sample key frames for a quick overview.",
171
+ setValues: { processingAuthorityConfirmed: false }
172
+ },
173
+ {
174
+ label: "Balanced",
175
+ value: "balanced",
176
+ description: "Sample visual and OCR evidence across the video.",
177
+ setValues: { processingAuthorityConfirmed: false }
178
+ },
179
+ {
180
+ label: "Thorough",
181
+ value: "thorough",
182
+ description: "Let the agent inspect denser evidence around subtle or infrequent details.",
183
+ setValues: { processingAuthorityConfirmed: false }
184
+ }
185
+ ]
186
+ }
187
+ ]
188
+ }
189
+ },
190
+ {
191
+ id: "video-evidence-result",
192
+ slot: "chat-result",
193
+ title: "Video evidence",
194
+ entrypoint: "./ui"
195
+ }
196
+ ]
197
+ },
198
+ tags: ["video", "ocr", "tracking", "local", "gpu", "private", "gdpr"],
199
+ downloads: 0,
200
+ repositoryUrl: "https://github.com/Larkup-AI/larkup",
201
+ license: "Apache-2.0",
202
+ updatedAt: "2026-08-20",
203
+ trustLevel: "elevated",
204
+ permissions: {
205
+ fsRead: true,
206
+ fsWrite: true,
207
+ exec: true,
208
+ httpAllow: ["127.0.0.1", "localhost", "video.larkup.de"]
209
+ },
210
+ configSchema: [
211
+ {
212
+ key: "runtimeMode",
213
+ label: "Runtime",
214
+ type: "select",
215
+ group: "Runtime",
216
+ defaultValue: "managed-cloud",
217
+ help: "Choose managed Larkup Cloud, a local runtime that auto-selects Docker or native, or your compatible deployment.",
218
+ options: [
219
+ {
220
+ label: "Larkup Cloud",
221
+ value: "managed-cloud",
222
+ icon: "/logo.png",
223
+ description: "Managed GPU capacity and automatic device access."
224
+ },
225
+ {
226
+ label: "Local runtime",
227
+ value: "local",
228
+ icon: "/docker.png",
229
+ description: "Run the application locally on your computer."
230
+ },
231
+ {
232
+ label: "Custom runtime",
233
+ value: "custom-remote",
234
+ icon: "/icons/cloudflare.png",
235
+ description: "Use your deployment URL and key."
236
+ }
237
+ ]
238
+ },
239
+ {
240
+ key: "localRuntimeUrl",
241
+ label: "Local runtime URL",
242
+ type: "text",
243
+ group: "Local runtime",
244
+ defaultValue: "http://127.0.0.1:8787",
245
+ help: "Use a LAN address to share the runtime with trusted users.",
246
+ verification: {
247
+ endpoint: "/api/tools/video-intelligence/verify",
248
+ method: "POST",
249
+ fields: {
250
+ localRuntimeUrl: "localRuntimeUrl",
251
+ localRuntimeApiKey: "localRuntimeApiKey",
252
+ runtimeMode: "runtimeMode"
253
+ }
254
+ },
255
+ visibleWhen: { field: "runtimeMode", equals: "local" }
256
+ },
257
+ {
258
+ key: "localRuntimeApiKey",
259
+ label: "Shared local API key",
260
+ type: "password",
261
+ group: "Local runtime",
262
+ help: "Optional bearer key when local runtime authentication is enabled.",
263
+ visibleWhen: { field: "runtimeMode", equals: "local" }
264
+ },
265
+ {
266
+ key: "videoVisionProvider",
267
+ label: "Vision provider",
268
+ type: "select",
269
+ group: "Video understanding",
270
+ defaultValue: "vercel_ai_gateway",
271
+ defaultFromGlobalConfigKey: "visionProvider",
272
+ help: "Defaults to your saved AI Models vision provider. Choose another provider here only for this tool.",
273
+ options: [
274
+ {
275
+ label: "Use AI Models",
276
+ value: "auto",
277
+ description: "Follow the workspace Vision Model setting."
278
+ },
279
+ { label: "Google Gemini", value: "google", icon: "/icons/gemini.svg" },
280
+ {
281
+ label: "Vercel AI Gateway",
282
+ value: "vercel_ai_gateway",
283
+ icon: "/icons/vercel.svg"
284
+ },
285
+ { label: "OpenAI", value: "openai", icon: "/icons/openai.svg" }
286
+ ]
287
+ },
288
+ {
289
+ key: "videoVisionApiKey",
290
+ label: "Vision provider API key",
291
+ type: "password",
292
+ group: "Video understanding",
293
+ help: "Optional tool override. Leave empty to use the matching provider key from AI Models; it is forwarded only for the active job."
294
+ },
295
+ {
296
+ key: "semanticVisionModel",
297
+ label: "Video vision model",
298
+ type: "select",
299
+ group: "Video understanding",
300
+ defaultValue: "google/gemini-3.6-flash",
301
+ defaultFromGlobalConfigKey: "visionModelId",
302
+ providerField: "videoVisionProvider",
303
+ defaultValueByProvider: {
304
+ auto: "auto",
305
+ google: "google/gemini-3.6-flash",
306
+ vercel_ai_gateway: "google/gemini-3.6-flash",
307
+ openai: "openai/gpt-4o-mini"
308
+ },
309
+ help: "Defaults to the Vision Model in AI Models. Choose another video-capable model to override it only for this tool.",
310
+ verification: {
311
+ endpoint: "/api/tools/video-intelligence/verify",
312
+ method: "POST",
313
+ fields: {
314
+ videoVisionProvider: "videoVisionProvider",
315
+ videoVisionApiKey: "videoVisionApiKey",
316
+ semanticVisionModel: "semanticVisionModel"
317
+ }
318
+ },
319
+ options: [
320
+ {
321
+ label: "Use AI Models",
322
+ value: "auto",
323
+ description: "Follow the workspace Vision Model setting."
324
+ },
325
+ {
326
+ label: "Gemini 3.6 Flash",
327
+ value: "google/gemini-3.6-flash",
328
+ icon: "/icons/gemini.svg"
329
+ },
330
+ {
331
+ label: "Gemini 3.5 Flash",
332
+ value: "google/gemini-3.5-flash",
333
+ icon: "/icons/gemini.svg"
334
+ },
335
+ {
336
+ label: "GPT-4o mini",
337
+ value: "openai/gpt-4o-mini",
338
+ icon: "/icons/openai.svg"
339
+ }
340
+ ]
341
+ },
342
+ {
343
+ key: "videoAgentProvider",
344
+ label: "Agent provider",
345
+ type: "select",
346
+ group: "Agent planning",
347
+ defaultValue: "vercel_ai_gateway",
348
+ defaultFromGlobalConfigKey: "chatProvider",
349
+ help: "Defaults to your saved AI Models chat provider. Choose another provider here only for this tool.",
350
+ options: [
351
+ {
352
+ label: "Use AI Models",
353
+ value: "auto",
354
+ description: "Follow the workspace Chat Model setting."
355
+ },
356
+ { label: "Google Gemini", value: "google", icon: "/icons/gemini.svg" },
357
+ {
358
+ label: "Vercel AI Gateway",
359
+ value: "vercel_ai_gateway",
360
+ icon: "/icons/vercel.svg"
361
+ },
362
+ { label: "OpenAI", value: "openai", icon: "/icons/openai.svg" },
363
+ {
364
+ label: "DeepSeek",
365
+ value: "deepseek",
366
+ icon: "https://models.dev/logos/deepseek.svg"
367
+ },
368
+ {
369
+ label: "Anthropic",
370
+ value: "anthropic",
371
+ icon: "https://models.dev/logos/anthropic.svg"
372
+ },
373
+ {
374
+ label: "Mistral",
375
+ value: "mistral",
376
+ icon: "https://models.dev/logos/mistral.svg"
377
+ },
378
+ {
379
+ label: "Cohere",
380
+ value: "cohere",
381
+ icon: "https://models.dev/logos/cohere.svg"
382
+ }
383
+ ]
384
+ },
385
+ {
386
+ key: "videoAgentApiKey",
387
+ label: "Agent provider API key",
388
+ type: "password",
389
+ group: "Agent planning",
390
+ help: "Optional tool override. Leave empty to use the matching chat-provider key from AI Models; it is forwarded only for the active job."
391
+ },
392
+ {
393
+ key: "agentModel",
394
+ label: "Agent / tool-brain model",
395
+ type: "select",
396
+ group: "Agent planning",
397
+ defaultValue: "openai/gpt-5-mini",
398
+ defaultFromGlobalConfigKey: "chatModelId",
399
+ providerField: "videoAgentProvider",
400
+ defaultValueByProvider: {
401
+ auto: "auto",
402
+ google: "google/gemini-3.5-flash-lite",
403
+ vercel_ai_gateway: "openai/gpt-5-mini",
404
+ openai: "openai/gpt-5-mini",
405
+ deepseek: "deepseek/deepseek-v3.2",
406
+ anthropic: "anthropic/claude-sonnet-4",
407
+ mistral: "mistral/mistral-large-3",
408
+ cohere: "cohere/command-a"
409
+ },
410
+ help: "Defaults to the Chat Model in AI Models. Choose another model to override it only for video planning and synthesis.",
411
+ verification: {
412
+ endpoint: "/api/tools/video-intelligence/verify",
413
+ method: "POST",
414
+ fields: {
415
+ videoAgentProvider: "videoAgentProvider",
416
+ videoAgentApiKey: "videoAgentApiKey",
417
+ agentModel: "agentModel"
418
+ }
419
+ },
420
+ options: [
421
+ {
422
+ label: "Use AI Models",
423
+ value: "auto",
424
+ description: "Follow the workspace Chat Model setting."
425
+ },
426
+ {
427
+ label: "Gemini 3.5 Flash Lite",
428
+ value: "google/gemini-3.5-flash-lite",
429
+ icon: "/icons/gemini.svg"
430
+ },
431
+ {
432
+ label: "Gemini 3.6 Flash",
433
+ value: "google/gemini-3.6-flash",
434
+ icon: "/icons/gemini.svg"
435
+ },
436
+ { label: "GPT-5 mini", value: "openai/gpt-5-mini", icon: "/icons/openai.svg" },
437
+ { label: "GPT-5.2", value: "openai/gpt-5.2", icon: "/icons/openai.svg" },
438
+ {
439
+ label: "DeepSeek V3.2",
440
+ value: "deepseek/deepseek-v3.2",
441
+ icon: "https://models.dev/logos/deepseek.svg"
442
+ },
443
+ {
444
+ label: "DeepSeek V4 Pro",
445
+ value: "deepseek/deepseek-v4-pro",
446
+ icon: "https://models.dev/logos/deepseek.svg"
447
+ },
448
+ {
449
+ label: "Claude Sonnet 4",
450
+ value: "anthropic/claude-sonnet-4",
451
+ icon: "https://models.dev/logos/anthropic.svg"
452
+ },
453
+ {
454
+ label: "Mistral Large 3",
455
+ value: "mistral/mistral-large-3",
456
+ icon: "https://models.dev/logos/mistral.svg"
457
+ },
458
+ {
459
+ label: "Command A",
460
+ value: "cohere/command-a",
461
+ icon: "https://models.dev/logos/cohere.svg"
462
+ }
463
+ ]
464
+ },
465
+ {
466
+ key: "customRuntimeUrl",
467
+ label: "Custom runtime URL",
468
+ type: "text",
469
+ group: "Custom runtime",
470
+ required: true,
471
+ verification: {
472
+ endpoint: "/api/tools/video-intelligence/verify",
473
+ method: "POST",
474
+ fields: {
475
+ customRuntimeUrl: "customRuntimeUrl",
476
+ customRuntimeApiKey: "customRuntimeApiKey",
477
+ runtimeMode: "runtimeMode"
478
+ }
479
+ },
480
+ visibleWhen: { field: "runtimeMode", equals: "custom-remote" }
481
+ },
482
+ {
483
+ key: "customRuntimeApiKey",
484
+ label: "Custom runtime API key",
485
+ type: "password",
486
+ group: "Custom runtime",
487
+ required: true,
488
+ visibleWhen: { field: "runtimeMode", equals: "custom-remote" }
489
+ },
490
+ {
491
+ key: "audioProvider",
492
+ label: "Audio provider",
493
+ type: "select",
494
+ group: "Audio",
495
+ defaultValue: "deepgram",
496
+ help: "Required for every runtime. Transcription is billed directly to your selected provider; Larkup Cloud does not supply audio-model credits.",
497
+ options: [
498
+ {
499
+ label: "OpenAI",
500
+ value: "openai",
501
+ icon: "/icons/openai.svg",
502
+ description: "Use your OpenAI transcription allowance."
503
+ },
504
+ {
505
+ label: "Groq",
506
+ value: "groq",
507
+ icon: "https://models.dev/logos/groq.svg",
508
+ description: "Fast hosted Whisper inference."
509
+ },
510
+ {
511
+ label: "Deepgram",
512
+ value: "deepgram",
513
+ icon: "/icons/deepgram.png",
514
+ description: "Speech-to-text with language features."
515
+ },
516
+ {
517
+ label: "ElevenLabs",
518
+ value: "elevenlabs",
519
+ icon: "/icons/elevenlabs.png",
520
+ description: "Use your ElevenLabs speech-to-text account."
521
+ }
522
+ ]
523
+ },
524
+ {
525
+ key: "audioApiKey",
526
+ label: "Audio provider API key",
527
+ type: "password",
528
+ group: "Audio",
529
+ help: "Required. It stays in this installation and is forwarded only for the active video job.",
530
+ verification: {
531
+ endpoint: "/api/tools/video-intelligence/verify",
532
+ method: "POST",
533
+ fields: {
534
+ audioProvider: "audioProvider",
535
+ audioApiKey: "audioApiKey"
536
+ }
537
+ },
538
+ visibleWhen: {
539
+ field: "audioProvider",
540
+ equals: ["openai", "groq", "deepgram", "elevenlabs"]
541
+ }
542
+ },
543
+ {
544
+ key: "cloudAccessKey",
545
+ label: "Larkup Cloud API key",
546
+ type: "password",
547
+ group: "Managed cloud connection",
548
+ help: "Paste the key Larkup provides for managed cloud access. Leave this empty only when automatic project provisioning is enabled.",
549
+ visibleWhen: { field: "runtimeMode", equals: "managed-cloud" }
550
+ },
551
+ {
552
+ key: "defaultIndexingMode",
553
+ label: "Default indexing mode",
554
+ type: "select",
555
+ group: "Indexing",
556
+ defaultValue: "balanced",
557
+ options: [
558
+ { label: "Fast", value: "fast" },
559
+ { label: "Balanced", value: "balanced" },
560
+ { label: "Thorough", value: "thorough" }
561
+ ]
562
+ },
563
+ {
564
+ key: "cloudInstallationId",
565
+ label: "Managed cloud installation ID",
566
+ type: "text",
567
+ group: "Managed cloud connection",
568
+ help: "Provisioned automatically for this project.",
569
+ serverManaged: true
570
+ }
571
+ ]
572
+ },
573
+ {
574
+ id: "clip-embeddings",
575
+ name: "CLIP Image Search",
576
+ description: "Direct image-to-image similarity search using CLIP/SigLIP embeddings.",
577
+ longDescription: "Enable native visual similarity search by embedding images with CLIP or SigLIP models. Search your image library by uploading a query image or describing what you're looking for. Requires downloading a ~200MB model on first use.",
578
+ category: "embedding",
579
+ version: "0.1.0",
580
+ pricing: "free",
581
+ emoji: "\u{1F50D}",
582
+ icon: "ScanEye",
583
+ packageName: "@larkup/tool-clip-embeddings",
584
+ installSize: "~200 MB",
585
+ author: "Larkup",
586
+ capabilities: ["clip-embeddings", "image-similarity"],
587
+ tags: ["clip", "siglip", "image-search", "visual-similarity"],
588
+ repositoryUrl: "https://github.com/Larkup-AI/larkup",
589
+ license: "Apache-2.0",
590
+ updatedAt: "2026-07-20",
591
+ comingSoon: true
592
+ },
593
+ {
594
+ id: "doc-editor",
595
+ name: "Document Editor",
596
+ description: "AI-powered form filling and document editing with Canvas-style live preview.",
597
+ longDescription: "Open PDF, Word, PowerPoint, and text files in a split-view Canvas. AI fills forms using your knowledge base, edits content on request, and previews changes live. Supports PDF form fields (AcroForm), DOCX paragraphs/tables, PPTX slide text, and plain text editing. Uses pdf-lib (native) for PDF and the Docker sandbox for DOCX/PPTX via python-docx/python-pptx.",
598
+ category: "utility",
599
+ version: "0.2.3",
600
+ pricing: "free",
601
+ emoji: "\u{1F4DD}",
602
+ icon: "FileEdit",
603
+ packageName: "@larkup/tool-doc-editor",
604
+ installSize: "~5 MB",
605
+ systemDeps: ["docker"],
606
+ requiresSandbox: true,
607
+ author: "Larkup",
608
+ capabilities: ["document-editing", "form-filling", "document-preview"],
609
+ tags: ["pdf", "docx", "pptx", "form", "canvas", "editor", "fill"],
610
+ repositoryUrl: "https://github.com/Larkup-AI/larkup",
611
+ license: "Apache-2.0",
612
+ updatedAt: "2026-07-20"
613
+ }
614
+ ];
615
+
616
+ // ../../packages/marketplace/src/tool-registry.ts
617
+ var cachedRegistry = null;
618
+ var RETIRED_CATALOG_TOOL_IDS = /* @__PURE__ */ new Set(["video-audio"]);
619
+ function normalizeToolDescriptor(descriptor) {
620
+ return { ...descriptor, requiresSandbox: descriptor.requiresSandbox !== false };
621
+ }
622
+ function compareToolVersions(left, right) {
623
+ const parse = (version) => {
624
+ const [core, suffix = ""] = version.split("-", 2);
625
+ const parts = core.split(".").map((part) => Number.parseInt(part, 10) || 0);
626
+ return { parts, stable: suffix.length === 0 };
627
+ };
628
+ const a = parse(left);
629
+ const b = parse(right);
630
+ const length = Math.max(a.parts.length, b.parts.length);
631
+ for (let index = 0; index < length; index += 1) {
632
+ const difference = (a.parts[index] ?? 0) - (b.parts[index] ?? 0);
633
+ if (difference !== 0) return difference;
634
+ }
635
+ if (a.stable === b.stable) return 0;
636
+ return a.stable ? 1 : -1;
637
+ }
638
+ function mergeToolDescriptors(current, incoming) {
639
+ const merged = { ...current };
640
+ for (const [id, descriptor] of Object.entries(incoming)) {
641
+ const existing = merged[id];
642
+ if (!existing || compareToolVersions(descriptor.version, existing.version) >= 0) {
643
+ merged[id] = descriptor;
644
+ }
645
+ }
646
+ return merged;
647
+ }
648
+ async function discoverLocalManifests() {
649
+ const registry = {};
650
+ const persistentDataDir = process.env.LARKUP_DATA_DIR?.trim();
651
+ const searchPaths = [
652
+ // Monorepo development: packages/marketplace-tools/*
653
+ path.resolve(process.cwd(), "packages", "marketplace-tools"),
654
+ // Next.js dev server runs in apps/web
655
+ path.resolve(process.cwd(), "..", "..", "packages", "marketplace-tools"),
656
+ // Installed tools: .larkup/tools/node_modules/@larkup
657
+ ...persistentDataDir ? [path.resolve(persistentDataDir, "tools", "node_modules", "@larkup")] : [],
658
+ path.resolve(process.cwd(), ".larkup", "tools", "node_modules", "@larkup"),
659
+ path.resolve(process.cwd(), "..", "..", ".larkup", "tools", "node_modules", "@larkup")
660
+ ];
661
+ for (const searchPath of searchPaths) {
662
+ try {
663
+ const entries = await fs.readdir(searchPath, { withFileTypes: true });
664
+ for (const entry of entries) {
665
+ if (!entry.isDirectory()) continue;
666
+ const manifestPath = path.join(searchPath, entry.name, "tool.manifest.json");
667
+ try {
668
+ const raw = await fs.readFile(manifestPath, "utf8");
669
+ const manifest = normalizeToolDescriptor(JSON.parse(raw));
670
+ if (manifest.id && !RETIRED_CATALOG_TOOL_IDS.has(manifest.id)) {
671
+ registry[manifest.id] = manifest;
672
+ }
673
+ } catch {
674
+ }
675
+ }
676
+ } catch {
677
+ }
678
+ }
679
+ return registry;
680
+ }
681
+ async function fetchHubCatalog(hubUrl) {
682
+ const baseUrl = hubUrl ?? DEFAULT_HUB_URL;
683
+ try {
684
+ const controller = new AbortController();
685
+ const timeout = setTimeout(() => controller.abort(), 5e3);
686
+ const res = await fetch(`${baseUrl}/v1/tools`, {
687
+ signal: controller.signal,
688
+ headers: { Accept: "application/json" }
689
+ });
690
+ clearTimeout(timeout);
691
+ if (!res.ok) return {};
692
+ const data = await res.json();
693
+ const registry = {};
694
+ for (const tool of data.tools ?? []) {
695
+ if (tool.id && !RETIRED_CATALOG_TOOL_IDS.has(tool.id)) {
696
+ registry[tool.id] = normalizeToolDescriptor(tool);
697
+ }
698
+ }
699
+ return registry;
700
+ } catch {
701
+ return {};
702
+ }
703
+ }
704
+ async function fetchToolFromHub(toolId, hubUrl) {
705
+ if (RETIRED_CATALOG_TOOL_IDS.has(toolId)) return null;
706
+ const baseUrl = hubUrl ?? DEFAULT_HUB_URL;
707
+ try {
708
+ const controller = new AbortController();
709
+ const timeout = setTimeout(() => controller.abort(), 5e3);
710
+ const res = await fetch(`${baseUrl}/v1/tools/${toolId}`, {
711
+ signal: controller.signal,
712
+ headers: { Accept: "application/json" }
713
+ });
714
+ clearTimeout(timeout);
715
+ if (!res.ok) return null;
716
+ const data = await res.json();
717
+ return data.tool ?? null;
718
+ } catch {
719
+ return null;
720
+ }
721
+ }
722
+ var FALLBACK_REGISTRY = Object.fromEntries(
723
+ BUILTIN_TOOLS.map((tool) => {
724
+ const descriptor = normalizeToolDescriptor(tool);
725
+ return [descriptor.id, descriptor];
726
+ })
727
+ );
728
+ async function buildRegistry(opts) {
729
+ if (cachedRegistry) return cachedRegistry;
730
+ let registry = { ...FALLBACK_REGISTRY };
731
+ let hubAvailable = false;
732
+ if (!opts?.skipHub) {
733
+ const hubCatalog = await fetchHubCatalog(opts?.hubUrl);
734
+ if (Object.keys(hubCatalog).length > 0) {
735
+ registry = mergeToolDescriptors(registry, hubCatalog);
736
+ hubAvailable = true;
737
+ }
738
+ }
739
+ if (!hubAvailable || opts?.preferLocal) {
740
+ const localManifests = await discoverLocalManifests();
741
+ registry = mergeToolDescriptors(registry, localManifests);
742
+ }
743
+ cachedRegistry = registry;
744
+ return registry;
745
+ }
746
+ function invalidateRegistryCache() {
747
+ cachedRegistry = null;
748
+ }
749
+ async function getAllTools() {
750
+ const registry = await buildRegistry();
751
+ return Object.values(registry).sort((a, b) => a.name.localeCompare(b.name));
752
+ }
753
+ async function getToolById(id) {
754
+ const registry = await buildRegistry();
755
+ return registry[id];
756
+ }
757
+ async function getToolsWithCapability(capability) {
758
+ const all = await getAllTools();
759
+ return all.filter((t) => t.capabilities.includes(capability));
760
+ }
761
+ async function getAllCategories() {
762
+ const all = await getAllTools();
763
+ const cats = new Set(all.map((t) => t.category));
764
+ return Array.from(cats).sort();
765
+ }
766
+
767
+ export {
768
+ mergeToolDescriptors,
769
+ fetchToolFromHub,
770
+ buildRegistry,
771
+ invalidateRegistryCache,
772
+ getAllTools,
773
+ getToolById,
774
+ getToolsWithCapability,
775
+ getAllCategories
776
+ };
package/dist/index.js CHANGED
@@ -6,13 +6,14 @@ import {
6
6
  isToolInstalled,
7
7
  loadTool,
8
8
  uninstallTool
9
- } from "./chunk-V3VNCOZ6.js";
9
+ } from "./chunk-FY7KWENQ.js";
10
10
  import {
11
11
  ALL_MODELS,
12
12
  DEFAULT_CONFIG,
13
13
  createProject,
14
14
  estimateCost,
15
15
  getActiveProject,
16
+ getLarkupDataDir,
16
17
  getProjectDataDir,
17
18
  getProjectWorkspace,
18
19
  requireProjectDataDir,
@@ -20,11 +21,11 @@ import {
20
21
  runWithProject,
21
22
  setActiveProject,
22
23
  trackUsageEvent
23
- } from "./chunk-JZ3UCEGF.js";
24
+ } from "./chunk-T2XKBURE.js";
24
25
  import {
25
26
  getAllTools,
26
27
  getToolById
27
- } from "./chunk-5UWKP64Q.js";
28
+ } from "./chunk-YUC4VMEE.js";
28
29
  import {
29
30
  __require
30
31
  } from "./chunk-QGM4M3NI.js";
@@ -35,7 +36,7 @@ import { Command } from "commander";
35
36
  // package.json
36
37
  var package_default = {
37
38
  name: "@larkup/cli",
38
- version: "0.2.27",
39
+ version: "0.2.29",
39
40
  engines: {
40
41
  node: ">=22"
41
42
  },
@@ -56,21 +57,21 @@ var package_default = {
56
57
  "type-check": "tsc --noEmit"
57
58
  },
58
59
  dependencies: {
59
- "@ai-sdk/cohere": "^4.0.33",
60
- "@ai-sdk/deepseek": "^3.0.35",
61
- "@ai-sdk/gateway": "^4.0.67",
62
- "@ai-sdk/google": "^4.0.54",
63
- "@ai-sdk/mistral": "^4.0.35",
64
- "@ai-sdk/openai": "^4.0.50",
65
- "@ai-sdk/openai-compatible": "^3.0.39",
60
+ "@ai-sdk/cohere": "^4.0.35",
61
+ "@ai-sdk/deepseek": "^3.0.37",
62
+ "@ai-sdk/gateway": "^4.0.70",
63
+ "@ai-sdk/google": "^4.0.59",
64
+ "@ai-sdk/mistral": "^4.0.37",
65
+ "@ai-sdk/openai": "^4.0.53",
66
+ "@ai-sdk/openai-compatible": "^3.0.41",
66
67
  "@clack/prompts": "^1.7.0",
67
- "@lancedb/lancedb": "^0.37.1",
68
+ "@lancedb/lancedb": "^0.38.0",
68
69
  "@larkup/core": "workspace:*",
69
70
  "@larkup/marketplace": "workspace:*",
70
71
  "@larkup/vector-stores": "workspace:*",
71
72
  "@pinecone-database/pinecone": "^8.1.0",
72
- ai: "^7.0.83",
73
- "apache-arrow": "21.2.0",
73
+ ai: "^7.0.87",
74
+ "apache-arrow": "18.1.0",
74
75
  chalk: "^6.0.0",
75
76
  chromadb: "^3.5.0",
76
77
  "cli-table3": "^0.6.5",
@@ -224,13 +225,22 @@ async function readConfig() {
224
225
  migratedEmbeddingId = `custom:${legacy.modelName}`;
225
226
  }
226
227
  }
228
+ const storedDbPath = parsed.storeConfig?.dbPath;
229
+ const usesLegacyRelativeProjectPath = typeof storedDbPath === "string" && /^(?:\.\/)?\.larkup\/projects\/[^/]+\/index\/?$/.test(storedDbPath);
227
230
  const result = {
228
231
  ...DEFAULT_CONFIG,
229
232
  ...parsed,
230
233
  embeddingModelId: migratedEmbeddingId ?? DEFAULT_CONFIG.embeddingModelId,
231
234
  customEmbeddings: migratedEmbeddings,
232
235
  chunking: { ...DEFAULT_CONFIG.chunking, ...parsed.chunking },
233
- storeConfig: { ...DEFAULT_CONFIG.storeConfig, ...parsed.storeConfig }
236
+ storeConfig: {
237
+ ...DEFAULT_CONFIG.storeConfig,
238
+ ...parsed.storeConfig,
239
+ // A copied packaged workspace used to point its vector index back into
240
+ // node_modules via a relative path. Keep the index alongside the
241
+ // migrated project whenever a durable data root is configured.
242
+ ...process.env.LARKUP_DATA_DIR?.trim() && usesLegacyRelativeProjectPath ? { dbPath: path.join(dir, "index") } : {}
243
+ }
234
244
  };
235
245
  delete result["customEmbedding"];
236
246
  return result;
@@ -1095,6 +1105,10 @@ async function buildAgentTools() {
1095
1105
  continue;
1096
1106
  }
1097
1107
  for (const [name, pluginTool] of Object.entries(pluginTools)) {
1108
+ if (!pluginTool || typeof pluginTool !== "object") {
1109
+ console.warn("[agent] Plugin returned an invalid tool:", plugin.id || plugin.packageName, name);
1110
+ continue;
1111
+ }
1098
1112
  tools[name] = pluginTool;
1099
1113
  descriptions.push({ id: name, name, description: "Tool provided by plugin " + (plugin.id || plugin.packageName), source: "plugin", pluginId: plugin.id, pluginName });
1100
1114
  }
@@ -3257,10 +3271,7 @@ function resolveRuntimePluginVersion(plugin, portable = false) {
3257
3271
  async function loadEnabledPlugins(config, portable = false) {
3258
3272
  if (config.runtimeProfile !== "assistant") return [];
3259
3273
  try {
3260
- const raw = await fs3.readFile(
3261
- path3.join(process.cwd(), ".larkup", "tools", "installed.json"),
3262
- "utf8"
3263
- );
3274
+ const raw = await fs3.readFile(path3.join(getLarkupDataDir(), "tools", "installed.json"), "utf8");
3264
3275
  const parsed = JSON.parse(raw);
3265
3276
  const enabled = new Set(config.enabledTools ?? []);
3266
3277
  const installed = (parsed.tools ?? []).filter(
@@ -4170,7 +4181,7 @@ async function embedTexts(config, texts, abortSignal) {
4170
4181
  ...abortSignal ? { abortSignal } : {}
4171
4182
  });
4172
4183
  const totalTokens = texts.reduce((acc, t) => acc + Math.ceil(t.length / 4), 0);
4173
- const { trackUsageEvent: trackUsageEvent2, estimateCost: estimateCost2 } = await import("./analytics-store-4NYU7DMO.js");
4184
+ const { trackUsageEvent: trackUsageEvent2, estimateCost: estimateCost2 } = await import("./analytics-store-5BPWULY4.js");
4174
4185
  void trackUsageEvent2({
4175
4186
  type: "embedding",
4176
4187
  embeddingModelId: config.embeddingModelId,
@@ -3,8 +3,8 @@ import {
3
3
  isToolLoaded,
4
4
  loadTool,
5
5
  unloadTool
6
- } from "./chunk-V3VNCOZ6.js";
7
- import "./chunk-5UWKP64Q.js";
6
+ } from "./chunk-FY7KWENQ.js";
7
+ import "./chunk-YUC4VMEE.js";
8
8
  import "./chunk-QGM4M3NI.js";
9
9
  export {
10
10
  hasCapability,
@@ -7,7 +7,7 @@ import {
7
7
  getToolsWithCapability,
8
8
  invalidateRegistryCache,
9
9
  mergeToolDescriptors
10
- } from "./chunk-5UWKP64Q.js";
10
+ } from "./chunk-YUC4VMEE.js";
11
11
  import "./chunk-QGM4M3NI.js";
12
12
  export {
13
13
  buildRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larkup/cli",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -15,26 +15,26 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@ai-sdk/cohere": "^4.0.33",
19
- "@ai-sdk/deepseek": "^3.0.35",
20
- "@ai-sdk/gateway": "^4.0.67",
21
- "@ai-sdk/google": "^4.0.54",
22
- "@ai-sdk/mistral": "^4.0.35",
23
- "@ai-sdk/openai": "^4.0.50",
24
- "@ai-sdk/openai-compatible": "^3.0.39",
18
+ "@ai-sdk/cohere": "^4.0.35",
19
+ "@ai-sdk/deepseek": "^3.0.37",
20
+ "@ai-sdk/gateway": "^4.0.70",
21
+ "@ai-sdk/google": "^4.0.59",
22
+ "@ai-sdk/mistral": "^4.0.37",
23
+ "@ai-sdk/openai": "^4.0.53",
24
+ "@ai-sdk/openai-compatible": "^3.0.41",
25
25
  "@clack/prompts": "^1.7.0",
26
- "@lancedb/lancedb": "^0.37.1",
26
+ "@lancedb/lancedb": "^0.38.0",
27
27
  "@pinecone-database/pinecone": "^8.1.0",
28
- "ai": "^7.0.83",
29
- "apache-arrow": "21.2.0",
28
+ "ai": "^7.0.87",
29
+ "apache-arrow": "18.1.0",
30
30
  "chalk": "^6.0.0",
31
31
  "chromadb": "^3.5.0",
32
32
  "cli-table3": "^0.6.5",
33
33
  "commander": "^15.0.0",
34
34
  "zod": "^4.4.3",
35
- "@larkup/core": "0.5.0",
36
- "@larkup/marketplace": "0.2.0",
37
- "@larkup/vector-stores": "0.1.26"
35
+ "@larkup/core": "0.5.2",
36
+ "@larkup/marketplace": "0.2.2",
37
+ "@larkup/vector-stores": "0.1.27"
38
38
  },
39
39
  "devDependencies": {
40
40
  "tsup": "^8.5.1",
@@ -1,212 +0,0 @@
1
- // ../../packages/marketplace/src/tool-registry.ts
2
- import { promises as fs } from "fs";
3
- import path from "path";
4
-
5
- // ../../packages/marketplace/src/types.ts
6
- var DEFAULT_HUB_URL = process.env.LARKUP_HUB_URL ?? "https://hub.larkup.de";
7
- var STORAGE_WARNING_THRESHOLDS = [
8
- 1 * 1024 * 1024 * 1024,
9
- // 1 GB
10
- 5 * 1024 * 1024 * 1024,
11
- // 5 GB
12
- 10 * 1024 * 1024 * 1024
13
- // 10 GB
14
- ];
15
-
16
- // ../../packages/marketplace/src/tool-registry.ts
17
- var cachedRegistry = null;
18
- var RETIRED_CATALOG_TOOL_IDS = /* @__PURE__ */ new Set(["video-audio"]);
19
- function normalizeToolDescriptor(descriptor) {
20
- return { ...descriptor, requiresSandbox: descriptor.requiresSandbox !== false };
21
- }
22
- function compareToolVersions(left, right) {
23
- const parse = (version) => {
24
- const [core, suffix = ""] = version.split("-", 2);
25
- const parts = core.split(".").map((part) => Number.parseInt(part, 10) || 0);
26
- return { parts, stable: suffix.length === 0 };
27
- };
28
- const a = parse(left);
29
- const b = parse(right);
30
- const length = Math.max(a.parts.length, b.parts.length);
31
- for (let index = 0; index < length; index += 1) {
32
- const difference = (a.parts[index] ?? 0) - (b.parts[index] ?? 0);
33
- if (difference !== 0) return difference;
34
- }
35
- if (a.stable === b.stable) return 0;
36
- return a.stable ? 1 : -1;
37
- }
38
- function mergeToolDescriptors(current, incoming) {
39
- const merged = { ...current };
40
- for (const [id, descriptor] of Object.entries(incoming)) {
41
- const existing = merged[id];
42
- if (!existing || compareToolVersions(descriptor.version, existing.version) >= 0) {
43
- merged[id] = descriptor;
44
- }
45
- }
46
- return merged;
47
- }
48
- async function discoverLocalManifests() {
49
- const registry = {};
50
- const searchPaths = [
51
- // Monorepo development: packages/marketplace-tools/*
52
- path.resolve(process.cwd(), "packages", "marketplace-tools"),
53
- // Next.js dev server runs in apps/web
54
- path.resolve(process.cwd(), "..", "..", "packages", "marketplace-tools"),
55
- // Installed tools: .larkup/tools/node_modules/@larkup
56
- path.resolve(process.cwd(), ".larkup", "tools", "node_modules", "@larkup"),
57
- path.resolve(process.cwd(), "..", "..", ".larkup", "tools", "node_modules", "@larkup")
58
- ];
59
- for (const searchPath of searchPaths) {
60
- try {
61
- const entries = await fs.readdir(searchPath, { withFileTypes: true });
62
- for (const entry of entries) {
63
- if (!entry.isDirectory()) continue;
64
- const manifestPath = path.join(searchPath, entry.name, "tool.manifest.json");
65
- try {
66
- const raw = await fs.readFile(manifestPath, "utf8");
67
- const manifest = normalizeToolDescriptor(JSON.parse(raw));
68
- if (manifest.id && !RETIRED_CATALOG_TOOL_IDS.has(manifest.id)) {
69
- registry[manifest.id] = manifest;
70
- }
71
- } catch {
72
- }
73
- }
74
- } catch {
75
- }
76
- }
77
- return registry;
78
- }
79
- async function fetchHubCatalog(hubUrl) {
80
- const baseUrl = hubUrl ?? DEFAULT_HUB_URL;
81
- try {
82
- const controller = new AbortController();
83
- const timeout = setTimeout(() => controller.abort(), 5e3);
84
- const res = await fetch(`${baseUrl}/v1/tools`, {
85
- signal: controller.signal,
86
- headers: { Accept: "application/json" }
87
- });
88
- clearTimeout(timeout);
89
- if (!res.ok) return {};
90
- const data = await res.json();
91
- const registry = {};
92
- for (const tool of data.tools ?? []) {
93
- if (tool.id && !RETIRED_CATALOG_TOOL_IDS.has(tool.id)) {
94
- registry[tool.id] = normalizeToolDescriptor(tool);
95
- }
96
- }
97
- return registry;
98
- } catch {
99
- return {};
100
- }
101
- }
102
- async function fetchToolFromHub(toolId, hubUrl) {
103
- if (RETIRED_CATALOG_TOOL_IDS.has(toolId)) return null;
104
- const baseUrl = hubUrl ?? DEFAULT_HUB_URL;
105
- try {
106
- const controller = new AbortController();
107
- const timeout = setTimeout(() => controller.abort(), 5e3);
108
- const res = await fetch(`${baseUrl}/v1/tools/${toolId}`, {
109
- signal: controller.signal,
110
- headers: { Accept: "application/json" }
111
- });
112
- clearTimeout(timeout);
113
- if (!res.ok) return null;
114
- const data = await res.json();
115
- return data.tool ?? null;
116
- } catch {
117
- return null;
118
- }
119
- }
120
- var FALLBACK_REGISTRY = {
121
- "clip-embeddings": {
122
- id: "clip-embeddings",
123
- name: "CLIP Image Search",
124
- description: "Direct image-to-image similarity search using CLIP/SigLIP embeddings.",
125
- category: "embedding",
126
- version: "0.1.0",
127
- pricing: "free",
128
- emoji: "\u{1F50D}",
129
- icon: "ScanEye",
130
- packageName: "@larkup/tool-clip-embeddings",
131
- installSize: "~200 MB",
132
- requiresSandbox: true,
133
- author: "Larkup",
134
- capabilities: ["clip-embeddings", "image-similarity"],
135
- tags: ["clip", "siglip", "image-search", "visual-similarity"],
136
- downloads: 0,
137
- repositoryUrl: "https://github.com/Larkup-AI/larkup",
138
- license: "Apache-2.0",
139
- updatedAt: "2026-07-20",
140
- comingSoon: true
141
- },
142
- "doc-editor": {
143
- id: "doc-editor",
144
- name: "Document Editor",
145
- description: "AI-powered form filling and document editing with Canvas-style live preview.",
146
- category: "utility",
147
- version: "0.2.3",
148
- pricing: "free",
149
- emoji: "\u{1F4DD}",
150
- icon: "FileEdit",
151
- packageName: "@larkup/tool-doc-editor",
152
- installSize: "~5 MB",
153
- systemDeps: ["docker"],
154
- requiresSandbox: true,
155
- author: "Larkup",
156
- capabilities: ["document-editing", "form-filling", "document-preview"],
157
- tags: ["pdf", "docx", "pptx", "form", "canvas", "editor", "fill"],
158
- downloads: 0,
159
- repositoryUrl: "https://github.com/Larkup-AI/larkup",
160
- license: "Apache-2.0",
161
- updatedAt: "2026-07-20"
162
- }
163
- };
164
- async function buildRegistry(opts) {
165
- if (cachedRegistry) return cachedRegistry;
166
- let registry = { ...FALLBACK_REGISTRY };
167
- let hubAvailable = false;
168
- if (!opts?.skipHub) {
169
- const hubCatalog = await fetchHubCatalog(opts?.hubUrl);
170
- if (Object.keys(hubCatalog).length > 0) {
171
- registry = mergeToolDescriptors(registry, hubCatalog);
172
- hubAvailable = true;
173
- }
174
- }
175
- if (!hubAvailable || opts?.preferLocal) {
176
- const localManifests = await discoverLocalManifests();
177
- registry = mergeToolDescriptors(registry, localManifests);
178
- }
179
- cachedRegistry = registry;
180
- return registry;
181
- }
182
- function invalidateRegistryCache() {
183
- cachedRegistry = null;
184
- }
185
- async function getAllTools() {
186
- const registry = await buildRegistry();
187
- return Object.values(registry).sort((a, b) => a.name.localeCompare(b.name));
188
- }
189
- async function getToolById(id) {
190
- const registry = await buildRegistry();
191
- return registry[id];
192
- }
193
- async function getToolsWithCapability(capability) {
194
- const all = await getAllTools();
195
- return all.filter((t) => t.capabilities.includes(capability));
196
- }
197
- async function getAllCategories() {
198
- const all = await getAllTools();
199
- const cats = new Set(all.map((t) => t.category));
200
- return Array.from(cats).sort();
201
- }
202
-
203
- export {
204
- mergeToolDescriptors,
205
- fetchToolFromHub,
206
- buildRegistry,
207
- invalidateRegistryCache,
208
- getAllTools,
209
- getToolById,
210
- getToolsWithCapability,
211
- getAllCategories
212
- };