@swarmvaultai/engine 0.1.12 → 0.1.13

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