@swarmvaultai/engine 0.1.17 → 0.1.19

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,1057 @@
1
+ // src/providers/registry.ts
2
+ import path3 from "path";
3
+ import { pathToFileURL } from "url";
4
+ import { z as z5 } from "zod";
5
+
6
+ // src/config.ts
7
+ import fs2 from "fs/promises";
8
+ import path2 from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { z as z2 } from "zod";
11
+
12
+ // src/types.ts
13
+ import { z } from "zod";
14
+ var providerCapabilitySchema = z.enum([
15
+ "responses",
16
+ "chat",
17
+ "structured",
18
+ "tools",
19
+ "vision",
20
+ "embeddings",
21
+ "streaming",
22
+ "local",
23
+ "image_generation"
24
+ ]);
25
+ var providerTypeSchema = z.enum([
26
+ "heuristic",
27
+ "openai",
28
+ "ollama",
29
+ "anthropic",
30
+ "gemini",
31
+ "openai-compatible",
32
+ "openrouter",
33
+ "groq",
34
+ "together",
35
+ "xai",
36
+ "cerebras",
37
+ "custom"
38
+ ]);
39
+ var agentTypeSchema = z.enum(["codex", "claude", "cursor", "goose", "pi", "gemini", "opencode"]);
40
+ var webSearchProviderTypeSchema = z.enum(["http-json", "custom"]);
41
+
42
+ // src/utils.ts
43
+ import crypto from "crypto";
44
+ import fs from "fs/promises";
45
+ import path from "path";
46
+ function slugify(value) {
47
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "item";
48
+ }
49
+ function sha256(value) {
50
+ return crypto.createHash("sha256").update(value).digest("hex");
51
+ }
52
+ async function ensureDir(dirPath) {
53
+ await fs.mkdir(dirPath, { recursive: true });
54
+ }
55
+ async function fileExists(filePath) {
56
+ try {
57
+ await fs.access(filePath);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ async function readJsonFile(filePath) {
64
+ if (!await fileExists(filePath)) {
65
+ return null;
66
+ }
67
+ const content = await fs.readFile(filePath, "utf8");
68
+ return JSON.parse(content);
69
+ }
70
+ async function writeJsonFile(filePath, value) {
71
+ await ensureDir(path.dirname(filePath));
72
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}
73
+ `, "utf8");
74
+ }
75
+ async function appendJsonLine(filePath, value) {
76
+ await ensureDir(path.dirname(filePath));
77
+ await fs.appendFile(filePath, `${JSON.stringify(value)}
78
+ `, "utf8");
79
+ }
80
+ async function writeFileIfChanged(filePath, content) {
81
+ await ensureDir(path.dirname(filePath));
82
+ if (await fileExists(filePath)) {
83
+ const existing = await fs.readFile(filePath, "utf8");
84
+ if (existing === content) {
85
+ return false;
86
+ }
87
+ }
88
+ await fs.writeFile(filePath, content, "utf8");
89
+ return true;
90
+ }
91
+ function toPosix(value) {
92
+ return value.split(path.sep).join(path.posix.sep);
93
+ }
94
+ function firstSentences(value, count = 3) {
95
+ const sentences = value.replace(/\s+/g, " ").split(/(?<=[.!?])\s+/).filter(Boolean);
96
+ return sentences.slice(0, count).join(" ").trim();
97
+ }
98
+ function uniqueBy(items, key) {
99
+ const seen = /* @__PURE__ */ new Set();
100
+ const result = [];
101
+ for (const item of items) {
102
+ const itemKey = key(item);
103
+ if (seen.has(itemKey)) {
104
+ continue;
105
+ }
106
+ seen.add(itemKey);
107
+ result.push(item);
108
+ }
109
+ return result;
110
+ }
111
+ function extractJson(text) {
112
+ const fencedMatch = text.match(/```json\s*([\s\S]*?)```/i);
113
+ if (fencedMatch) {
114
+ return fencedMatch[1].trim();
115
+ }
116
+ const start = text.indexOf("{");
117
+ const end = text.lastIndexOf("}");
118
+ if (start !== -1 && end !== -1 && end > start) {
119
+ return text.slice(start, end + 1);
120
+ }
121
+ throw new Error("Could not locate JSON object in provider response.");
122
+ }
123
+ function normalizeWhitespace(value) {
124
+ return value.replace(/\s+/g, " ").trim();
125
+ }
126
+ function truncate(value, maxLength) {
127
+ if (value.length <= maxLength) {
128
+ return value;
129
+ }
130
+ return `${value.slice(0, maxLength - 3)}...`;
131
+ }
132
+ async function listFilesRecursive(rootDir) {
133
+ const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch(() => []);
134
+ const files = [];
135
+ for (const entry of entries) {
136
+ const absolutePath = path.join(rootDir, entry.name);
137
+ if (entry.isDirectory()) {
138
+ files.push(...await listFilesRecursive(absolutePath));
139
+ continue;
140
+ }
141
+ if (entry.isFile()) {
142
+ files.push(absolutePath);
143
+ }
144
+ }
145
+ return files;
146
+ }
147
+
148
+ // src/config.ts
149
+ var PRIMARY_CONFIG_FILENAME = "swarmvault.config.json";
150
+ var PRIMARY_SCHEMA_FILENAME = "swarmvault.schema.md";
151
+ var moduleDir = path2.dirname(fileURLToPath(import.meta.url));
152
+ var viewerDistDir = path2.basename(moduleDir) === "src" ? path2.resolve(moduleDir, "../../viewer/dist") : path2.resolve(moduleDir, "viewer");
153
+ var providerConfigSchema = z2.object({
154
+ type: providerTypeSchema,
155
+ model: z2.string().min(1),
156
+ baseUrl: z2.string().url().optional(),
157
+ apiKeyEnv: z2.string().min(1).optional(),
158
+ headers: z2.record(z2.string(), z2.string()).optional(),
159
+ module: z2.string().min(1).optional(),
160
+ capabilities: z2.array(providerCapabilitySchema).optional(),
161
+ apiStyle: z2.enum(["responses", "chat"]).optional()
162
+ });
163
+ var scheduleTriggerSchema = z2.object({
164
+ cron: z2.string().min(1).optional(),
165
+ every: z2.string().min(1).optional()
166
+ }).refine((value) => Boolean(value.cron || value.every), {
167
+ message: "Schedule triggers require `cron` or `every`."
168
+ });
169
+ var scheduledTaskSchema = z2.discriminatedUnion("type", [
170
+ z2.object({
171
+ type: z2.literal("compile"),
172
+ approve: z2.boolean().optional()
173
+ }),
174
+ z2.object({
175
+ type: z2.literal("lint"),
176
+ deep: z2.boolean().optional(),
177
+ web: z2.boolean().optional()
178
+ }),
179
+ z2.object({
180
+ type: z2.literal("query"),
181
+ question: z2.string().min(1),
182
+ format: z2.enum(["markdown", "report", "slides", "chart", "image"]).optional(),
183
+ save: z2.boolean().optional()
184
+ }),
185
+ z2.object({
186
+ type: z2.literal("explore"),
187
+ question: z2.string().min(1),
188
+ steps: z2.number().int().positive().optional(),
189
+ format: z2.enum(["markdown", "report", "slides", "chart", "image"]).optional()
190
+ })
191
+ ]);
192
+ var roleExecutorConfigSchema = z2.discriminatedUnion("type", [
193
+ z2.object({
194
+ type: z2.literal("provider"),
195
+ provider: z2.string().min(1)
196
+ }),
197
+ z2.object({
198
+ type: z2.literal("command"),
199
+ command: z2.array(z2.string().min(1)).min(1),
200
+ cwd: z2.string().min(1).optional(),
201
+ env: z2.record(z2.string(), z2.string()).optional(),
202
+ timeoutMs: z2.number().int().positive().optional()
203
+ })
204
+ ]);
205
+ var webSearchProviderConfigSchema = z2.object({
206
+ type: webSearchProviderTypeSchema,
207
+ endpoint: z2.string().url().optional(),
208
+ method: z2.enum(["GET", "POST"]).optional(),
209
+ apiKeyEnv: z2.string().min(1).optional(),
210
+ apiKeyHeader: z2.string().min(1).optional(),
211
+ apiKeyPrefix: z2.string().optional(),
212
+ headers: z2.record(z2.string(), z2.string()).optional(),
213
+ queryParam: z2.string().min(1).optional(),
214
+ limitParam: z2.string().min(1).optional(),
215
+ resultsPath: z2.string().min(1).optional(),
216
+ titleField: z2.string().min(1).optional(),
217
+ urlField: z2.string().min(1).optional(),
218
+ snippetField: z2.string().min(1).optional(),
219
+ module: z2.string().min(1).optional()
220
+ });
221
+ var vaultConfigSchema = z2.object({
222
+ workspace: z2.object({
223
+ rawDir: z2.string().min(1),
224
+ wikiDir: z2.string().min(1),
225
+ stateDir: z2.string().min(1),
226
+ agentDir: z2.string().min(1),
227
+ inboxDir: z2.string().min(1)
228
+ }),
229
+ providers: z2.record(z2.string(), providerConfigSchema),
230
+ tasks: z2.object({
231
+ compileProvider: z2.string().min(1),
232
+ queryProvider: z2.string().min(1),
233
+ lintProvider: z2.string().min(1),
234
+ visionProvider: z2.string().min(1),
235
+ imageProvider: z2.string().min(1).optional()
236
+ }),
237
+ viewer: z2.object({
238
+ port: z2.number().int().positive()
239
+ }),
240
+ projects: z2.record(
241
+ z2.string(),
242
+ z2.object({
243
+ roots: z2.array(z2.string().min(1)).min(1),
244
+ schemaPath: z2.string().min(1).optional()
245
+ })
246
+ ).optional(),
247
+ agents: z2.array(agentTypeSchema).default(["codex", "claude", "cursor"]),
248
+ schedules: z2.record(z2.string(), z2.object({ enabled: z2.boolean().optional(), when: scheduleTriggerSchema, task: scheduledTaskSchema })).optional(),
249
+ orchestration: z2.object({
250
+ maxParallelRoles: z2.number().int().positive().optional(),
251
+ compilePostPass: z2.boolean().optional(),
252
+ roles: z2.object({
253
+ research: z2.object({ executor: roleExecutorConfigSchema }).optional(),
254
+ audit: z2.object({ executor: roleExecutorConfigSchema }).optional(),
255
+ context: z2.object({ executor: roleExecutorConfigSchema }).optional(),
256
+ safety: z2.object({ executor: roleExecutorConfigSchema }).optional()
257
+ }).optional()
258
+ }).optional(),
259
+ webSearch: z2.object({
260
+ providers: z2.record(z2.string(), webSearchProviderConfigSchema),
261
+ tasks: z2.object({
262
+ deepLintProvider: z2.string().min(1)
263
+ })
264
+ }).optional()
265
+ });
266
+ function defaultVaultConfig() {
267
+ return {
268
+ workspace: {
269
+ rawDir: "raw",
270
+ wikiDir: "wiki",
271
+ stateDir: "state",
272
+ agentDir: "agent",
273
+ inboxDir: "inbox"
274
+ },
275
+ providers: {
276
+ local: {
277
+ type: "heuristic",
278
+ model: "heuristic-v1",
279
+ capabilities: ["chat", "structured", "vision", "local"]
280
+ }
281
+ },
282
+ tasks: {
283
+ compileProvider: "local",
284
+ queryProvider: "local",
285
+ lintProvider: "local",
286
+ visionProvider: "local",
287
+ imageProvider: "local"
288
+ },
289
+ viewer: {
290
+ port: 4123
291
+ },
292
+ projects: {},
293
+ agents: ["codex", "claude", "cursor"],
294
+ schedules: {},
295
+ orchestration: {
296
+ maxParallelRoles: 2,
297
+ compilePostPass: false,
298
+ roles: {}
299
+ }
300
+ };
301
+ }
302
+ function defaultVaultSchema() {
303
+ return [
304
+ "# SwarmVault Schema",
305
+ "",
306
+ "Edit this file to teach SwarmVault how this vault should be organized and maintained.",
307
+ "",
308
+ "## Vault Purpose",
309
+ "",
310
+ "- Describe the domain this vault covers.",
311
+ "- Note the intended audience and the kinds of questions the vault should answer well.",
312
+ "",
313
+ "## Naming Conventions",
314
+ "",
315
+ "- Prefer stable, descriptive page titles.",
316
+ "- Keep concept and entity names specific to the domain.",
317
+ "",
318
+ "## Page Structure Rules",
319
+ "",
320
+ "- Source pages should stay grounded in the original material.",
321
+ "- Concept and entity pages should aggregate source-backed claims instead of inventing new ones.",
322
+ "- Preserve contradictions instead of smoothing them away.",
323
+ "",
324
+ "## Categories",
325
+ "",
326
+ "- List domain-specific concept categories here.",
327
+ "- List important entity types here.",
328
+ "",
329
+ "## Relationship Types",
330
+ "",
331
+ "- Mentions",
332
+ "- Supports",
333
+ "- Contradicts",
334
+ "- Depends on",
335
+ "",
336
+ "## Grounding Rules",
337
+ "",
338
+ "- Prefer raw sources over summaries.",
339
+ "- Cite source ids whenever claims are stated.",
340
+ "- Do not treat the wiki as a source of truth when the raw material disagrees.",
341
+ "",
342
+ "## Exclusions",
343
+ "",
344
+ "- List topics, claims, or page types the compiler should avoid generating.",
345
+ ""
346
+ ].join("\n");
347
+ }
348
+ async function findConfigPath(rootDir) {
349
+ const primaryPath = path2.join(rootDir, PRIMARY_CONFIG_FILENAME);
350
+ return primaryPath;
351
+ }
352
+ async function findSchemaPath(rootDir) {
353
+ const primaryPath = path2.join(rootDir, PRIMARY_SCHEMA_FILENAME);
354
+ return primaryPath;
355
+ }
356
+ function resolvePaths(rootDir, config, configPath = path2.join(rootDir, PRIMARY_CONFIG_FILENAME), schemaPath = path2.join(rootDir, PRIMARY_SCHEMA_FILENAME)) {
357
+ const effective = config ?? defaultVaultConfig();
358
+ const rawDir = path2.resolve(rootDir, effective.workspace.rawDir);
359
+ const rawSourcesDir = path2.join(rawDir, "sources");
360
+ const rawAssetsDir = path2.join(rawDir, "assets");
361
+ const wikiDir = path2.resolve(rootDir, effective.workspace.wikiDir);
362
+ const outputsAssetsDir = path2.join(wikiDir, "outputs", "assets");
363
+ const projectsDir = path2.join(wikiDir, "projects");
364
+ const candidatesDir = path2.join(wikiDir, "candidates");
365
+ const stateDir = path2.resolve(rootDir, effective.workspace.stateDir);
366
+ const schedulesDir = path2.join(stateDir, "schedules");
367
+ const agentDir = path2.resolve(rootDir, effective.workspace.agentDir);
368
+ const inboxDir = path2.resolve(rootDir, effective.workspace.inboxDir);
369
+ return {
370
+ rootDir,
371
+ schemaPath,
372
+ rawDir,
373
+ rawSourcesDir,
374
+ rawAssetsDir,
375
+ wikiDir,
376
+ outputsAssetsDir,
377
+ projectsDir,
378
+ candidatesDir,
379
+ candidateConceptsDir: path2.join(candidatesDir, "concepts"),
380
+ candidateEntitiesDir: path2.join(candidatesDir, "entities"),
381
+ stateDir,
382
+ schedulesDir,
383
+ agentDir,
384
+ inboxDir,
385
+ manifestsDir: path2.join(stateDir, "manifests"),
386
+ extractsDir: path2.join(stateDir, "extracts"),
387
+ analysesDir: path2.join(stateDir, "analyses"),
388
+ viewerDistDir,
389
+ graphPath: path2.join(stateDir, "graph.json"),
390
+ searchDbPath: path2.join(stateDir, "search.sqlite"),
391
+ compileStatePath: path2.join(stateDir, "compile-state.json"),
392
+ codeIndexPath: path2.join(stateDir, "code-index.json"),
393
+ jobsLogPath: path2.join(stateDir, "jobs.ndjson"),
394
+ sessionsDir: path2.join(stateDir, "sessions"),
395
+ approvalsDir: path2.join(stateDir, "approvals"),
396
+ configPath
397
+ };
398
+ }
399
+ async function loadVaultConfig(rootDir) {
400
+ const configPath = await findConfigPath(rootDir);
401
+ const schemaPath = await findSchemaPath(rootDir);
402
+ const raw = await readJsonFile(configPath);
403
+ const parsed = vaultConfigSchema.parse(raw ?? defaultVaultConfig());
404
+ return {
405
+ config: parsed,
406
+ paths: resolvePaths(rootDir, parsed, configPath, schemaPath)
407
+ };
408
+ }
409
+ async function initWorkspace(rootDir) {
410
+ const configPath = await findConfigPath(rootDir);
411
+ const schemaPath = await findSchemaPath(rootDir);
412
+ const config = await fileExists(configPath) ? (await loadVaultConfig(rootDir)).config : defaultVaultConfig();
413
+ const paths = resolvePaths(rootDir, config, configPath, schemaPath);
414
+ const primarySchemaPath = path2.join(rootDir, PRIMARY_SCHEMA_FILENAME);
415
+ await Promise.all([
416
+ ensureDir(paths.rawDir),
417
+ ensureDir(paths.wikiDir),
418
+ ensureDir(paths.outputsAssetsDir),
419
+ ensureDir(paths.projectsDir),
420
+ ensureDir(paths.candidatesDir),
421
+ ensureDir(paths.candidateConceptsDir),
422
+ ensureDir(paths.candidateEntitiesDir),
423
+ ensureDir(paths.stateDir),
424
+ ensureDir(paths.schedulesDir),
425
+ ensureDir(paths.sessionsDir),
426
+ ensureDir(paths.approvalsDir),
427
+ ensureDir(paths.agentDir),
428
+ ensureDir(paths.inboxDir),
429
+ ensureDir(paths.manifestsDir),
430
+ ensureDir(paths.extractsDir),
431
+ ensureDir(paths.analysesDir),
432
+ ensureDir(paths.rawSourcesDir),
433
+ ensureDir(paths.rawAssetsDir)
434
+ ]);
435
+ if (!await fileExists(configPath)) {
436
+ await writeJsonFile(configPath, config);
437
+ }
438
+ if (!await fileExists(primarySchemaPath)) {
439
+ await ensureDir(path2.dirname(primarySchemaPath));
440
+ await fs2.writeFile(primarySchemaPath, defaultVaultSchema(), "utf8");
441
+ }
442
+ return { config, paths };
443
+ }
444
+
445
+ // src/providers/base.ts
446
+ import fs3 from "fs/promises";
447
+ import { z as z3 } from "zod";
448
+ var BaseProviderAdapter = class {
449
+ constructor(id, type, model, capabilities) {
450
+ this.id = id;
451
+ this.type = type;
452
+ this.model = model;
453
+ this.capabilities = new Set(capabilities);
454
+ }
455
+ id;
456
+ type;
457
+ model;
458
+ capabilities;
459
+ async generateImage(_request) {
460
+ throw new Error(`Provider ${this.id} does not support image generation.`);
461
+ }
462
+ async generateStructured(request, schema) {
463
+ const schemaDescription = JSON.stringify(z3.toJSONSchema(schema), null, 2);
464
+ const response = await this.generateText({
465
+ ...request,
466
+ prompt: `${request.prompt}
467
+
468
+ Return JSON only. Follow this JSON Schema exactly:
469
+ ${schemaDescription}`
470
+ });
471
+ const parsed = JSON.parse(extractJson(response.text));
472
+ return schema.parse(parsed);
473
+ }
474
+ async encodeAttachments(attachments = []) {
475
+ return Promise.all(
476
+ attachments.map(async (attachment) => ({
477
+ mimeType: attachment.mimeType,
478
+ base64: await fs3.readFile(attachment.filePath, "base64")
479
+ }))
480
+ );
481
+ }
482
+ };
483
+
484
+ // src/providers/anthropic.ts
485
+ var AnthropicProviderAdapter = class extends BaseProviderAdapter {
486
+ apiKey;
487
+ headers;
488
+ baseUrl;
489
+ constructor(id, model, options) {
490
+ super(id, "anthropic", model, ["chat", "structured", "tools", "vision", "streaming"]);
491
+ this.apiKey = options.apiKey;
492
+ this.headers = options.headers;
493
+ this.baseUrl = (options.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
494
+ }
495
+ async generateText(request) {
496
+ const encodedAttachments = await this.encodeAttachments(request.attachments);
497
+ const content = [
498
+ { type: "text", text: request.prompt },
499
+ ...encodedAttachments.map((item) => ({
500
+ type: "image",
501
+ source: {
502
+ type: "base64",
503
+ media_type: item.mimeType,
504
+ data: item.base64
505
+ }
506
+ }))
507
+ ];
508
+ const response = await fetch(`${this.baseUrl}/v1/messages`, {
509
+ method: "POST",
510
+ headers: {
511
+ "content-type": "application/json",
512
+ "anthropic-version": "2023-06-01",
513
+ ...this.apiKey ? { "x-api-key": this.apiKey } : {},
514
+ ...this.headers
515
+ },
516
+ body: JSON.stringify({
517
+ model: this.model,
518
+ max_tokens: request.maxOutputTokens ?? 1200,
519
+ system: request.system,
520
+ messages: [
521
+ {
522
+ role: "user",
523
+ content
524
+ }
525
+ ]
526
+ })
527
+ });
528
+ if (!response.ok) {
529
+ throw new Error(`Provider ${this.id} failed: ${response.status} ${response.statusText}`);
530
+ }
531
+ const payload = await response.json();
532
+ return {
533
+ text: payload.content?.filter((item) => item.type === "text").map((item) => item.text ?? "").join("\n") ?? "",
534
+ usage: payload.usage ? { inputTokens: payload.usage.input_tokens, outputTokens: payload.usage.output_tokens } : void 0
535
+ };
536
+ }
537
+ };
538
+
539
+ // src/providers/gemini.ts
540
+ var GeminiProviderAdapter = class extends BaseProviderAdapter {
541
+ apiKey;
542
+ baseUrl;
543
+ constructor(id, model, options) {
544
+ super(id, "gemini", model, ["chat", "structured", "vision", "tools", "streaming"]);
545
+ this.apiKey = options.apiKey;
546
+ this.baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "");
547
+ }
548
+ async generateText(request) {
549
+ const encodedAttachments = await this.encodeAttachments(request.attachments);
550
+ const parts = [
551
+ ...request.system ? [{ text: `System instructions:
552
+ ${request.system}` }] : [],
553
+ { text: request.prompt },
554
+ ...encodedAttachments.map((item) => ({
555
+ inline_data: {
556
+ mime_type: item.mimeType,
557
+ data: item.base64
558
+ }
559
+ }))
560
+ ];
561
+ const response = await fetch(`${this.baseUrl}/models/${this.model}:generateContent`, {
562
+ method: "POST",
563
+ headers: {
564
+ "content-type": "application/json",
565
+ ...this.apiKey ? { "x-goog-api-key": this.apiKey } : {}
566
+ },
567
+ body: JSON.stringify({
568
+ contents: [
569
+ {
570
+ role: "user",
571
+ parts
572
+ }
573
+ ],
574
+ generationConfig: {
575
+ maxOutputTokens: request.maxOutputTokens ?? 1200
576
+ }
577
+ })
578
+ });
579
+ if (!response.ok) {
580
+ throw new Error(`Provider ${this.id} failed: ${response.status} ${response.statusText}`);
581
+ }
582
+ const payload = await response.json();
583
+ const text = payload.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "";
584
+ return {
585
+ text,
586
+ usage: payload.usageMetadata ? { inputTokens: payload.usageMetadata.promptTokenCount, outputTokens: payload.usageMetadata.candidatesTokenCount } : void 0
587
+ };
588
+ }
589
+ };
590
+
591
+ // src/providers/heuristic.ts
592
+ function summarizePrompt(prompt) {
593
+ const cleaned = normalizeWhitespace(prompt);
594
+ if (!cleaned) {
595
+ return "No prompt content provided.";
596
+ }
597
+ return firstSentences(cleaned, 2) || cleaned.slice(0, 280);
598
+ }
599
+ var HeuristicProviderAdapter = class extends BaseProviderAdapter {
600
+ constructor(id, model) {
601
+ super(id, "heuristic", model, ["chat", "structured", "vision", "local"]);
602
+ }
603
+ async generateText(request) {
604
+ const attachmentHint = request.attachments?.length ? ` Attachments: ${request.attachments.length}.` : "";
605
+ return {
606
+ text: `Heuristic provider response.${attachmentHint} ${summarizePrompt(request.prompt)}`.trim()
607
+ };
608
+ }
609
+ };
610
+
611
+ // src/providers/openai-compatible.ts
612
+ import { z as z4 } from "zod";
613
+ function buildAuthHeaders(apiKey) {
614
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
615
+ }
616
+ function extractResponsesText(payload) {
617
+ if (typeof payload.output_text === "string" && payload.output_text.trim()) {
618
+ return payload.output_text;
619
+ }
620
+ return payload.output?.flatMap((item) => item.content ?? []).filter((item) => item.type === "output_text" && typeof item.text === "string").map((item) => item.text ?? "").join("\n").trim() ?? "";
621
+ }
622
+ function isJsonSchemaObject(value) {
623
+ return typeof value === "object" && value !== null && !Array.isArray(value);
624
+ }
625
+ function allowNullInSchema(schema) {
626
+ if (Array.isArray(schema.type)) {
627
+ return schema.type.includes("null") ? schema : { ...schema, type: [...schema.type, "null"] };
628
+ }
629
+ if (typeof schema.type === "string") {
630
+ return schema.type === "null" ? schema : { ...schema, type: [schema.type, "null"] };
631
+ }
632
+ if (Array.isArray(schema.enum)) {
633
+ return schema.enum.includes(null) ? schema : { ...schema, enum: [...schema.enum, null] };
634
+ }
635
+ if (Array.isArray(schema.anyOf)) {
636
+ return schema.anyOf.some((item) => isJsonSchemaObject(item) && item.type === "null") ? schema : { ...schema, anyOf: [...schema.anyOf, { type: "null" }] };
637
+ }
638
+ return { anyOf: [schema, { type: "null" }] };
639
+ }
640
+ function toOpenAiStrictJsonSchema(schema) {
641
+ if (Array.isArray(schema)) {
642
+ return schema.map((item) => toOpenAiStrictJsonSchema(item));
643
+ }
644
+ if (!isJsonSchemaObject(schema)) {
645
+ return schema;
646
+ }
647
+ const normalizedEntries = Object.entries(schema).filter(([key]) => key !== "$schema").map(([key, value]) => [key, toOpenAiStrictJsonSchema(value)]);
648
+ const normalizedSchema = Object.fromEntries(normalizedEntries);
649
+ if (isJsonSchemaObject(normalizedSchema.properties)) {
650
+ const properties = normalizedSchema.properties;
651
+ const originalRequired = Array.isArray(normalizedSchema.required) ? normalizedSchema.required.filter((item) => typeof item === "string") : [];
652
+ const requiredSet = new Set(originalRequired);
653
+ const propertyEntries = Object.entries(properties).map(([key, value]) => {
654
+ const normalizedProperty = isJsonSchemaObject(value) ? value : {};
655
+ return [key, requiredSet.has(key) ? normalizedProperty : allowNullInSchema(normalizedProperty)];
656
+ });
657
+ return {
658
+ ...normalizedSchema,
659
+ properties: Object.fromEntries(propertyEntries),
660
+ required: Object.keys(properties),
661
+ additionalProperties: false
662
+ };
663
+ }
664
+ return normalizedSchema;
665
+ }
666
+ function stripNullObjectProperties(value) {
667
+ if (Array.isArray(value)) {
668
+ return value.map((item) => stripNullObjectProperties(item));
669
+ }
670
+ if (!isJsonSchemaObject(value)) {
671
+ return value;
672
+ }
673
+ const entries = Object.entries(value).filter(([, item]) => item !== null).map(([key, item]) => [key, stripNullObjectProperties(item)]);
674
+ return Object.fromEntries(entries);
675
+ }
676
+ function buildStructuredFormat(schema) {
677
+ return {
678
+ type: "json_schema",
679
+ name: "swarmvault_response",
680
+ schema: toOpenAiStrictJsonSchema(z4.toJSONSchema(schema)),
681
+ strict: true
682
+ };
683
+ }
684
+ var OpenAiCompatibleProviderAdapter = class extends BaseProviderAdapter {
685
+ baseUrl;
686
+ apiKey;
687
+ headers;
688
+ apiStyle;
689
+ constructor(id, type, model, options) {
690
+ super(id, type, model, options.capabilities);
691
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
692
+ this.apiKey = options.apiKey;
693
+ this.headers = options.headers;
694
+ this.apiStyle = options.apiStyle ?? "responses";
695
+ }
696
+ async generateText(request) {
697
+ if (this.apiStyle === "chat") {
698
+ return this.generateViaChatCompletions(request);
699
+ }
700
+ return this.generateViaResponses(request);
701
+ }
702
+ async generateStructured(request, schema) {
703
+ if (this.type !== "openai") {
704
+ return super.generateStructured(request, schema);
705
+ }
706
+ const structuredFormat = buildStructuredFormat(schema);
707
+ const text = this.apiStyle === "chat" ? await this.generateStructuredViaChatCompletions(
708
+ {
709
+ ...request
710
+ },
711
+ structuredFormat
712
+ ) : await this.generateStructuredViaResponses(
713
+ {
714
+ ...request
715
+ },
716
+ structuredFormat
717
+ );
718
+ return schema.parse(stripNullObjectProperties(JSON.parse(extractJson(text))));
719
+ }
720
+ async generateImage(request) {
721
+ const encodedAttachments = await this.encodeAttachments(request.attachments);
722
+ const response = await fetch(`${this.baseUrl}/images/generations`, {
723
+ method: "POST",
724
+ headers: {
725
+ "content-type": "application/json",
726
+ ...buildAuthHeaders(this.apiKey),
727
+ ...this.headers
728
+ },
729
+ body: JSON.stringify({
730
+ model: this.model,
731
+ prompt: request.prompt,
732
+ size: request.width && request.height ? `${Math.max(256, Math.round(request.width))}x${Math.max(256, Math.round(request.height))}` : void 0,
733
+ response_format: "b64_json",
734
+ ...encodedAttachments.length ? {
735
+ input_image: encodedAttachments.map((item) => `data:${item.mimeType};base64,${item.base64}`)
736
+ } : {}
737
+ })
738
+ });
739
+ if (!response.ok) {
740
+ throw new Error(`Provider ${this.id} failed: ${response.status} ${response.statusText}`);
741
+ }
742
+ const payload = await response.json();
743
+ const image = payload.data?.[0];
744
+ if (!image?.b64_json) {
745
+ throw new Error(`Provider ${this.id} returned no image data.`);
746
+ }
747
+ return {
748
+ mimeType: "image/png",
749
+ bytes: Buffer.from(image.b64_json, "base64"),
750
+ width: request.width,
751
+ height: request.height,
752
+ revisedPrompt: image.revised_prompt
753
+ };
754
+ }
755
+ async generateViaResponses(request) {
756
+ const encodedAttachments = await this.encodeAttachments(request.attachments);
757
+ const input = encodedAttachments.length ? [
758
+ {
759
+ role: "user",
760
+ content: [
761
+ { type: "input_text", text: request.prompt },
762
+ ...encodedAttachments.map((item) => ({
763
+ type: "input_image",
764
+ image_url: `data:${item.mimeType};base64,${item.base64}`
765
+ }))
766
+ ]
767
+ }
768
+ ] : request.prompt;
769
+ const response = await fetch(`${this.baseUrl}/responses`, {
770
+ method: "POST",
771
+ headers: {
772
+ "content-type": "application/json",
773
+ ...buildAuthHeaders(this.apiKey),
774
+ ...this.headers
775
+ },
776
+ body: JSON.stringify({
777
+ model: this.model,
778
+ input,
779
+ instructions: request.system,
780
+ max_output_tokens: request.maxOutputTokens
781
+ })
782
+ });
783
+ if (!response.ok) {
784
+ throw new Error(`Provider ${this.id} failed: ${response.status} ${response.statusText}`);
785
+ }
786
+ const payload = await response.json();
787
+ return {
788
+ text: extractResponsesText(payload),
789
+ usage: payload.usage ? { inputTokens: payload.usage.input_tokens, outputTokens: payload.usage.output_tokens } : void 0
790
+ };
791
+ }
792
+ async generateStructuredViaResponses(request, format) {
793
+ const encodedAttachments = await this.encodeAttachments(request.attachments);
794
+ const input = encodedAttachments.length ? [
795
+ {
796
+ role: "user",
797
+ content: [
798
+ { type: "input_text", text: request.prompt },
799
+ ...encodedAttachments.map((item) => ({
800
+ type: "input_image",
801
+ image_url: `data:${item.mimeType};base64,${item.base64}`
802
+ }))
803
+ ]
804
+ }
805
+ ] : request.prompt;
806
+ const response = await fetch(`${this.baseUrl}/responses`, {
807
+ method: "POST",
808
+ headers: {
809
+ "content-type": "application/json",
810
+ ...buildAuthHeaders(this.apiKey),
811
+ ...this.headers
812
+ },
813
+ body: JSON.stringify({
814
+ model: this.model,
815
+ input,
816
+ instructions: request.system,
817
+ max_output_tokens: request.maxOutputTokens,
818
+ text: {
819
+ format
820
+ }
821
+ })
822
+ });
823
+ if (!response.ok) {
824
+ throw new Error(`Provider ${this.id} failed: ${response.status} ${response.statusText}`);
825
+ }
826
+ const payload = await response.json();
827
+ return extractResponsesText(payload);
828
+ }
829
+ async generateViaChatCompletions(request) {
830
+ const encodedAttachments = await this.encodeAttachments(request.attachments);
831
+ const content = encodedAttachments.length ? [
832
+ { type: "text", text: request.prompt },
833
+ ...encodedAttachments.map((item) => ({
834
+ type: "image_url",
835
+ image_url: {
836
+ url: `data:${item.mimeType};base64,${item.base64}`
837
+ }
838
+ }))
839
+ ] : request.prompt;
840
+ const messages = [...request.system ? [{ role: "system", content: request.system }] : [], { role: "user", content }];
841
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
842
+ method: "POST",
843
+ headers: {
844
+ "content-type": "application/json",
845
+ ...buildAuthHeaders(this.apiKey),
846
+ ...this.headers
847
+ },
848
+ body: JSON.stringify({
849
+ model: this.model,
850
+ messages,
851
+ max_tokens: request.maxOutputTokens
852
+ })
853
+ });
854
+ if (!response.ok) {
855
+ throw new Error(`Provider ${this.id} failed: ${response.status} ${response.statusText}`);
856
+ }
857
+ const payload = await response.json();
858
+ const contentValue = payload.choices?.[0]?.message?.content;
859
+ const text = Array.isArray(contentValue) ? contentValue.map((item) => item.text ?? "").join("\n") : contentValue ?? "";
860
+ return {
861
+ text,
862
+ usage: payload.usage ? { inputTokens: payload.usage.prompt_tokens, outputTokens: payload.usage.completion_tokens } : void 0
863
+ };
864
+ }
865
+ async generateStructuredViaChatCompletions(request, format) {
866
+ const encodedAttachments = await this.encodeAttachments(request.attachments);
867
+ const content = encodedAttachments.length ? [
868
+ { type: "text", text: request.prompt },
869
+ ...encodedAttachments.map((item) => ({
870
+ type: "image_url",
871
+ image_url: {
872
+ url: `data:${item.mimeType};base64,${item.base64}`
873
+ }
874
+ }))
875
+ ] : request.prompt;
876
+ const messages = [...request.system ? [{ role: "system", content: request.system }] : [], { role: "user", content }];
877
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
878
+ method: "POST",
879
+ headers: {
880
+ "content-type": "application/json",
881
+ ...buildAuthHeaders(this.apiKey),
882
+ ...this.headers
883
+ },
884
+ body: JSON.stringify({
885
+ model: this.model,
886
+ messages,
887
+ max_tokens: request.maxOutputTokens,
888
+ response_format: {
889
+ type: "json_schema",
890
+ json_schema: format
891
+ }
892
+ })
893
+ });
894
+ if (!response.ok) {
895
+ throw new Error(`Provider ${this.id} failed: ${response.status} ${response.statusText}`);
896
+ }
897
+ const payload = await response.json();
898
+ const contentValue = payload.choices?.[0]?.message?.content;
899
+ return Array.isArray(contentValue) ? contentValue.map((item) => item.text ?? "").join("\n") : contentValue ?? "";
900
+ }
901
+ };
902
+
903
+ // src/providers/registry.ts
904
+ var customModuleSchema = z5.object({
905
+ createAdapter: z5.function({
906
+ input: [z5.string(), z5.custom(), z5.string()],
907
+ output: z5.promise(z5.custom())
908
+ })
909
+ });
910
+ function resolveCapabilities(config, fallback) {
911
+ return config.capabilities?.length ? config.capabilities : fallback;
912
+ }
913
+ function envOrUndefined(name) {
914
+ return name ? process.env[name] : void 0;
915
+ }
916
+ function createOpenAiCompatiblePreset(id, type, config, defaults) {
917
+ return new OpenAiCompatibleProviderAdapter(id, type, config.model, {
918
+ baseUrl: config.baseUrl ?? defaults.baseUrl,
919
+ apiKey: envOrUndefined(config.apiKeyEnv ?? defaults.apiKeyEnv),
920
+ headers: config.headers,
921
+ apiStyle: config.apiStyle ?? defaults.apiStyle ?? "chat",
922
+ capabilities: resolveCapabilities(config, defaults.capabilities)
923
+ });
924
+ }
925
+ async function createProvider(id, config, rootDir) {
926
+ switch (config.type) {
927
+ case "heuristic":
928
+ return new HeuristicProviderAdapter(id, config.model);
929
+ case "openai":
930
+ return new OpenAiCompatibleProviderAdapter(id, "openai", config.model, {
931
+ baseUrl: config.baseUrl ?? "https://api.openai.com/v1",
932
+ apiKey: envOrUndefined(config.apiKeyEnv),
933
+ headers: config.headers,
934
+ apiStyle: config.apiStyle ?? "responses",
935
+ capabilities: resolveCapabilities(config, ["responses", "chat", "structured", "tools", "vision", "streaming", "image_generation"])
936
+ });
937
+ case "ollama":
938
+ return new OpenAiCompatibleProviderAdapter(id, "ollama", config.model, {
939
+ baseUrl: config.baseUrl ?? "http://localhost:11434/v1",
940
+ apiKey: envOrUndefined(config.apiKeyEnv) ?? "ollama",
941
+ headers: config.headers,
942
+ apiStyle: config.apiStyle ?? "responses",
943
+ capabilities: resolveCapabilities(config, ["responses", "chat", "structured", "tools", "vision", "streaming", "local"])
944
+ });
945
+ case "openai-compatible":
946
+ return new OpenAiCompatibleProviderAdapter(id, "openai-compatible", config.model, {
947
+ baseUrl: config.baseUrl ?? "http://localhost:8000/v1",
948
+ apiKey: envOrUndefined(config.apiKeyEnv),
949
+ headers: config.headers,
950
+ apiStyle: config.apiStyle ?? "responses",
951
+ capabilities: resolveCapabilities(config, ["chat", "structured"])
952
+ });
953
+ case "openrouter":
954
+ return createOpenAiCompatiblePreset(id, "openrouter", config, {
955
+ baseUrl: "https://openrouter.ai/api/v1",
956
+ apiKeyEnv: "OPENROUTER_API_KEY",
957
+ apiStyle: "chat",
958
+ capabilities: ["chat", "structured"]
959
+ });
960
+ case "groq":
961
+ return createOpenAiCompatiblePreset(id, "groq", config, {
962
+ baseUrl: "https://api.groq.com/openai/v1",
963
+ apiKeyEnv: "GROQ_API_KEY",
964
+ apiStyle: "chat",
965
+ capabilities: ["chat", "structured"]
966
+ });
967
+ case "together":
968
+ return createOpenAiCompatiblePreset(id, "together", config, {
969
+ baseUrl: "https://api.together.xyz/v1",
970
+ apiKeyEnv: "TOGETHER_API_KEY",
971
+ apiStyle: "chat",
972
+ capabilities: ["chat", "structured"]
973
+ });
974
+ case "xai":
975
+ return createOpenAiCompatiblePreset(id, "xai", config, {
976
+ baseUrl: "https://api.x.ai/v1",
977
+ apiKeyEnv: "XAI_API_KEY",
978
+ apiStyle: "chat",
979
+ capabilities: ["chat", "structured"]
980
+ });
981
+ case "cerebras":
982
+ return createOpenAiCompatiblePreset(id, "cerebras", config, {
983
+ baseUrl: "https://api.cerebras.ai/v1",
984
+ apiKeyEnv: "CEREBRAS_API_KEY",
985
+ apiStyle: "chat",
986
+ capabilities: ["chat", "structured"]
987
+ });
988
+ case "anthropic":
989
+ return new AnthropicProviderAdapter(id, config.model, {
990
+ apiKey: envOrUndefined(config.apiKeyEnv),
991
+ headers: config.headers,
992
+ baseUrl: config.baseUrl
993
+ });
994
+ case "gemini":
995
+ return new GeminiProviderAdapter(id, config.model, {
996
+ apiKey: envOrUndefined(config.apiKeyEnv),
997
+ baseUrl: config.baseUrl
998
+ });
999
+ case "custom": {
1000
+ if (!config.module) {
1001
+ throw new Error(`Provider ${id} is type "custom" but no module path was configured.`);
1002
+ }
1003
+ const resolvedModule = path3.isAbsolute(config.module) ? config.module : path3.resolve(rootDir, config.module);
1004
+ const loaded = await import(pathToFileURL(resolvedModule).href);
1005
+ const parsed = customModuleSchema.parse(loaded);
1006
+ return parsed.createAdapter(id, config, rootDir);
1007
+ }
1008
+ default:
1009
+ throw new Error(`Unsupported provider type ${String(config.type)}`);
1010
+ }
1011
+ }
1012
+ async function getProviderForTask(rootDir, task) {
1013
+ const { config } = await loadVaultConfig(rootDir);
1014
+ const providerId = config.tasks[task];
1015
+ if (!providerId) {
1016
+ throw new Error(`No provider configured for task "${String(task)}".`);
1017
+ }
1018
+ const providerConfig = config.providers[providerId];
1019
+ if (!providerConfig) {
1020
+ throw new Error(`No provider configured with id "${providerId}" for task "${task}".`);
1021
+ }
1022
+ return createProvider(providerId, providerConfig, rootDir);
1023
+ }
1024
+ function assertProviderCapability(provider, capability) {
1025
+ if (!provider.capabilities.has(capability)) {
1026
+ throw new Error(`Provider ${provider.id} does not support required capability "${capability}".`);
1027
+ }
1028
+ }
1029
+ async function getResolvedPaths(rootDir) {
1030
+ return (await loadVaultConfig(rootDir)).paths;
1031
+ }
1032
+
1033
+ export {
1034
+ slugify,
1035
+ sha256,
1036
+ ensureDir,
1037
+ fileExists,
1038
+ readJsonFile,
1039
+ writeJsonFile,
1040
+ appendJsonLine,
1041
+ writeFileIfChanged,
1042
+ toPosix,
1043
+ firstSentences,
1044
+ uniqueBy,
1045
+ normalizeWhitespace,
1046
+ truncate,
1047
+ listFilesRecursive,
1048
+ defaultVaultConfig,
1049
+ defaultVaultSchema,
1050
+ resolvePaths,
1051
+ loadVaultConfig,
1052
+ initWorkspace,
1053
+ createProvider,
1054
+ getProviderForTask,
1055
+ assertProviderCapability,
1056
+ getResolvedPaths
1057
+ };