@swarmvaultai/engine 0.3.0 → 0.4.0

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +39 -4
  2. package/dist/index.js +1457 -132
  3. package/package.json +6 -1
package/dist/index.js CHANGED
@@ -4504,6 +4504,7 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
4504
4504
  import fs7 from "fs/promises";
4505
4505
  import os from "os";
4506
4506
  import path7 from "path";
4507
+ import { Readable } from "stream";
4507
4508
  import { parse as parseCsvSync } from "csv-parse/sync";
4508
4509
  import { strFromU8, unzipSync } from "fflate";
4509
4510
  import { JSDOM } from "jsdom";
@@ -5171,6 +5172,508 @@ async function extractEpubChapters(input) {
5171
5172
  };
5172
5173
  }
5173
5174
  }
5175
+ function timestampFromMs(value) {
5176
+ const totalMs = Math.max(0, Math.floor(value));
5177
+ const totalSeconds = Math.floor(totalMs / 1e3);
5178
+ const hours = Math.floor(totalSeconds / 3600);
5179
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
5180
+ const seconds = totalSeconds % 60;
5181
+ const milliseconds = totalMs % 1e3;
5182
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(
5183
+ milliseconds
5184
+ ).padStart(3, "0")}`;
5185
+ }
5186
+ function normalizeDelimitedList(values) {
5187
+ const unique = [...new Set(values.map((value) => normalizeWhitespace(value)).filter(Boolean))];
5188
+ return unique.length ? unique.join(", ") : void 0;
5189
+ }
5190
+ function normalizeIsoDate(value) {
5191
+ if (value instanceof Date && Number.isFinite(value.getTime())) {
5192
+ return value.toISOString();
5193
+ }
5194
+ if (typeof value === "string" && value.trim()) {
5195
+ const parsed = new Date(value);
5196
+ if (Number.isFinite(parsed.getTime())) {
5197
+ return parsed.toISOString();
5198
+ }
5199
+ }
5200
+ return void 0;
5201
+ }
5202
+ function addressNames(value) {
5203
+ if (!value || typeof value !== "object" || !("value" in value) || !Array.isArray(value.value)) {
5204
+ return [];
5205
+ }
5206
+ return value.value.map((entry) => normalizeWhitespace(entry.name ?? entry.address ?? "")).filter(Boolean);
5207
+ }
5208
+ function addressList(value) {
5209
+ return normalizeDelimitedList(addressNames(value));
5210
+ }
5211
+ function emailConversationId(parsed) {
5212
+ const asArray = (value) => Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
5213
+ return normalizeWhitespace(parsed.messageId ?? "") || normalizeWhitespace(asArray(parsed.inReplyTo)[0] ?? "") || normalizeWhitespace(asArray(parsed.references)[0] ?? "") || void 0;
5214
+ }
5215
+ function emailBodyMarkdown(parsed) {
5216
+ const text = normalizeDocumentText(parsed.text ?? "");
5217
+ if (text) {
5218
+ return text;
5219
+ }
5220
+ if (typeof parsed.html === "string" && parsed.html.trim()) {
5221
+ return normalizeDocumentText(htmlToMarkdown(parsed.html));
5222
+ }
5223
+ return "";
5224
+ }
5225
+ function normalizeParsedEmail(parsed, fallbackTitle) {
5226
+ const title = normalizeWhitespace(parsed.subject ?? "") || fallbackTitle;
5227
+ const sender = addressList(parsed.from);
5228
+ const recipients = addressList(parsed.to);
5229
+ const cc = addressList(parsed.cc);
5230
+ const occurredAt = normalizeIsoDate(parsed.date);
5231
+ const participants = normalizeDelimitedList([...addressNames(parsed.from), ...addressNames(parsed.to), ...addressNames(parsed.cc)]);
5232
+ const conversationId = emailConversationId(parsed);
5233
+ const body = emailBodyMarkdown(parsed);
5234
+ const attachmentCount = Array.isArray(parsed.attachments) ? parsed.attachments.length : 0;
5235
+ return {
5236
+ title,
5237
+ conversationId,
5238
+ metadata: {
5239
+ ...occurredAt ? { occurred_at: occurredAt } : {},
5240
+ ...sender ? { sender } : {},
5241
+ ...recipients ? { recipients } : {},
5242
+ ...cc ? { cc } : {},
5243
+ ...participants ? { participants } : {},
5244
+ ...conversationId ? { conversation_id: conversationId } : {},
5245
+ ...normalizeWhitespace(parsed.messageId ?? "") ? { message_id: normalizeWhitespace(parsed.messageId ?? "") } : {},
5246
+ ...attachmentCount ? { attachment_count: String(attachmentCount) } : {}
5247
+ },
5248
+ markdown: [
5249
+ `# ${title}`,
5250
+ "",
5251
+ ...occurredAt ? [`Date: ${occurredAt}`] : [],
5252
+ ...sender ? [`From: ${sender}`] : [],
5253
+ ...recipients ? [`To: ${recipients}`] : [],
5254
+ ...cc ? [`CC: ${cc}`] : [],
5255
+ ...conversationId ? [`Conversation ID: ${conversationId}`] : [],
5256
+ ...attachmentCount ? [`Attachments: ${attachmentCount}`] : [],
5257
+ "",
5258
+ "## Message",
5259
+ "",
5260
+ body || "No readable body content was extracted from this email.",
5261
+ ""
5262
+ ].join("\n")
5263
+ };
5264
+ }
5265
+ function calendarAttendees(value) {
5266
+ if (!value) {
5267
+ return [];
5268
+ }
5269
+ const attendees = Array.isArray(value) ? value : [value];
5270
+ return attendees.map((entry) => {
5271
+ if (!entry || typeof entry !== "object") {
5272
+ return "";
5273
+ }
5274
+ const item = entry;
5275
+ const name = normalizeWhitespace(String(item.params?.CN ?? ""));
5276
+ const address = normalizeWhitespace(String(item.val ?? item.value ?? ""));
5277
+ return name || address;
5278
+ }).filter(Boolean);
5279
+ }
5280
+ function slackFormatSpeakerId(input, usersById) {
5281
+ return usersById.get(input) ?? input;
5282
+ }
5283
+ function slackNormalizeText(text, usersById) {
5284
+ return normalizeWhitespace(
5285
+ text.replace(/<@([A-Z0-9]+)>/g, (_, userId) => `@${slackFormatSpeakerId(userId, usersById)}`).replace(/<#[A-Z0-9]+\|([^>]+)>/g, "#$1").replace(/<(https?:\/\/[^>|]+)\|([^>]+)>/g, "$2 ($1)").replace(/<(https?:\/\/[^>]+)>/g, "$1")
5286
+ );
5287
+ }
5288
+ function slackMessageTimestamp(ts2, fallbackDate) {
5289
+ const numeric = Number(ts2);
5290
+ if (Number.isFinite(numeric) && numeric > 0) {
5291
+ return new Date(numeric * 1e3).toISOString();
5292
+ }
5293
+ return (/* @__PURE__ */ new Date(`${fallbackDate}T00:00:00.000Z`)).toISOString();
5294
+ }
5295
+ async function loadZipMessageBuffers(bytes) {
5296
+ const { MboxStream } = await import("node-mbox");
5297
+ const stream = MboxStream(Readable.from([bytes]));
5298
+ return await new Promise((resolve, reject) => {
5299
+ const messages = [];
5300
+ stream.on("data", (message) => {
5301
+ messages.push(Buffer.isBuffer(message) ? message : Buffer.from(message));
5302
+ });
5303
+ stream.on("error", reject);
5304
+ stream.on("finish", () => resolve(messages));
5305
+ stream.on("end", () => resolve(messages));
5306
+ });
5307
+ }
5308
+ function archiveEntriesAsText(archive) {
5309
+ return new Map(
5310
+ Object.entries(archive).filter(([, value]) => value).map(([entryPath, value]) => [entryPath, strFromU8(value)])
5311
+ );
5312
+ }
5313
+ function looksLikeSlackEntries(entries) {
5314
+ const all = [...entries];
5315
+ const hasChannelsIndex = all.some(
5316
+ (entry) => entry === "channels.json" || entry === "groups.json" || entry === "dms.json" || entry === "mpims.json"
5317
+ );
5318
+ const hasChannelDayFiles = all.some((entry) => /^[^/]+\/\d{4}-\d{2}-\d{2}\.json$/i.test(entry));
5319
+ return hasChannelsIndex && hasChannelDayFiles;
5320
+ }
5321
+ function slackEntriesFromChannelIndex(raw, usersById) {
5322
+ const entries = /* @__PURE__ */ new Map();
5323
+ if (!Array.isArray(raw)) {
5324
+ return entries;
5325
+ }
5326
+ for (const item of raw) {
5327
+ if (!item || typeof item !== "object") {
5328
+ continue;
5329
+ }
5330
+ const value = item;
5331
+ const id = normalizeWhitespace(value.id ?? "");
5332
+ const title = normalizeWhitespace(value.name ?? "");
5333
+ if (!title) {
5334
+ continue;
5335
+ }
5336
+ const members = (Array.isArray(value.members) ? value.members : value.user ? [value.user] : []).map((member) => slackFormatSpeakerId(member, usersById)).filter(Boolean);
5337
+ entries.set(title, { id, title, members });
5338
+ }
5339
+ return entries;
5340
+ }
5341
+ async function extractTranscriptText(input) {
5342
+ try {
5343
+ const { parseSync } = await import("subtitle");
5344
+ const rawText = decodeTextBytes(input.bytes);
5345
+ const cues = parseSync(rawText).filter((node) => node.type === "cue" && node.data).map((node) => ({
5346
+ start: Math.max(0, node.data?.start ?? 0),
5347
+ end: Math.max(0, node.data?.end ?? 0),
5348
+ text: normalizeWhitespace((node.data?.text ?? "").replace(/\s*\n+\s*/g, " "))
5349
+ })).filter((cue) => cue.text);
5350
+ const title = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : void 0;
5351
+ const extractedText = [
5352
+ title ? `# ${title}` : null,
5353
+ `Format: ${input.fileName?.toLowerCase().endsWith(".vtt") ? "WebVTT" : "SRT"}`,
5354
+ `Segments: ${cues.length}`,
5355
+ ...cues.length ? [`Start: ${timestampFromMs(cues[0].start)}`, `End: ${timestampFromMs(cues[cues.length - 1].end)}`] : [],
5356
+ "",
5357
+ "## Transcript",
5358
+ "",
5359
+ ...cues.length ? cues.map((cue) => `- [${timestampFromMs(cue.start)} - ${timestampFromMs(cue.end)}] ${cue.text}`) : ["- No transcript segments were extracted."],
5360
+ ""
5361
+ ].filter((item) => Boolean(item)).join("\n");
5362
+ return {
5363
+ title,
5364
+ extractedText,
5365
+ artifact: {
5366
+ ...extractionMetadata("transcript", input.mimeType, "transcript_text"),
5367
+ metadata: {
5368
+ format: input.fileName?.toLowerCase().endsWith(".vtt") ? "vtt" : "srt",
5369
+ segment_count: String(cues.length),
5370
+ ...cues.length ? { started_at: timestampFromMs(cues[0].start), ended_at: timestampFromMs(cues[cues.length - 1].end) } : {}
5371
+ }
5372
+ }
5373
+ };
5374
+ } catch (error) {
5375
+ return {
5376
+ artifact: {
5377
+ ...extractionMetadata("transcript", input.mimeType, "transcript_text"),
5378
+ warnings: [`Transcript extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5379
+ }
5380
+ };
5381
+ }
5382
+ }
5383
+ async function extractEmailText(input) {
5384
+ try {
5385
+ const { simpleParser } = await import("mailparser");
5386
+ const fallbackTitle = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Email";
5387
+ const parsed = await simpleParser(input.bytes);
5388
+ const normalized = normalizeParsedEmail(parsed, fallbackTitle);
5389
+ return {
5390
+ title: normalized.title,
5391
+ extractedText: normalized.markdown,
5392
+ artifact: {
5393
+ ...extractionMetadata("email", input.mimeType, "email_text"),
5394
+ metadata: normalized.metadata
5395
+ }
5396
+ };
5397
+ } catch (error) {
5398
+ return {
5399
+ artifact: {
5400
+ ...extractionMetadata("email", input.mimeType, "email_text"),
5401
+ warnings: [`Email extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5402
+ }
5403
+ };
5404
+ }
5405
+ }
5406
+ async function extractMboxMessages(input) {
5407
+ try {
5408
+ const title = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Mailbox";
5409
+ const { simpleParser } = await import("mailparser");
5410
+ const messages = await loadZipMessageBuffers(input.bytes);
5411
+ const extracted = [];
5412
+ for (let index = 0; index < messages.length; index += 1) {
5413
+ const parsed = await simpleParser(messages[index]);
5414
+ const normalized = normalizeParsedEmail(parsed, `Message ${index + 1}`);
5415
+ const conversationId = normalized.conversationId || `${index + 1}`;
5416
+ extracted.push({
5417
+ partKey: `${conversationId}-${index + 1}`,
5418
+ title: normalized.title,
5419
+ markdown: normalized.markdown,
5420
+ metadata: {
5421
+ ...normalized.metadata,
5422
+ container_title: title,
5423
+ mailbox_title: title,
5424
+ part_index: String(index + 1),
5425
+ part_count: String(messages.length)
5426
+ }
5427
+ });
5428
+ }
5429
+ return {
5430
+ title,
5431
+ messages: extracted,
5432
+ warnings: extracted.length ? void 0 : ["Mailbox extraction completed but found no readable messages."]
5433
+ };
5434
+ } catch (error) {
5435
+ return {
5436
+ messages: [],
5437
+ warnings: [`Mailbox extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5438
+ };
5439
+ }
5440
+ }
5441
+ async function extractCalendarEvents(input) {
5442
+ try {
5443
+ const ical = await import("node-ical");
5444
+ const calendarTitle = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Calendar";
5445
+ const parsed = ical.default.sync.parseICS(decodeTextBytes(input.bytes));
5446
+ const events = [];
5447
+ for (const item of Object.values(parsed)) {
5448
+ if (!item || typeof item !== "object" || item.type !== "VEVENT") {
5449
+ continue;
5450
+ }
5451
+ const event = item;
5452
+ const title = normalizeWhitespace(event.summary ?? "") || "Calendar Event";
5453
+ const occurredAt = normalizeIsoDate(event.start);
5454
+ const endsAt = normalizeIsoDate(event.end);
5455
+ const organizer = event.organizer ? normalizeWhitespace(String(event.organizer.params?.CN ?? event.organizer.val ?? "")) : void 0;
5456
+ const attendees = calendarAttendees(event.attendees);
5457
+ const participants = normalizeDelimitedList([organizer ?? "", ...attendees]);
5458
+ const location = normalizeWhitespace(event.location ?? "") || void 0;
5459
+ const description = normalizeDocumentText(event.description ?? "");
5460
+ const conversationId = normalizeWhitespace(event.uid ?? "") || `${title}-${occurredAt ?? events.length + 1}`;
5461
+ events.push({
5462
+ partKey: conversationId,
5463
+ title,
5464
+ metadata: {
5465
+ container_title: calendarTitle,
5466
+ ...occurredAt ? { occurred_at: occurredAt } : {},
5467
+ ...endsAt ? { ends_at: endsAt } : {},
5468
+ ...organizer ? { organizer } : {},
5469
+ ...location ? { location } : {},
5470
+ ...participants ? { participants } : {},
5471
+ conversation_id: conversationId
5472
+ },
5473
+ markdown: [
5474
+ `# ${title}`,
5475
+ "",
5476
+ ...occurredAt ? [`Start: ${occurredAt}`] : [],
5477
+ ...endsAt ? [`End: ${endsAt}`] : [],
5478
+ ...organizer ? [`Organizer: ${organizer}`] : [],
5479
+ ...attendees.length ? [`Attendees: ${attendees.join(", ")}`] : [],
5480
+ ...location ? [`Location: ${location}`] : [],
5481
+ ...conversationId ? [`Event ID: ${conversationId}`] : [],
5482
+ "",
5483
+ "## Description",
5484
+ "",
5485
+ description || "No event description was provided.",
5486
+ ""
5487
+ ].join("\n")
5488
+ });
5489
+ }
5490
+ return {
5491
+ title: calendarTitle,
5492
+ events,
5493
+ warnings: events.length ? void 0 : ["Calendar extraction completed but found no VEVENT entries."]
5494
+ };
5495
+ } catch (error) {
5496
+ return {
5497
+ events: [],
5498
+ warnings: [`Calendar extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5499
+ };
5500
+ }
5501
+ }
5502
+ function parseSlackExportEntries(entries, exportTitle) {
5503
+ const usersById = /* @__PURE__ */ new Map();
5504
+ const rawUsers = entries.get("users.json");
5505
+ if (rawUsers) {
5506
+ const parsed = JSON.parse(rawUsers);
5507
+ for (const user of parsed) {
5508
+ const id = normalizeWhitespace(user.id ?? "");
5509
+ const name = normalizeWhitespace(user.profile?.display_name ?? user.real_name ?? user.profile?.real_name ?? user.name ?? "");
5510
+ if (id && name) {
5511
+ usersById.set(id, name);
5512
+ }
5513
+ }
5514
+ }
5515
+ const channelIndex = /* @__PURE__ */ new Map();
5516
+ for (const indexPath of ["channels.json", "groups.json", "dms.json", "mpims.json"]) {
5517
+ const rawIndex = entries.get(indexPath);
5518
+ if (!rawIndex) {
5519
+ continue;
5520
+ }
5521
+ const parsed = JSON.parse(rawIndex);
5522
+ for (const [key, value] of slackEntriesFromChannelIndex(parsed, usersById)) {
5523
+ channelIndex.set(key, value);
5524
+ }
5525
+ }
5526
+ const conversationPaths = [...entries.keys()].filter((entryPath) => /^[^/]+\/\d{4}-\d{2}-\d{2}\.json$/i.test(entryPath)).sort((left, right) => left.localeCompare(right));
5527
+ const conversations = [];
5528
+ for (const entryPath of conversationPaths) {
5529
+ const raw = entries.get(entryPath);
5530
+ if (!raw) {
5531
+ continue;
5532
+ }
5533
+ const messages = JSON.parse(raw);
5534
+ if (!Array.isArray(messages)) {
5535
+ continue;
5536
+ }
5537
+ const [channelName, dateFile] = entryPath.split("/");
5538
+ const date = dateFile?.replace(/\.json$/i, "") ?? "";
5539
+ const channel = channelIndex.get(channelName ?? "") ?? {
5540
+ id: channelName ?? "",
5541
+ title: channelName ?? "channel",
5542
+ members: []
5543
+ };
5544
+ const participants = new Set(channel.members);
5545
+ const lines = [];
5546
+ const threadIds = /* @__PURE__ */ new Set();
5547
+ const sortedMessages = [...messages].sort((left, right) => Number(left.ts ?? 0) - Number(right.ts ?? 0));
5548
+ let occurredAt;
5549
+ for (const message of sortedMessages) {
5550
+ const speaker = normalizeWhitespace(
5551
+ message.username ?? message.bot_profile?.name ?? (message.user ? slackFormatSpeakerId(message.user, usersById) : "")
5552
+ ) || "unknown";
5553
+ participants.add(speaker);
5554
+ const messageTime = slackMessageTimestamp(message.ts, date);
5555
+ occurredAt ??= messageTime;
5556
+ const normalizedText = slackNormalizeText(
5557
+ [
5558
+ message.text ?? "",
5559
+ ...Array.isArray(message.files) ? message.files.map((file) => normalizeWhitespace(file.title ?? file.name ?? "")).filter(Boolean).map((label) => `Attachment: ${label}`) : []
5560
+ ].join("\n"),
5561
+ usersById
5562
+ );
5563
+ if (message.thread_ts && message.thread_ts !== message.ts) {
5564
+ threadIds.add(message.thread_ts);
5565
+ }
5566
+ lines.push(
5567
+ `- [${messageTime}] ${speaker}${message.thread_ts ? ` {thread:${message.thread_ts}}` : ""}${message.ts ? ` {id:${message.ts}}` : ""}: ${normalizedText || normalizeWhitespace(message.subtype ?? "") || "[no text]"}`
5568
+ );
5569
+ }
5570
+ const participantsList = normalizeDelimitedList([...participants]);
5571
+ const conversationId = `${channel.id || channel.title}:${date}`;
5572
+ conversations.push({
5573
+ partKey: `${channel.title}-${date}`,
5574
+ title: `#${channel.title} - ${date}`,
5575
+ metadata: {
5576
+ workspace_title: exportTitle,
5577
+ channel: channel.title,
5578
+ ...channel.id ? { channel_id: channel.id } : {},
5579
+ ...occurredAt ? { occurred_at: occurredAt } : {},
5580
+ ...participantsList ? { participants: participantsList } : {},
5581
+ container_title: `${exportTitle} / #${channel.title}`,
5582
+ conversation_id: conversationId,
5583
+ date,
5584
+ message_count: String(sortedMessages.length),
5585
+ thread_count: String(threadIds.size)
5586
+ },
5587
+ markdown: [
5588
+ `# #${channel.title} - ${date}`,
5589
+ "",
5590
+ `Workspace: ${exportTitle}`,
5591
+ `Messages: ${sortedMessages.length}`,
5592
+ `Threads: ${threadIds.size}`,
5593
+ ...participantsList ? [`Participants: ${participantsList}`] : [],
5594
+ "",
5595
+ "## Messages",
5596
+ "",
5597
+ ...lines.length ? lines : ["- No messages were extracted."],
5598
+ ""
5599
+ ].join("\n")
5600
+ });
5601
+ }
5602
+ return {
5603
+ title: exportTitle,
5604
+ conversations,
5605
+ warnings: conversations.length ? void 0 : ["Slack export parsing completed but found no channel day files."]
5606
+ };
5607
+ }
5608
+ function isSlackExportArchive(bytes) {
5609
+ try {
5610
+ const archive = unzipSync(new Uint8Array(bytes));
5611
+ return looksLikeSlackEntries(Object.keys(archive));
5612
+ } catch {
5613
+ return false;
5614
+ }
5615
+ }
5616
+ async function isSlackExportDirectory(directoryPath) {
5617
+ const entries = await fs7.readdir(directoryPath).catch(() => []);
5618
+ if (!entries.length) {
5619
+ return false;
5620
+ }
5621
+ const fileSet = new Set(entries);
5622
+ const hasIndex = ["channels.json", "groups.json", "dms.json", "mpims.json"].some((name) => fileSet.has(name));
5623
+ if (!hasIndex) {
5624
+ return false;
5625
+ }
5626
+ for (const entry of entries) {
5627
+ const channelDir = path7.join(directoryPath, entry);
5628
+ const stat = await fs7.stat(channelDir).catch(() => null);
5629
+ if (!stat?.isDirectory()) {
5630
+ continue;
5631
+ }
5632
+ const channelEntries = await fs7.readdir(channelDir).catch(() => []);
5633
+ if (channelEntries.some((name) => /^\d{4}-\d{2}-\d{2}\.json$/i.test(name))) {
5634
+ return true;
5635
+ }
5636
+ }
5637
+ return false;
5638
+ }
5639
+ async function extractSlackExportArchive(input) {
5640
+ try {
5641
+ const archive = unzipSync(new Uint8Array(input.bytes));
5642
+ const title = input.fileName ? path7.basename(input.fileName, path7.extname(input.fileName)) : "Slack Export";
5643
+ return parseSlackExportEntries(archiveEntriesAsText(archive), title);
5644
+ } catch (error) {
5645
+ return {
5646
+ conversations: [],
5647
+ warnings: [`Slack export extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5648
+ };
5649
+ }
5650
+ }
5651
+ async function extractSlackExportDirectory(directoryPath) {
5652
+ const title = path7.basename(directoryPath) || "Slack Export";
5653
+ try {
5654
+ const entries = /* @__PURE__ */ new Map();
5655
+ const queue = [directoryPath];
5656
+ while (queue.length > 0) {
5657
+ const current = queue.shift();
5658
+ const children = await fs7.readdir(current, { withFileTypes: true });
5659
+ for (const child of children) {
5660
+ const absoluteChild = path7.join(current, child.name);
5661
+ if (child.isDirectory()) {
5662
+ queue.push(absoluteChild);
5663
+ continue;
5664
+ }
5665
+ const relativeChild = path7.posix.relative(directoryPath, absoluteChild.split(path7.sep).join(path7.posix.sep));
5666
+ entries.set(relativeChild, await fs7.readFile(absoluteChild, "utf8"));
5667
+ }
5668
+ }
5669
+ return parseSlackExportEntries(entries, title);
5670
+ } catch (error) {
5671
+ return {
5672
+ conversations: [],
5673
+ warnings: [`Slack export extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
5674
+ };
5675
+ }
5676
+ }
5174
5677
 
5175
5678
  // src/logs.ts
5176
5679
  import fs8 from "fs/promises";
@@ -5600,6 +6103,9 @@ function inferKind(mimeType, filePath) {
5600
6103
  if (isRstFilePath(filePath)) {
5601
6104
  return "text";
5602
6105
  }
6106
+ if (isTranscriptFilePath(filePath) || mimeType === "application/x-subrip" || mimeType === "text/vtt") {
6107
+ return "transcript";
6108
+ }
5603
6109
  if (mimeType.includes("markdown")) {
5604
6110
  return "markdown";
5605
6111
  }
@@ -5612,6 +6118,12 @@ function inferKind(mimeType, filePath) {
5612
6118
  if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || filePath.toLowerCase().endsWith(".docx")) {
5613
6119
  return "docx";
5614
6120
  }
6121
+ if (isEmailFilePath(filePath) || mimeType === "message/rfc822" || mimeType === "application/mbox") {
6122
+ return "email";
6123
+ }
6124
+ if (isCalendarFilePath(filePath) || mimeType === "text/calendar") {
6125
+ return "calendar";
6126
+ }
5615
6127
  if (mimeType === "application/epub+zip" || filePath.toLowerCase().endsWith(".epub")) {
5616
6128
  return "epub";
5617
6129
  }
@@ -5636,6 +6148,17 @@ function isRstFilePath(filePath) {
5636
6148
  const extension = path12.extname(filePath).toLowerCase();
5637
6149
  return extension === ".rst" || extension === ".rest";
5638
6150
  }
6151
+ function isTranscriptFilePath(filePath) {
6152
+ const extension = path12.extname(filePath).toLowerCase();
6153
+ return extension === ".srt" || extension === ".vtt";
6154
+ }
6155
+ function isEmailFilePath(filePath) {
6156
+ const extension = path12.extname(filePath).toLowerCase();
6157
+ return extension === ".eml" || extension === ".mbox";
6158
+ }
6159
+ function isCalendarFilePath(filePath) {
6160
+ return path12.extname(filePath).toLowerCase() === ".ics";
6161
+ }
5639
6162
  function titleFromText(fallback, content, filePath) {
5640
6163
  if (filePath && isRstFilePath(filePath)) {
5641
6164
  const rstTitle = titleFromRst(fallback, content);
@@ -5656,6 +6179,53 @@ function sourceGroupIdFor(prepared) {
5656
6179
  const originKey = prepared.originType === "url" ? prepared.url ?? prepared.title : prepared.originalPath ?? prepared.title;
5657
6180
  return `${slugify(prepared.title)}-${sha256(originKey).slice(0, 8)}`;
5658
6181
  }
6182
+ function groupedPreparedInputsFor(input) {
6183
+ const groupId = sourceGroupIdFor({
6184
+ title: input.title,
6185
+ originType: input.originType,
6186
+ originalPath: input.originalPath,
6187
+ url: input.url
6188
+ });
6189
+ return input.parts.map(
6190
+ (part, index) => finalizePreparedInput({
6191
+ title: `${input.title} - ${part.title}`,
6192
+ originType: input.originType,
6193
+ sourceKind: input.sourceKind,
6194
+ sourceClass: input.sourceClass,
6195
+ originalPath: input.originalPath,
6196
+ repoRelativePath: input.repoRelativePath,
6197
+ url: input.url,
6198
+ mimeType: "text/markdown",
6199
+ storedExtension: input.storedExtension,
6200
+ payloadBytes: Buffer.from(part.markdown, "utf8"),
6201
+ extractedText: part.markdown,
6202
+ extractionArtifact: {
6203
+ extractor: `${input.sourceKind}_text`,
6204
+ sourceKind: input.sourceKind,
6205
+ mimeType: input.mimeType,
6206
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
6207
+ metadata: {
6208
+ ...part.metadata,
6209
+ part_index: String(index + 1),
6210
+ part_count: String(input.parts.length)
6211
+ },
6212
+ warnings: input.warnings
6213
+ },
6214
+ sourceGroupId: groupId,
6215
+ sourceGroupTitle: input.title,
6216
+ sourcePartKey: part.partKey,
6217
+ partIndex: index + 1,
6218
+ partCount: input.parts.length,
6219
+ partTitle: part.title,
6220
+ details: {
6221
+ ...part.metadata,
6222
+ part_index: String(index + 1),
6223
+ part_count: String(input.parts.length)
6224
+ },
6225
+ logDetails: input.logDetails
6226
+ })
6227
+ );
6228
+ }
5659
6229
  function rstAdornmentLine(line) {
5660
6230
  const trimmed = line.trim();
5661
6231
  if (trimmed.length < 3) {
@@ -5886,6 +6456,13 @@ async function findNearestGitRoot2(startPath) {
5886
6456
  current = parent;
5887
6457
  }
5888
6458
  }
6459
+ async function detectScopedRepoRoot(rootDir, inputPath, fallbackRoot) {
6460
+ const detectedRepoRoot = await findNearestGitRoot2(inputPath);
6461
+ if (!detectedRepoRoot) {
6462
+ return fallbackRoot;
6463
+ }
6464
+ return withinRoot(rootDir, inputPath) && !withinRoot(rootDir, detectedRepoRoot) ? fallbackRoot : detectedRepoRoot;
6465
+ }
5889
6466
  function withinRoot(rootPath, targetPath) {
5890
6467
  const relative = path12.relative(rootPath, targetPath);
5891
6468
  return relative === "" || !relative.startsWith("..") && !path12.isAbsolute(relative);
@@ -6440,7 +7017,13 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
6440
7017
  continue;
6441
7018
  }
6442
7019
  const mimeType = guessMimeType(absolutePath);
6443
- const sourceKind = inferKind(mimeType, absolutePath);
7020
+ let sourceKind = inferKind(mimeType, absolutePath);
7021
+ if (sourceKind === "binary" && path12.extname(absolutePath).toLowerCase() === ".zip") {
7022
+ const bytes = await fs11.readFile(absolutePath);
7023
+ if (isSlackExportArchive(bytes)) {
7024
+ sourceKind = "chat_export";
7025
+ }
7026
+ }
6444
7027
  const sourceClass = sourceClassForRelativePath(relativePath, options);
6445
7028
  if (!supportedDirectoryKind(sourceKind)) {
6446
7029
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
@@ -6775,7 +7358,7 @@ function preparedMatchesManifest(manifest, prepared, contentHash) {
6775
7358
  return manifest.contentHash === contentHash && manifest.extractionHash === (prepared.extractionHash ?? buildExtractionHash(prepared.extractedText, prepared.extractionArtifact)) && manifest.semanticHash === (prepared.semanticHash ?? contentHash) && manifest.title === prepared.title && manifest.sourceKind === prepared.sourceKind && manifest.sourceType === prepared.sourceType && manifest.sourceClass === prepared.sourceClass && manifest.language === prepared.language && manifest.mimeType === prepared.mimeType && manifest.repoRelativePath === prepared.repoRelativePath && manifest.sourceGroupId === prepared.sourceGroupId && manifest.sourceGroupTitle === prepared.sourceGroupTitle && manifest.sourcePartKey === prepared.sourcePartKey && manifest.partIndex === prepared.partIndex && manifest.partCount === prepared.partCount && manifest.partTitle === prepared.partTitle && JSON.stringify(manifest.details ?? {}) === JSON.stringify(prepared.details ?? {});
6776
7359
  }
6777
7360
  function shouldDeferWatchSemanticRefresh(sourceKind) {
6778
- return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "docx" || sourceKind === "epub" || sourceKind === "csv" || sourceKind === "xlsx" || sourceKind === "pptx" || sourceKind === "image";
7361
+ return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "docx" || sourceKind === "epub" || sourceKind === "csv" || sourceKind === "xlsx" || sourceKind === "pptx" || sourceKind === "transcript" || sourceKind === "chat_export" || sourceKind === "email" || sourceKind === "calendar" || sourceKind === "image";
6779
7362
  }
6780
7363
  function pendingSemanticRefreshId(changeType, repoRoot, relativePath) {
6781
7364
  return `pending:${changeType}:${sha256(`${toPosix(repoRoot)}:${relativePath}`).slice(0, 12)}`;
@@ -7043,6 +7626,23 @@ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
7043
7626
  }
7044
7627
  async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass) {
7045
7628
  const payloadBytes = await fs11.readFile(absoluteInput);
7629
+ if (path12.extname(absoluteInput).toLowerCase() === ".zip" && isSlackExportArchive(payloadBytes)) {
7630
+ const slackExport = await extractSlackExportArchive({ mimeType: "application/zip", bytes: payloadBytes, fileName: absoluteInput });
7631
+ if (slackExport.conversations.length) {
7632
+ return groupedPreparedInputsFor({
7633
+ title: slackExport.title?.trim() || path12.basename(absoluteInput, path12.extname(absoluteInput)),
7634
+ originType: "file",
7635
+ sourceKind: "chat_export",
7636
+ sourceClass,
7637
+ originalPath: toPosix(absoluteInput),
7638
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7639
+ mimeType: "application/zip",
7640
+ storedExtension: ".md",
7641
+ warnings: slackExport.warnings,
7642
+ parts: slackExport.conversations
7643
+ });
7644
+ }
7645
+ }
7046
7646
  const mimeType = guessMimeType(absoluteInput);
7047
7647
  const sourceKind = inferKind(mimeType, absoluteInput);
7048
7648
  const language = inferCodeLanguage(absoluteInput, mimeType);
@@ -7072,6 +7672,68 @@ async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass)
7072
7672
  title = extracted.artifact.metadata?.title?.trim() || title;
7073
7673
  extractedText = extracted.extractedText;
7074
7674
  extractionArtifact = extracted.artifact;
7675
+ } else if (sourceKind === "transcript") {
7676
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7677
+ const extracted = await extractTranscriptText({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7678
+ title = extracted.title?.trim() || title;
7679
+ extractedText = extracted.extractedText;
7680
+ extractionArtifact = extracted.artifact;
7681
+ } else if (sourceKind === "email" && path12.extname(absoluteInput).toLowerCase() === ".eml") {
7682
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7683
+ const extracted = await extractEmailText({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7684
+ title = extracted.title?.trim() || title;
7685
+ extractedText = extracted.extractedText;
7686
+ extractionArtifact = extracted.artifact;
7687
+ } else if (sourceKind === "email" && path12.extname(absoluteInput).toLowerCase() === ".mbox") {
7688
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7689
+ const extracted = await extractMboxMessages({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7690
+ title = extracted.title?.trim() || title;
7691
+ if (extracted.messages.length) {
7692
+ return groupedPreparedInputsFor({
7693
+ title,
7694
+ originType: "file",
7695
+ sourceKind: "email",
7696
+ sourceClass,
7697
+ originalPath: toPosix(absoluteInput),
7698
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7699
+ mimeType,
7700
+ storedExtension: ".md",
7701
+ warnings: extracted.warnings,
7702
+ parts: extracted.messages
7703
+ });
7704
+ }
7705
+ extractionArtifact = {
7706
+ extractor: "email_text",
7707
+ sourceKind: "email",
7708
+ mimeType,
7709
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7710
+ warnings: extracted.warnings ?? ["Mailbox extraction completed but produced no readable messages."]
7711
+ };
7712
+ } else if (sourceKind === "calendar") {
7713
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7714
+ const extracted = await extractCalendarEvents({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7715
+ title = extracted.title?.trim() || title;
7716
+ if (extracted.events.length) {
7717
+ return groupedPreparedInputsFor({
7718
+ title,
7719
+ originType: "file",
7720
+ sourceKind: "calendar",
7721
+ sourceClass,
7722
+ originalPath: toPosix(absoluteInput),
7723
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7724
+ mimeType,
7725
+ storedExtension: ".md",
7726
+ warnings: extracted.warnings,
7727
+ parts: extracted.events
7728
+ });
7729
+ }
7730
+ extractionArtifact = {
7731
+ extractor: "calendar_text",
7732
+ sourceKind: "calendar",
7733
+ mimeType,
7734
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7735
+ warnings: extracted.warnings ?? ["Calendar extraction completed but found no events."]
7736
+ };
7075
7737
  } else if (sourceKind === "csv") {
7076
7738
  title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7077
7739
  const extracted = await extractCsvText({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
@@ -7094,63 +7756,25 @@ async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass)
7094
7756
  title = path12.basename(absoluteInput, path12.extname(absoluteInput));
7095
7757
  const extracted = await extractEpubChapters({ mimeType, bytes: payloadBytes, fileName: absoluteInput });
7096
7758
  title = extracted.title?.trim() || title;
7097
- const groupId = sourceGroupIdFor({
7098
- title,
7099
- originType: "file",
7100
- originalPath: toPosix(absoluteInput)
7101
- });
7102
7759
  if (extracted.chapters.length) {
7103
- return extracted.chapters.map(
7104
- (chapter, index) => finalizePreparedInput({
7105
- title: `${title} - ${chapter.title}`,
7106
- originType: "file",
7107
- sourceKind: "epub",
7108
- sourceClass,
7109
- originalPath: toPosix(absoluteInput),
7110
- repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7111
- mimeType: "text/markdown",
7112
- storedExtension: ".md",
7113
- payloadBytes: Buffer.from(chapter.markdown, "utf8"),
7114
- extractedText: chapter.markdown,
7115
- extractionArtifact: {
7116
- extractor: "epub_text",
7117
- sourceKind: "epub",
7118
- mimeType,
7119
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7120
- metadata: {
7121
- ...chapter.metadata,
7122
- chapter_index: String(index + 1),
7123
- chapter_count: String(extracted.chapters.length)
7124
- },
7125
- warnings: extracted.warnings
7126
- },
7127
- extractionHash: buildExtractionHash(chapter.markdown, {
7128
- extractor: "epub_text",
7129
- sourceKind: "epub",
7130
- mimeType,
7131
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7132
- metadata: {
7133
- ...chapter.metadata,
7134
- chapter_index: String(index + 1),
7135
- chapter_count: String(extracted.chapters.length)
7136
- },
7137
- warnings: extracted.warnings
7138
- }),
7139
- sourceGroupId: groupId,
7140
- sourceGroupTitle: title,
7141
- sourcePartKey: chapter.partKey,
7142
- partIndex: index + 1,
7143
- partCount: extracted.chapters.length,
7144
- partTitle: chapter.title,
7145
- details: {
7146
- book_title: title,
7147
- chapter_title: chapter.title,
7148
- chapter_index: String(index + 1),
7149
- chapter_count: String(extracted.chapters.length),
7760
+ return groupedPreparedInputsFor({
7761
+ title,
7762
+ originType: "file",
7763
+ sourceKind: "epub",
7764
+ sourceClass,
7765
+ originalPath: toPosix(absoluteInput),
7766
+ repoRelativePath: repoRelativePathFor(absoluteInput, repoRoot),
7767
+ mimeType,
7768
+ storedExtension: ".md",
7769
+ warnings: extracted.warnings,
7770
+ parts: extracted.chapters.map((chapter) => ({
7771
+ ...chapter,
7772
+ metadata: {
7773
+ ...chapter.metadata,
7150
7774
  ...extracted.author ? { author: extracted.author } : {}
7151
7775
  }
7152
- })
7153
- );
7776
+ }))
7777
+ });
7154
7778
  }
7155
7779
  extractedText = void 0;
7156
7780
  extractionArtifact = {
@@ -7208,6 +7832,25 @@ async function prepareUrlInputs(rootDir, input, options) {
7208
7832
  const finalUrl = normalizeOriginUrl(response.url || input);
7209
7833
  const inputUrl = new URL(finalUrl);
7210
7834
  const originalPayloadBytes = Buffer.from(await response.arrayBuffer());
7835
+ if (path12.extname(inputUrl.pathname).toLowerCase() === ".zip" && isSlackExportArchive(originalPayloadBytes)) {
7836
+ const slackExport = await extractSlackExportArchive({
7837
+ mimeType: "application/zip",
7838
+ bytes: originalPayloadBytes,
7839
+ fileName: inputUrl.pathname
7840
+ });
7841
+ if (slackExport.conversations.length) {
7842
+ return groupedPreparedInputsFor({
7843
+ title: slackExport.title?.trim() || inputUrl.hostname,
7844
+ originType: "url",
7845
+ sourceKind: "chat_export",
7846
+ url: finalUrl,
7847
+ mimeType: "application/zip",
7848
+ storedExtension: ".md",
7849
+ warnings: slackExport.warnings,
7850
+ parts: slackExport.conversations
7851
+ });
7852
+ }
7853
+ }
7211
7854
  let payloadBytes = originalPayloadBytes;
7212
7855
  let mimeType = resolveUrlMimeType(input, response);
7213
7856
  let sourceKind = inferKind(mimeType, inputUrl.pathname);
@@ -7294,6 +7937,60 @@ async function prepareUrlInputs(rootDir, input, options) {
7294
7937
  title = extracted.artifact.metadata?.title?.trim() || title;
7295
7938
  extractedText = extracted.extractedText;
7296
7939
  extractionArtifact = extracted.artifact;
7940
+ } else if (sourceKind === "transcript") {
7941
+ const extracted = await extractTranscriptText({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7942
+ title = extracted.title?.trim() || title;
7943
+ extractedText = extracted.extractedText;
7944
+ extractionArtifact = extracted.artifact;
7945
+ } else if (sourceKind === "email" && path12.extname(inputUrl.pathname).toLowerCase() === ".eml") {
7946
+ const extracted = await extractEmailText({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7947
+ title = extracted.title?.trim() || title;
7948
+ extractedText = extracted.extractedText;
7949
+ extractionArtifact = extracted.artifact;
7950
+ } else if (sourceKind === "email" && path12.extname(inputUrl.pathname).toLowerCase() === ".mbox") {
7951
+ const extracted = await extractMboxMessages({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7952
+ title = extracted.title?.trim() || title;
7953
+ if (extracted.messages.length) {
7954
+ return groupedPreparedInputsFor({
7955
+ title,
7956
+ originType: "url",
7957
+ sourceKind: "email",
7958
+ url: finalUrl,
7959
+ mimeType,
7960
+ storedExtension: ".md",
7961
+ warnings: extracted.warnings,
7962
+ parts: extracted.messages
7963
+ });
7964
+ }
7965
+ extractionArtifact = {
7966
+ extractor: "email_text",
7967
+ sourceKind: "email",
7968
+ mimeType,
7969
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7970
+ warnings: extracted.warnings ?? ["Mailbox extraction completed but produced no readable messages."]
7971
+ };
7972
+ } else if (sourceKind === "calendar") {
7973
+ const extracted = await extractCalendarEvents({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7974
+ title = extracted.title?.trim() || title;
7975
+ if (extracted.events.length) {
7976
+ return groupedPreparedInputsFor({
7977
+ title,
7978
+ originType: "url",
7979
+ sourceKind: "calendar",
7980
+ url: finalUrl,
7981
+ mimeType,
7982
+ storedExtension: ".md",
7983
+ warnings: extracted.warnings,
7984
+ parts: extracted.events
7985
+ });
7986
+ }
7987
+ extractionArtifact = {
7988
+ extractor: "calendar_text",
7989
+ sourceKind: "calendar",
7990
+ mimeType,
7991
+ producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7992
+ warnings: extracted.warnings ?? ["Calendar extraction completed but found no events."]
7993
+ };
7297
7994
  } else if (sourceKind === "csv") {
7298
7995
  const extracted = await extractCsvText({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7299
7996
  title = extracted.title?.trim() || title;
@@ -7312,62 +8009,24 @@ async function prepareUrlInputs(rootDir, input, options) {
7312
8009
  } else if (sourceKind === "epub") {
7313
8010
  const extracted = await extractEpubChapters({ mimeType, bytes: payloadBytes, fileName: inputUrl.pathname });
7314
8011
  title = extracted.title?.trim() || title;
7315
- const groupId = sourceGroupIdFor({
7316
- title,
7317
- originType: "url",
7318
- url: finalUrl
7319
- });
7320
8012
  if (extracted.chapters.length) {
7321
- return extracted.chapters.map(
7322
- (chapter, index) => finalizePreparedInput({
7323
- title: `${title} - ${chapter.title}`,
7324
- originType: "url",
7325
- sourceKind: "epub",
7326
- url: finalUrl,
7327
- mimeType: "text/markdown",
7328
- storedExtension: ".md",
7329
- payloadBytes: Buffer.from(chapter.markdown, "utf8"),
7330
- extractedText: chapter.markdown,
7331
- extractionArtifact: {
7332
- extractor: "epub_text",
7333
- sourceKind: "epub",
7334
- mimeType,
7335
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7336
- metadata: {
7337
- ...chapter.metadata,
7338
- chapter_index: String(index + 1),
7339
- chapter_count: String(extracted.chapters.length)
7340
- },
7341
- warnings: extracted.warnings
7342
- },
7343
- extractionHash: buildExtractionHash(chapter.markdown, {
7344
- extractor: "epub_text",
7345
- sourceKind: "epub",
7346
- mimeType,
7347
- producedAt: (/* @__PURE__ */ new Date()).toISOString(),
7348
- metadata: {
7349
- ...chapter.metadata,
7350
- chapter_index: String(index + 1),
7351
- chapter_count: String(extracted.chapters.length)
7352
- },
7353
- warnings: extracted.warnings
7354
- }),
7355
- sourceGroupId: groupId,
7356
- sourceGroupTitle: title,
7357
- sourcePartKey: chapter.partKey,
7358
- partIndex: index + 1,
7359
- partCount: extracted.chapters.length,
7360
- partTitle: chapter.title,
7361
- details: {
7362
- book_title: title,
7363
- chapter_title: chapter.title,
7364
- chapter_index: String(index + 1),
7365
- chapter_count: String(extracted.chapters.length),
8013
+ return groupedPreparedInputsFor({
8014
+ title,
8015
+ originType: "url",
8016
+ sourceKind: "epub",
8017
+ url: finalUrl,
8018
+ mimeType,
8019
+ storedExtension: ".md",
8020
+ warnings: extracted.warnings,
8021
+ parts: extracted.chapters.map((chapter) => ({
8022
+ ...chapter,
8023
+ metadata: {
8024
+ ...chapter.metadata,
7366
8025
  ...extracted.author ? { author: extracted.author } : {}
7367
- },
7368
- logDetails
7369
- })
7370
- );
8026
+ }
8027
+ })),
8028
+ logDetails
8029
+ });
7371
8030
  }
7372
8031
  extractionArtifact = {
7373
8032
  extractor: "epub_text",
@@ -7544,13 +8203,28 @@ async function prepareInboxHtmlInput(absolutePath, attachmentRefs) {
7544
8203
  };
7545
8204
  }
7546
8205
  function isSupportedInboxKind(sourceKind) {
7547
- return ["markdown", "text", "html", "pdf", "docx", "epub", "csv", "xlsx", "pptx", "image"].includes(sourceKind);
8206
+ return [
8207
+ "markdown",
8208
+ "text",
8209
+ "html",
8210
+ "pdf",
8211
+ "docx",
8212
+ "epub",
8213
+ "csv",
8214
+ "xlsx",
8215
+ "pptx",
8216
+ "transcript",
8217
+ "chat_export",
8218
+ "email",
8219
+ "calendar",
8220
+ "image"
8221
+ ].includes(sourceKind);
7548
8222
  }
7549
8223
  async function ingestInputDetailed(rootDir, input, options) {
7550
8224
  const { paths } = await initWorkspace(rootDir);
7551
8225
  const normalizedOptions = normalizeIngestOptions(options);
7552
8226
  const absoluteInput = path12.resolve(rootDir, input);
7553
- const repoRoot = isHttpUrl(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await findNearestGitRoot2(absoluteInput).then((value) => value ?? path12.dirname(absoluteInput));
8227
+ const repoRoot = isHttpUrl(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await detectScopedRepoRoot(rootDir, absoluteInput, path12.dirname(absoluteInput));
7554
8228
  const prepared = isHttpUrl(input) ? await prepareUrlInputs(rootDir, input, normalizedOptions) : await prepareFileInputs(rootDir, absoluteInput, repoRoot);
7555
8229
  return await persistPreparedInputs(rootDir, input, prepared, paths);
7556
8230
  }
@@ -7648,10 +8322,39 @@ async function ingestDirectory(rootDir, inputDir, options) {
7648
8322
  const { paths } = await initWorkspace(rootDir);
7649
8323
  const normalizedOptions = await resolveRepoIngestOptions(rootDir, options);
7650
8324
  const absoluteInputDir = path12.resolve(rootDir, inputDir);
7651
- const repoRoot = normalizedOptions.repoRoot ?? await findNearestGitRoot2(absoluteInputDir) ?? absoluteInputDir;
8325
+ const repoRoot = normalizedOptions.repoRoot ?? await detectScopedRepoRoot(rootDir, absoluteInputDir, absoluteInputDir);
7652
8326
  if (!await fileExists(absoluteInputDir)) {
7653
8327
  throw new Error(`Directory not found: ${absoluteInputDir}`);
7654
8328
  }
8329
+ if (await isSlackExportDirectory(absoluteInputDir)) {
8330
+ const extracted = await extractSlackExportDirectory(absoluteInputDir);
8331
+ const preparedInputs = groupedPreparedInputsFor({
8332
+ title: extracted.title?.trim() || path12.basename(absoluteInputDir),
8333
+ originType: "file",
8334
+ sourceKind: "chat_export",
8335
+ originalPath: toPosix(absoluteInputDir),
8336
+ mimeType: "application/json",
8337
+ storedExtension: ".md",
8338
+ warnings: extracted.warnings,
8339
+ parts: extracted.conversations
8340
+ });
8341
+ const result = await persistPreparedInputs(rootDir, absoluteInputDir, preparedInputs, paths);
8342
+ await appendLogEntry(rootDir, "ingest_directory", toPosix(path12.relative(rootDir, absoluteInputDir)) || ".", [
8343
+ `repo_root=${toPosix(path12.relative(rootDir, repoRoot)) || "."}`,
8344
+ `scanned=${preparedInputs.length}`,
8345
+ `imported=${result.created.length}`,
8346
+ `updated=${result.updated.length}`,
8347
+ `skipped=${result.skipped.length}`
8348
+ ]);
8349
+ return {
8350
+ inputDir: absoluteInputDir,
8351
+ repoRoot,
8352
+ scannedCount: preparedInputs.length,
8353
+ imported: result.created,
8354
+ updated: result.updated,
8355
+ skipped: result.skipped
8356
+ };
8357
+ }
7655
8358
  const { files, skipped } = await collectDirectoryFiles(rootDir, absoluteInputDir, repoRoot, normalizedOptions);
7656
8359
  const imported = [];
7657
8360
  const updated = [];
@@ -7716,7 +8419,13 @@ async function importInbox(rootDir, inputDir) {
7716
8419
  continue;
7717
8420
  }
7718
8421
  const mimeType = guessMimeType(absolutePath);
7719
- const sourceKind = inferKind(mimeType, absolutePath);
8422
+ let sourceKind = inferKind(mimeType, absolutePath);
8423
+ if (sourceKind === "binary" && path12.extname(absolutePath).toLowerCase() === ".zip") {
8424
+ const bytes = await fs11.readFile(absolutePath);
8425
+ if (isSlackExportArchive(bytes)) {
8426
+ sourceKind = "chat_export";
8427
+ }
8428
+ }
7720
8429
  if (!isSupportedInboxKind(sourceKind)) {
7721
8430
  skipped.push({ path: toPosix(path12.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
7722
8431
  continue;
@@ -9945,6 +10654,19 @@ function relatedOutputsSection(relatedOutputs) {
9945
10654
  }
9946
10655
  return ["## Related Outputs", "", ...relatedOutputs.map((page) => `- ${pageLink(page)}`), ""];
9947
10656
  }
10657
+ function detailValue(manifest, key) {
10658
+ const value = manifest.details?.[key];
10659
+ const normalized = typeof value === "string" ? value.trim() : "";
10660
+ return normalized || void 0;
10661
+ }
10662
+ function detailList(manifest, key) {
10663
+ const value = detailValue(manifest, key);
10664
+ if (!value) {
10665
+ return void 0;
10666
+ }
10667
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
10668
+ return items.length ? items : void 0;
10669
+ }
9948
10670
  function buildSourcePage(manifest, analysis, schemaHash, metadata, relatedOutputs = [], modulePage, decorations) {
9949
10671
  const relativePath = pagePathFor("source", manifest.sourceId);
9950
10672
  const pageId = `source:${manifest.sourceId}`;
@@ -9968,6 +10690,10 @@ function buildSourcePage(manifest, analysis, schemaHash, metadata, relatedOutput
9968
10690
  title: analysis.title,
9969
10691
  ...manifest.sourceType ? { source_type: manifest.sourceType } : {},
9970
10692
  ...manifest.sourceClass ? { source_class: manifest.sourceClass } : {},
10693
+ ...detailValue(manifest, "occurred_at") ? { occurred_at: detailValue(manifest, "occurred_at") } : {},
10694
+ ...detailList(manifest, "participants") ? { participants: detailList(manifest, "participants") } : {},
10695
+ ...detailValue(manifest, "container_title") ? { container_title: detailValue(manifest, "container_title") } : {},
10696
+ ...detailValue(manifest, "conversation_id") ? { conversation_id: detailValue(manifest, "conversation_id") } : {},
9971
10697
  tags: decoratedTags(analysis.code ? ["source", "code"] : ["source"], decorations),
9972
10698
  source_ids: [manifest.sourceId],
9973
10699
  project_ids: decorations?.projectIds ?? [],
@@ -10300,6 +11026,9 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
10300
11026
  const outputs = pages.filter((page) => page.kind === "output");
10301
11027
  const insights = pages.filter((page) => page.kind === "insight");
10302
11028
  const graphPages = pages.filter((page) => page.kind === "graph_report" || page.kind === "community_summary");
11029
+ const dashboards = pages.filter(
11030
+ (page) => page.kind === "index" && page.path.startsWith("dashboards/") && page.path !== "dashboards/index.md"
11031
+ );
10303
11032
  return [
10304
11033
  "---",
10305
11034
  "page_id: index",
@@ -10345,6 +11074,10 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
10345
11074
  "",
10346
11075
  ...outputs.length ? outputs.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No saved outputs yet."],
10347
11076
  "",
11077
+ "## Dashboards",
11078
+ "",
11079
+ ...dashboards.length ? dashboards.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No dashboards yet."],
11080
+ "",
10348
11081
  "## Graph",
10349
11082
  "",
10350
11083
  ...graphPages.length ? graphPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No graph reports yet."],
@@ -11821,15 +12554,37 @@ async function rebuildSearchIndex(dbPath, pages, wikiDir) {
11821
12554
  const insertPage = db.prepare(
11822
12555
  "INSERT INTO pages (id, path, title, body, kind, status, source_type, source_class, project_ids, project_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
11823
12556
  );
12557
+ const rootDir = path21.dirname(wikiDir);
11824
12558
  for (const page of pages) {
11825
12559
  const absolutePath = path21.join(wikiDir, page.path);
11826
12560
  const content = await fs17.readFile(absolutePath, "utf8");
11827
12561
  const parsed = matter8(content);
12562
+ let body = parsed.content;
12563
+ const primarySourceId = Array.isArray(parsed.data.source_ids) && typeof parsed.data.source_ids[0] === "string" ? parsed.data.source_ids[0] : page.sourceIds[0];
12564
+ if ((page.kind === "source" || page.kind === "module") && primarySourceId) {
12565
+ try {
12566
+ const manifest = JSON.parse(
12567
+ await fs17.readFile(path21.join(rootDir, "state", "manifests", `${primarySourceId}.json`), "utf8")
12568
+ );
12569
+ const excerptPath = manifest.extractedTextPath ?? manifest.storedPath;
12570
+ if (excerptPath) {
12571
+ const excerpt = await fs17.readFile(path21.join(rootDir, excerptPath), "utf8");
12572
+ if (excerpt.trim()) {
12573
+ body = `${body}
12574
+
12575
+ ## Source Excerpt
12576
+
12577
+ ${excerpt.trim()}`.trim();
12578
+ }
12579
+ }
12580
+ } catch {
12581
+ }
12582
+ }
11828
12583
  insertPage.run(
11829
12584
  page.id,
11830
12585
  page.path,
11831
12586
  page.title,
11832
- parsed.content,
12587
+ body,
11833
12588
  page.kind,
11834
12589
  page.status,
11835
12590
  typeof parsed.data.source_type === "string" ? parsed.data.source_type : "",
@@ -11890,7 +12645,25 @@ function searchPages(dbPath, query, limitOrOptions = 5) {
11890
12645
  FROM page_search
11891
12646
  JOIN pages ON pages.rowid = page_search.rowid
11892
12647
  WHERE ${clauses.join(" AND ")}
11893
- ORDER BY rank
12648
+ ORDER BY
12649
+ CASE pages.status
12650
+ WHEN 'active' THEN 0
12651
+ WHEN 'draft' THEN 1
12652
+ WHEN 'candidate' THEN 2
12653
+ ELSE 3
12654
+ END,
12655
+ CASE pages.kind
12656
+ WHEN 'source' THEN 0
12657
+ WHEN 'module' THEN 1
12658
+ WHEN 'output' THEN 2
12659
+ WHEN 'insight' THEN 3
12660
+ WHEN 'graph_report' THEN 4
12661
+ WHEN 'community_summary' THEN 5
12662
+ WHEN 'concept' THEN 6
12663
+ WHEN 'entity' THEN 7
12664
+ ELSE 8
12665
+ END,
12666
+ rank
11894
12667
  LIMIT ?
11895
12668
  `);
11896
12669
  params.push(options.limit ?? 5);
@@ -12587,6 +13360,267 @@ async function buildManagedContent(absolutePath, defaults, build) {
12587
13360
  }
12588
13361
  return content;
12589
13362
  }
13363
+ function manifestDetailValue(manifest, key) {
13364
+ const value = manifest.details?.[key];
13365
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
13366
+ }
13367
+ async function loadAnalysesBySourceIds(paths, sourceIds) {
13368
+ const analyses = await Promise.all(
13369
+ sourceIds.map(async (sourceId) => await readJsonFile(path22.join(paths.analysesDir, `${sourceId}.json`)))
13370
+ );
13371
+ return analyses.filter((analysis) => Boolean(analysis?.sourceId));
13372
+ }
13373
+ async function buildDashboardRecords(paths, graph, schemaHash, report) {
13374
+ const sourcePages = graph.pages.filter((page) => page.kind === "source");
13375
+ const reviewPages = graph.pages.filter((page) => page.kind === "output" && page.path.startsWith("outputs/source-reviews/"));
13376
+ const briefPages = graph.pages.filter((page) => page.kind === "output" && page.path.startsWith("outputs/source-briefs/"));
13377
+ const manifests = graph.sources;
13378
+ const manifestBySourceId = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
13379
+ const timelineManifests = manifests.filter((manifest) => manifestDetailValue(manifest, "occurred_at")).sort((left, right) => (manifestDetailValue(right, "occurred_at") ?? "").localeCompare(manifestDetailValue(left, "occurred_at") ?? "")).slice(0, 25);
13380
+ const recentSourcePages = [...sourcePages].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 20);
13381
+ const analyses = await loadAnalysesBySourceIds(paths, uniqueStrings3(sourcePages.flatMap((page) => page.sourceIds)));
13382
+ const openQuestions = uniqueStrings3(
13383
+ analyses.flatMap((analysis) => analysis.questions.map((question) => `${analysis.title}: ${question}`))
13384
+ ).slice(0, 20);
13385
+ const dashboards = [
13386
+ {
13387
+ relativePath: "dashboards/index.md",
13388
+ title: "Dashboards",
13389
+ content: (metadata) => matter9.stringify(
13390
+ [
13391
+ "# Dashboards",
13392
+ "",
13393
+ "- [[dashboards/recent-sources|Recent Sources]]",
13394
+ "- [[dashboards/timeline|Timeline]]",
13395
+ "- [[dashboards/contradictions|Contradictions]]",
13396
+ "- [[dashboards/open-questions|Open Questions]]",
13397
+ "",
13398
+ "```dataview",
13399
+ "TABLE file.mtime AS updated",
13400
+ 'FROM "dashboards"',
13401
+ 'WHERE file.name != "index"',
13402
+ "SORT file.mtime desc",
13403
+ "```",
13404
+ ""
13405
+ ].join("\n"),
13406
+ {
13407
+ page_id: "dashboards:index",
13408
+ kind: "index",
13409
+ title: "Dashboards",
13410
+ tags: ["index", "dashboards"],
13411
+ source_ids: [],
13412
+ project_ids: [],
13413
+ node_ids: [],
13414
+ freshness: "fresh",
13415
+ status: metadata.status,
13416
+ confidence: 1,
13417
+ created_at: metadata.createdAt,
13418
+ updated_at: metadata.updatedAt,
13419
+ compiled_from: metadata.compiledFrom,
13420
+ managed_by: metadata.managedBy,
13421
+ backlinks: [],
13422
+ schema_hash: schemaHash,
13423
+ source_hashes: {},
13424
+ source_semantic_hashes: {}
13425
+ }
13426
+ )
13427
+ },
13428
+ {
13429
+ relativePath: "dashboards/recent-sources.md",
13430
+ title: "Recent Sources",
13431
+ content: (metadata) => matter9.stringify(
13432
+ [
13433
+ "# Recent Sources",
13434
+ "",
13435
+ ...recentSourcePages.length ? recentSourcePages.map((page) => `- ${page.updatedAt}: [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No source pages yet."],
13436
+ "",
13437
+ "```dataview",
13438
+ "TABLE source_type, occurred_at, participants",
13439
+ 'FROM "sources"',
13440
+ "SORT updated_at desc",
13441
+ "LIMIT 25",
13442
+ "```",
13443
+ ""
13444
+ ].join("\n"),
13445
+ {
13446
+ page_id: "dashboards:recent-sources",
13447
+ kind: "index",
13448
+ title: "Recent Sources",
13449
+ tags: ["index", "dashboard", "recent-sources"],
13450
+ source_ids: recentSourcePages.flatMap((page) => page.sourceIds),
13451
+ project_ids: [],
13452
+ node_ids: [],
13453
+ freshness: "fresh",
13454
+ status: metadata.status,
13455
+ confidence: 1,
13456
+ created_at: metadata.createdAt,
13457
+ updated_at: metadata.updatedAt,
13458
+ compiled_from: recentSourcePages.flatMap((page) => page.sourceIds),
13459
+ managed_by: metadata.managedBy,
13460
+ backlinks: [],
13461
+ schema_hash: schemaHash,
13462
+ source_hashes: {},
13463
+ source_semantic_hashes: {}
13464
+ }
13465
+ )
13466
+ },
13467
+ {
13468
+ relativePath: "dashboards/timeline.md",
13469
+ title: "Timeline",
13470
+ content: (metadata) => matter9.stringify(
13471
+ [
13472
+ "# Timeline",
13473
+ "",
13474
+ ...timelineManifests.length ? timelineManifests.map((manifest) => {
13475
+ const occurredAt = manifestDetailValue(manifest, "occurred_at") ?? manifest.updatedAt;
13476
+ const sourcePage = sourcePages.find((page) => page.sourceIds.includes(manifest.sourceId));
13477
+ return `- ${occurredAt}: ${sourcePage ? `[[${sourcePage.path.replace(/\.md$/, "")}|${sourcePage.title}]]` : manifest.title}`;
13478
+ }) : ["- No timeline-aware sources yet."],
13479
+ "",
13480
+ "```dataview",
13481
+ "TABLE occurred_at, participants, container_title",
13482
+ 'FROM "sources"',
13483
+ "WHERE occurred_at",
13484
+ "SORT occurred_at desc",
13485
+ "```",
13486
+ ""
13487
+ ].join("\n"),
13488
+ {
13489
+ page_id: "dashboards:timeline",
13490
+ kind: "index",
13491
+ title: "Timeline",
13492
+ tags: ["index", "dashboard", "timeline"],
13493
+ source_ids: timelineManifests.map((manifest) => manifest.sourceId),
13494
+ project_ids: [],
13495
+ node_ids: [],
13496
+ freshness: "fresh",
13497
+ status: metadata.status,
13498
+ confidence: 1,
13499
+ created_at: metadata.createdAt,
13500
+ updated_at: metadata.updatedAt,
13501
+ compiled_from: timelineManifests.map((manifest) => manifest.sourceId),
13502
+ managed_by: metadata.managedBy,
13503
+ backlinks: [],
13504
+ schema_hash: schemaHash,
13505
+ source_hashes: {},
13506
+ source_semantic_hashes: {}
13507
+ }
13508
+ )
13509
+ },
13510
+ {
13511
+ relativePath: "dashboards/contradictions.md",
13512
+ title: "Contradictions",
13513
+ content: (metadata) => matter9.stringify(
13514
+ [
13515
+ "# Contradictions",
13516
+ "",
13517
+ ...report?.contradictions.length ? report.contradictions.map((contradiction) => {
13518
+ const left = manifestBySourceId.get(contradiction.sourceIdA)?.title ?? contradiction.sourceIdA;
13519
+ const right = manifestBySourceId.get(contradiction.sourceIdB)?.title ?? contradiction.sourceIdB;
13520
+ return `- ${left} / ${right}: ${contradiction.claimA} <> ${contradiction.claimB}`;
13521
+ }) : ["- No contradictions are currently flagged."],
13522
+ "",
13523
+ ...reviewPages.length || briefPages.length ? [
13524
+ "## Related Reviews",
13525
+ "",
13526
+ ...[...reviewPages, ...briefPages].slice(0, 12).map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`),
13527
+ ""
13528
+ ] : [],
13529
+ "```dataview",
13530
+ 'LIST FROM "outputs/source-reviews"',
13531
+ "SORT file.mtime desc",
13532
+ "```",
13533
+ ""
13534
+ ].join("\n"),
13535
+ {
13536
+ page_id: "dashboards:contradictions",
13537
+ kind: "index",
13538
+ title: "Contradictions",
13539
+ tags: ["index", "dashboard", "contradictions"],
13540
+ source_ids: report?.contradictions.flatMap((item) => [item.sourceIdA, item.sourceIdB]) ?? [],
13541
+ project_ids: [],
13542
+ node_ids: [],
13543
+ freshness: "fresh",
13544
+ status: metadata.status,
13545
+ confidence: 1,
13546
+ created_at: metadata.createdAt,
13547
+ updated_at: metadata.updatedAt,
13548
+ compiled_from: report?.contradictions.flatMap((item) => [item.sourceIdA, item.sourceIdB]) ?? [],
13549
+ managed_by: metadata.managedBy,
13550
+ backlinks: [],
13551
+ schema_hash: schemaHash,
13552
+ source_hashes: {},
13553
+ source_semantic_hashes: {}
13554
+ }
13555
+ )
13556
+ },
13557
+ {
13558
+ relativePath: "dashboards/open-questions.md",
13559
+ title: "Open Questions",
13560
+ content: (metadata) => matter9.stringify(
13561
+ [
13562
+ "# Open Questions",
13563
+ "",
13564
+ ...openQuestions.length ? openQuestions.map((question) => `- ${question}`) : ["- No open questions are currently extracted."],
13565
+ "",
13566
+ "```dataview",
13567
+ 'LIST FROM "outputs/source-briefs" OR "outputs/source-reviews"',
13568
+ "SORT file.mtime desc",
13569
+ "```",
13570
+ ""
13571
+ ].join("\n"),
13572
+ {
13573
+ page_id: "dashboards:open-questions",
13574
+ kind: "index",
13575
+ title: "Open Questions",
13576
+ tags: ["index", "dashboard", "open-questions"],
13577
+ source_ids: analyses.map((analysis) => analysis.sourceId),
13578
+ project_ids: [],
13579
+ node_ids: [],
13580
+ freshness: "fresh",
13581
+ status: metadata.status,
13582
+ confidence: 1,
13583
+ created_at: metadata.createdAt,
13584
+ updated_at: metadata.updatedAt,
13585
+ compiled_from: analyses.map((analysis) => analysis.sourceId),
13586
+ managed_by: metadata.managedBy,
13587
+ backlinks: [],
13588
+ schema_hash: schemaHash,
13589
+ source_hashes: {},
13590
+ source_semantic_hashes: {}
13591
+ }
13592
+ )
13593
+ }
13594
+ ];
13595
+ const records = [];
13596
+ for (const dashboard of dashboards) {
13597
+ const absolutePath = path22.join(paths.wikiDir, dashboard.relativePath);
13598
+ const compiledFrom = dashboard.relativePath === "dashboards/recent-sources.md" ? recentSourcePages.flatMap((page) => page.sourceIds) : [];
13599
+ const content = await buildManagedContent(
13600
+ absolutePath,
13601
+ {
13602
+ managedBy: "system",
13603
+ compiledFrom
13604
+ },
13605
+ dashboard.content
13606
+ );
13607
+ records.push({
13608
+ page: emptyGraphPage({
13609
+ id: `dashboard:${dashboard.relativePath.replace(/\.md$/, "")}`,
13610
+ path: dashboard.relativePath,
13611
+ title: dashboard.title,
13612
+ kind: "index",
13613
+ sourceIds: compiledFrom,
13614
+ nodeIds: [],
13615
+ schemaHash,
13616
+ sourceHashes: {},
13617
+ confidence: 1
13618
+ }),
13619
+ content
13620
+ });
13621
+ }
13622
+ return records;
13623
+ }
12590
13624
  function indexCompiledFrom(pages) {
12591
13625
  return uniqueStrings3(pages.flatMap((page) => page.sourceIds));
12592
13626
  }
@@ -13612,8 +14646,19 @@ async function syncVaultArtifacts(rootDir, input) {
13612
14646
  input.previousState?.generatedAt,
13613
14647
  contradictions
13614
14648
  );
13615
- records.push(...graphOrientation.records);
13616
- const allPages = [...basePages, ...graphOrientation.records.map((record) => record.page)];
14649
+ const preliminaryPages = [...basePages, ...graphOrientation.records.map((record) => record.page)];
14650
+ const dashboardRecords = await buildDashboardRecords(
14651
+ paths,
14652
+ {
14653
+ ...baseGraph,
14654
+ sources: input.manifests,
14655
+ pages: preliminaryPages
14656
+ },
14657
+ globalSchemaHash,
14658
+ graphOrientation.report
14659
+ );
14660
+ records.push(...graphOrientation.records, ...dashboardRecords);
14661
+ const allPages = uniqueBy([...preliminaryPages, ...dashboardRecords.map((record) => record.page)], (page) => page.id);
13617
14662
  const graph = {
13618
14663
  ...baseGraph,
13619
14664
  pages: allPages
@@ -13717,6 +14762,11 @@ async function syncVaultArtifacts(rootDir, input) {
13717
14762
  ["concepts/index.md", "concepts", activeConceptPages],
13718
14763
  ["entities/index.md", "entities", activeEntityPages],
13719
14764
  ["outputs/index.md", "outputs", allPages.filter((page) => page.kind === "output")],
14765
+ [
14766
+ "dashboards/index.md",
14767
+ "dashboards",
14768
+ allPages.filter((page) => page.kind === "index" && page.path.startsWith("dashboards/") && page.path !== "dashboards/index.md")
14769
+ ],
13720
14770
  ["candidates/index.md", "candidates", candidatePages],
13721
14771
  ["graph/index.md", "graph", allPages.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
13722
14772
  ]) {
@@ -13817,17 +14867,40 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13817
14867
  const compileState = await readJsonFile(paths.compileStatePath);
13818
14868
  const globalSchemaHash = schemas.effective.global.hash;
13819
14869
  const currentGraph = await readJsonFile(paths.graphPath);
13820
- const basePages = pages.filter((page) => page.kind !== "graph_report" && page.kind !== "community_summary");
14870
+ const orientationPages = uniqueBy(
14871
+ pages.filter((page) => page.kind !== "graph_report" && page.kind !== "community_summary"),
14872
+ (page) => page.id
14873
+ );
14874
+ const basePages = uniqueBy(
14875
+ pages.filter(
14876
+ (page) => page.kind !== "graph_report" && page.kind !== "community_summary" && !(page.kind === "index" && page.path.startsWith("dashboards/"))
14877
+ ),
14878
+ (page) => page.id
14879
+ );
13821
14880
  const graphOrientation = currentGraph ? await buildGraphOrientationPages(
13822
14881
  {
13823
14882
  ...currentGraph,
13824
- pages: basePages
14883
+ pages: orientationPages
13825
14884
  },
13826
14885
  paths,
13827
14886
  globalSchemaHash,
13828
14887
  compileState?.generatedAt
13829
14888
  ) : { records: [], report: null };
13830
- const pagesWithGraph = sortGraphPages([...basePages, ...graphOrientation.records.map((record) => record.page)]);
14889
+ const dashboardRecords = currentGraph ? await buildDashboardRecords(
14890
+ paths,
14891
+ {
14892
+ ...currentGraph,
14893
+ pages: [...basePages, ...graphOrientation.records.map((record) => record.page)]
14894
+ },
14895
+ globalSchemaHash,
14896
+ graphOrientation.report
14897
+ ) : [];
14898
+ const pagesWithGraph = sortGraphPages(
14899
+ uniqueBy(
14900
+ [...basePages, ...graphOrientation.records.map((record) => record.page), ...dashboardRecords.map((record) => record.page)],
14901
+ (page) => page.id
14902
+ )
14903
+ );
13831
14904
  if (currentGraph) {
13832
14905
  await writeJsonFile(paths.graphPath, {
13833
14906
  ...currentGraph,
@@ -13855,6 +14928,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13855
14928
  ensureDir(path22.join(paths.wikiDir, "concepts")),
13856
14929
  ensureDir(path22.join(paths.wikiDir, "entities")),
13857
14930
  ensureDir(path22.join(paths.wikiDir, "outputs")),
14931
+ ensureDir(path22.join(paths.wikiDir, "dashboards")),
13858
14932
  ensureDir(path22.join(paths.wikiDir, "graph")),
13859
14933
  ensureDir(path22.join(paths.wikiDir, "graph", "communities")),
13860
14934
  ensureDir(path22.join(paths.wikiDir, "projects")),
@@ -13917,6 +14991,11 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13917
14991
  ["concepts/index.md", "concepts", pagesWithGraph.filter((page) => page.kind === "concept" && page.status !== "candidate")],
13918
14992
  ["entities/index.md", "entities", pagesWithGraph.filter((page) => page.kind === "entity" && page.status !== "candidate")],
13919
14993
  ["outputs/index.md", "outputs", pagesWithGraph.filter((page) => page.kind === "output")],
14994
+ [
14995
+ "dashboards/index.md",
14996
+ "dashboards",
14997
+ pagesWithGraph.filter((page) => page.kind === "index" && page.path.startsWith("dashboards/") && page.path !== "dashboards/index.md")
14998
+ ],
13920
14999
  ["candidates/index.md", "candidates", pagesWithGraph.filter((page) => page.status === "candidate")],
13921
15000
  ["graph/index.md", "graph", pagesWithGraph.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
13922
15001
  ]) {
@@ -13936,6 +15015,9 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13936
15015
  for (const record of graphOrientation.records) {
13937
15016
  await writeFileIfChanged(path22.join(paths.wikiDir, record.page.path), record.content);
13938
15017
  }
15018
+ for (const record of dashboardRecords) {
15019
+ await writeFileIfChanged(path22.join(paths.wikiDir, record.page.path), record.content);
15020
+ }
13939
15021
  if (graphOrientation.report) {
13940
15022
  await writeJsonFile(path22.join(paths.wikiDir, "graph", "report.json"), graphOrientation.report);
13941
15023
  }
@@ -13952,6 +15034,11 @@ async function refreshIndexesAndSearch(rootDir, pages) {
13952
15034
  await Promise.all(
13953
15035
  existingGraphPages.filter((relativePath) => !allowedGraphPages.has(relativePath)).map((relativePath) => fs18.rm(path22.join(paths.wikiDir, relativePath), { force: true }))
13954
15036
  );
15037
+ const existingDashboardPages = (await listFilesRecursive(path22.join(paths.wikiDir, "dashboards")).catch(() => [])).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path22.relative(paths.wikiDir, absolutePath)));
15038
+ const allowedDashboardPages = /* @__PURE__ */ new Set(["dashboards/index.md", ...dashboardRecords.map((record) => record.page.path)]);
15039
+ await Promise.all(
15040
+ existingDashboardPages.filter((relativePath) => !allowedDashboardPages.has(relativePath)).map((relativePath) => fs18.rm(path22.join(paths.wikiDir, relativePath), { force: true }))
15041
+ );
13955
15042
  await rebuildSearchIndex(paths.searchDbPath, pagesWithGraph, paths.wikiDir);
13956
15043
  }
13957
15044
  async function prepareOutputPageSave(rootDir, input) {
@@ -14087,6 +15174,9 @@ async function stageOutputApprovalBundle(rootDir, stagedPages) {
14087
15174
  });
14088
15175
  return { approvalId, approvalDir };
14089
15176
  }
15177
+ async function stageGeneratedOutputPages(rootDir, stagedPages) {
15178
+ return await stageOutputApprovalBundle(rootDir, stagedPages);
15179
+ }
14090
15180
  async function executeQuery(rootDir, question, format) {
14091
15181
  const { paths } = await loadVaultConfig(rootDir);
14092
15182
  const schemas = await loadVaultSchemas(rootDir);
@@ -15428,7 +16518,17 @@ async function benchmarkVault(rootDir, options = {}) {
15428
16518
  });
15429
16519
  await writeJsonFile(paths.benchmarkPath, artifact);
15430
16520
  await refreshIndexesAndSearch(rootDir, graph.pages);
15431
- return artifact;
16521
+ const refreshedGraph = await readJsonFile(paths.graphPath) ?? graph;
16522
+ const refreshedHash = graphHash(refreshedGraph);
16523
+ if (artifact.graphHash === refreshedHash) {
16524
+ return artifact;
16525
+ }
16526
+ const refreshedArtifact = {
16527
+ ...artifact,
16528
+ graphHash: refreshedHash
16529
+ };
16530
+ await writeJsonFile(paths.benchmarkPath, refreshedArtifact);
16531
+ return refreshedArtifact;
15432
16532
  }
15433
16533
  async function pathGraphVault(rootDir, from, to) {
15434
16534
  const graph = await ensureCompiledGraph(rootDir);
@@ -15648,7 +16748,7 @@ async function bootstrapDemo(rootDir, input) {
15648
16748
  }
15649
16749
 
15650
16750
  // src/mcp.ts
15651
- var SERVER_VERSION = "0.3.0";
16751
+ var SERVER_VERSION = "0.4.0";
15652
16752
  async function createMcpServer(rootDir) {
15653
16753
  const server = new McpServer({
15654
16754
  name: "swarmvault",
@@ -16504,7 +17604,7 @@ function matchesManagedSourceSpec(existing, input) {
16504
17604
  if (existing.kind !== input.kind) {
16505
17605
  return false;
16506
17606
  }
16507
- if (input.kind === "directory") {
17607
+ if (input.kind === "directory" || input.kind === "file") {
16508
17608
  return path25.resolve(existing.path ?? "") === path25.resolve(input.path);
16509
17609
  }
16510
17610
  return (existing.url ?? "") === input.url;
@@ -16516,10 +17616,15 @@ async function resolveManagedSourceInput(rootDir, input) {
16516
17616
  if (!stat) {
16517
17617
  throw new Error(`Source not found: ${input}`);
16518
17618
  }
17619
+ if (stat.isFile()) {
17620
+ return {
17621
+ kind: "file",
17622
+ path: absoluteInput,
17623
+ title: path25.basename(absoluteInput, path25.extname(absoluteInput)) || absoluteInput
17624
+ };
17625
+ }
16519
17626
  if (!stat.isDirectory()) {
16520
- throw new Error(
16521
- "`swarmvault source add` supports directories, public GitHub repo root URLs, and docs hubs. Use `swarmvault ingest` for single files."
16522
- );
17627
+ throw new Error("`swarmvault source add` supports local files, directories, public GitHub repo root URLs, and docs hubs.");
16523
17628
  }
16524
17629
  const detectedRepoRoot = await findNearestGitRoot3(absoluteInput);
16525
17630
  const repoRoot = detectedRepoRoot && !(withinRoot2(rootDir, absoluteInput) && !withinRoot2(rootDir, detectedRepoRoot)) ? detectedRepoRoot : absoluteInput;
@@ -16552,6 +17657,10 @@ async function resolveManagedSourceInput(rootDir, input) {
16552
17657
  function directorySourceIdsFor(manifests, inputPath) {
16553
17658
  return manifests.filter((manifest) => manifest.originalPath && withinRoot2(path25.resolve(inputPath), path25.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
16554
17659
  }
17660
+ function fileSourceIdsFor(manifests, inputPath) {
17661
+ const absoluteInput = path25.resolve(inputPath);
17662
+ return manifests.filter((manifest) => manifest.originalPath && path25.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
17663
+ }
16555
17664
  async function syncDirectorySource(rootDir, inputPath, repoRoot) {
16556
17665
  const manifestsBefore = await listManifests(rootDir);
16557
17666
  const previousInScope = manifestsBefore.filter(
@@ -16585,6 +17694,22 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
16585
17694
  changed: result.imported.length + result.updated.length + removed.length > 0
16586
17695
  };
16587
17696
  }
17697
+ async function syncFileSource(rootDir, inputPath) {
17698
+ const result = await ingestInputDetailed(rootDir, inputPath);
17699
+ const manifestsAfter = await listManifests(rootDir);
17700
+ return {
17701
+ title: path25.basename(inputPath, path25.extname(inputPath)) || inputPath,
17702
+ sourceIds: fileSourceIdsFor(manifestsAfter, inputPath),
17703
+ counts: {
17704
+ scannedCount: result.scannedCount,
17705
+ importedCount: result.created.length,
17706
+ updatedCount: result.updated.length,
17707
+ removedCount: result.removed.length,
17708
+ skippedCount: result.skipped.length
17709
+ },
17710
+ changed: result.created.length + result.updated.length + result.removed.length > 0
17711
+ };
17712
+ }
16588
17713
  async function runGitCommand(cwd, args) {
16589
17714
  await new Promise((resolve, reject) => {
16590
17715
  const child = spawn2("git", args, {
@@ -16679,6 +17804,22 @@ async function syncManagedSource(rootDir, entry, options) {
16679
17804
  };
16680
17805
  }
16681
17806
  sync = await syncDirectorySource(rootDir, entry.path, entry.repoRoot);
17807
+ } else if (entry.kind === "file") {
17808
+ if (!entry.path) {
17809
+ throw new Error(`Managed source ${entry.id} is missing its file path.`);
17810
+ }
17811
+ if (!await fileExists(entry.path)) {
17812
+ return {
17813
+ ...entry,
17814
+ status: "missing",
17815
+ updatedAt: now,
17816
+ lastSyncAt: now,
17817
+ lastSyncStatus: "error",
17818
+ lastError: `File not found: ${entry.path}`,
17819
+ changed: false
17820
+ };
17821
+ }
17822
+ sync = await syncFileSource(rootDir, entry.path);
16682
17823
  } else if (entry.kind === "github_repo") {
16683
17824
  sync = await syncGitHubRepoSource(rootDir, entry);
16684
17825
  } else {
@@ -16897,6 +18038,179 @@ async function generateBriefsForSources(rootDir, sources) {
16897
18038
  }
16898
18039
  return briefPaths;
16899
18040
  }
18041
+ function renderDeterministicSourceReview(input) {
18042
+ const canonicalPages = input.sourcePages.filter((page) => page.kind === "source" || page.kind === "concept" || page.kind === "entity").slice(0, 10);
18043
+ const modulePages = input.sourcePages.filter((page) => page.kind === "module").slice(0, 8);
18044
+ const questions = uniqueStrings4(input.analyses.flatMap((analysis) => analysis.questions)).slice(0, 8);
18045
+ const concepts = uniqueStrings4(input.analyses.flatMap((analysis) => analysis.concepts.map((concept) => concept.name))).slice(0, 8);
18046
+ const entities = uniqueStrings4(input.analyses.flatMap((analysis) => analysis.entities.map((entity) => entity.name))).slice(0, 8);
18047
+ const contradictions = input.report?.contradictions.filter(
18048
+ (contradiction) => input.scope.sourceIds.includes(contradiction.sourceIdA) || input.scope.sourceIds.includes(contradiction.sourceIdB)
18049
+ ) ?? [];
18050
+ return [
18051
+ `# Source Review: ${input.scope.title}`,
18052
+ "",
18053
+ "## What This Source Contains",
18054
+ "",
18055
+ ...input.analyses.length ? input.analyses.map((analysis) => `- ${analysis.title}: ${analysis.summary}`) : ["- This source has not been analyzed yet. Compile the vault before trusting downstream pages."],
18056
+ "",
18057
+ "## Likely Canonical Pages To Update",
18058
+ "",
18059
+ ...canonicalPages.length ? canonicalPages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`) : ["- No canonical source, concept, or entity pages are linked to this source yet."],
18060
+ "",
18061
+ "## Important Topics And Entities",
18062
+ "",
18063
+ ...concepts.length ? [`Concepts: ${concepts.join(", ")}`] : ["Concepts: none detected."],
18064
+ ...entities.length ? [`Entities: ${entities.join(", ")}`] : ["Entities: none detected."],
18065
+ ...modulePages.length ? ["", ...modulePages.map((page) => `- Module: [[${page.path.replace(/\.md$/, "")}|${page.title}]]`)] : [],
18066
+ "",
18067
+ "## Contradictions To Inspect",
18068
+ "",
18069
+ ...contradictions.length ? contradictions.map((contradiction) => `- ${contradiction.claimA} / ${contradiction.claimB}`) : ["- No contradictions are currently flagged for this source scope."],
18070
+ "",
18071
+ "## Open Questions",
18072
+ "",
18073
+ ...questions.length ? questions.map((question) => `- ${question}`) : ["- No extracted open questions yet."],
18074
+ "",
18075
+ "## Suggested Next Steps",
18076
+ "",
18077
+ ...canonicalPages.length ? canonicalPages.slice(0, 5).map((page) => `- Review [[${page.path.replace(/\.md$/, "")}|${page.title}]] for canonical updates.`) : ["- Review the source page and decide which canonical pages should exist."],
18078
+ ""
18079
+ ].join("\n");
18080
+ }
18081
+ async function generateSourceReviewMarkdown(rootDir, scope) {
18082
+ const { paths } = await loadVaultConfig(rootDir);
18083
+ let graph = await readJsonFile(paths.graphPath);
18084
+ if (!graph) {
18085
+ await compileVault(rootDir, {});
18086
+ graph = await readJsonFile(paths.graphPath);
18087
+ }
18088
+ if (!graph) {
18089
+ return null;
18090
+ }
18091
+ const sourcePages = scopedSourcePages(graph, scope.sourceIds);
18092
+ const analyses = await loadSourceAnalyses(rootDir, scope.sourceIds);
18093
+ const report = await readGraphReport(rootDir);
18094
+ const fallback = renderDeterministicSourceReview({
18095
+ scope,
18096
+ sourcePages,
18097
+ graph,
18098
+ analyses,
18099
+ report
18100
+ });
18101
+ const provider = await getProviderForTask(rootDir, "queryProvider");
18102
+ if (provider.type === "heuristic") {
18103
+ return fallback;
18104
+ }
18105
+ try {
18106
+ const schemas = await loadVaultSchemas(rootDir);
18107
+ const pageContext = sourcePages.slice(0, 12).map((page) => `- ${page.title} (${page.kind}) -> ${page.path}`).join("\n");
18108
+ const analysisContext = analyses.slice(0, 8).map(
18109
+ (analysis) => `# ${analysis.title}
18110
+ Summary: ${analysis.summary}
18111
+ Questions: ${analysis.questions.join(" | ") || "none"}
18112
+ Concepts: ${analysis.concepts.map((concept) => concept.name).join(", ") || "none"}
18113
+ Entities: ${analysis.entities.map((entity) => entity.name).join(", ") || "none"}`
18114
+ ).join("\n\n---\n\n");
18115
+ const response = await provider.generateText({
18116
+ system: buildSchemaPrompt(
18117
+ schemas.effective.global,
18118
+ "Write a concise markdown source review with sections: What This Source Contains, Likely Canonical Pages To Update, Important Topics And Entities, Contradictions To Inspect, Open Questions, Suggested Next Steps. Focus on helping a human decide what to keep, update, or question in the wiki."
18119
+ ),
18120
+ prompt: [
18121
+ `Source scope: ${scope.title}`,
18122
+ `Scope id: ${scope.id}`,
18123
+ `Tracked source ids: ${scope.sourceIds.join(", ") || "none"}`,
18124
+ "",
18125
+ "Pages:",
18126
+ pageContext || "- none",
18127
+ "",
18128
+ "Analyses:",
18129
+ analysisContext || "No analysis context available.",
18130
+ "",
18131
+ "Deterministic fallback draft:",
18132
+ fallback
18133
+ ].join("\n")
18134
+ });
18135
+ return response.text?.trim() ? response.text.trim() : fallback;
18136
+ } catch {
18137
+ return fallback;
18138
+ }
18139
+ }
18140
+ async function stageSourceReviewForScope(rootDir, scope) {
18141
+ const { paths } = await loadVaultConfig(rootDir);
18142
+ const markdown = await generateSourceReviewMarkdown(rootDir, scope);
18143
+ if (!markdown) {
18144
+ throw new Error(`Could not generate a source review for ${scope.id}.`);
18145
+ }
18146
+ const graph = await readJsonFile(paths.graphPath);
18147
+ const relatedPages = graph ? scopedSourcePages(graph, scope.sourceIds) : [];
18148
+ const relatedPageIds = relatedPages.slice(0, 16).map((page) => page.id);
18149
+ const relatedNodeIds = graph ? scopedNodeIds(graph, scope.sourceIds).slice(0, 24) : [];
18150
+ const projectIds = uniqueStrings4(relatedPages.flatMap((page) => page.projectIds));
18151
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18152
+ const output = buildOutputPage({
18153
+ title: `Source Review: ${scope.title}`,
18154
+ question: `Review ${scope.title}`,
18155
+ answer: markdown,
18156
+ citations: scope.sourceIds,
18157
+ schemaHash: graph?.generatedAt ?? "",
18158
+ outputFormat: "report",
18159
+ relatedPageIds,
18160
+ relatedNodeIds,
18161
+ relatedSourceIds: scope.sourceIds,
18162
+ projectIds,
18163
+ extraTags: ["source-review"],
18164
+ origin: "query",
18165
+ slug: `source-reviews/${scope.id}`,
18166
+ metadata: {
18167
+ status: "draft",
18168
+ createdAt: now,
18169
+ updatedAt: now,
18170
+ compiledFrom: scope.sourceIds,
18171
+ managedBy: "system",
18172
+ confidence: 0.79
18173
+ }
18174
+ });
18175
+ const approval = await stageGeneratedOutputPages(rootDir, [{ page: output.page, content: output.content }]);
18176
+ return {
18177
+ sourceId: scope.id,
18178
+ pageId: output.page.id,
18179
+ reviewPath: path25.join(approval.approvalDir, "wiki", output.page.path),
18180
+ staged: true,
18181
+ approvalId: approval.approvalId,
18182
+ approvalDir: approval.approvalDir
18183
+ };
18184
+ }
18185
+ function scopeFromManagedSource(source) {
18186
+ return {
18187
+ id: source.id,
18188
+ title: source.title,
18189
+ sourceIds: source.sourceIds
18190
+ };
18191
+ }
18192
+ async function reviewSourceScope(rootDir, scope) {
18193
+ return await stageSourceReviewForScope(rootDir, scope);
18194
+ }
18195
+ async function reviewManagedSource(rootDir, id) {
18196
+ const managedSources = await loadManagedSources(rootDir);
18197
+ const managedSource = managedSources.find((source) => source.id === id);
18198
+ if (managedSource) {
18199
+ if (!await loadVaultConfig(rootDir).then(({ paths }) => fileExists(paths.graphPath))) {
18200
+ await compileVault(rootDir, {});
18201
+ }
18202
+ return await stageSourceReviewForScope(rootDir, scopeFromManagedSource(managedSource));
18203
+ }
18204
+ const manifest = (await listManifests(rootDir)).find((candidate) => candidate.sourceId === id);
18205
+ if (!manifest) {
18206
+ throw new Error(`Managed source or source id not found: ${id}`);
18207
+ }
18208
+ return await stageSourceReviewForScope(rootDir, {
18209
+ id: manifest.sourceId,
18210
+ title: manifest.title,
18211
+ sourceIds: [manifest.sourceId]
18212
+ });
18213
+ }
16900
18214
  function shouldCompile(changedSources, graphExists, compileRequested) {
16901
18215
  return compileRequested && (!graphExists || changedSources.length > 0);
16902
18216
  }
@@ -16907,17 +18221,18 @@ async function listManagedSourceRecords(rootDir) {
16907
18221
  async function addManagedSource(rootDir, input, options = {}) {
16908
18222
  const compileRequested = options.compile ?? true;
16909
18223
  const briefRequested = options.brief ?? true;
18224
+ const reviewRequested = options.review ?? false;
16910
18225
  const sources = await loadManagedSources(rootDir);
16911
18226
  const resolved = await resolveManagedSourceInput(rootDir, input);
16912
18227
  const existing = sources.find((candidate) => matchesManagedSourceSpec(candidate, resolved));
16913
18228
  const now = (/* @__PURE__ */ new Date()).toISOString();
16914
18229
  const source = existing ?? {
16915
- id: resolved.kind === "directory" ? stableManagedSourceId("directory", path25.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
18230
+ id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path25.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
16916
18231
  kind: resolved.kind,
16917
18232
  title: resolved.title,
16918
- path: resolved.kind === "directory" ? resolved.path : void 0,
18233
+ path: resolved.kind === "directory" || resolved.kind === "file" ? resolved.path : void 0,
16919
18234
  repoRoot: resolved.kind === "directory" ? resolved.repoRoot : void 0,
16920
- url: resolved.kind === "directory" ? void 0 : resolved.url,
18235
+ url: resolved.kind === "directory" || resolved.kind === "file" ? void 0 : resolved.url,
16921
18236
  createdAt: now,
16922
18237
  updatedAt: now,
16923
18238
  status: "ready",
@@ -16946,15 +18261,18 @@ async function addManagedSource(rootDir, input, options = {}) {
16946
18261
  };
16947
18262
  const nextSources = existing ? sources.map((candidate) => candidate.id === nextSource.id ? nextSource : candidate) : [...sources, nextSource];
16948
18263
  await saveManagedSources(rootDir, nextSources);
18264
+ const review = reviewRequested && nextSource.status === "ready" ? await stageSourceReviewForScope(rootDir, scopeFromManagedSource(nextSource)) : void 0;
16949
18265
  return {
16950
18266
  source: nextSource,
16951
18267
  compile,
16952
- briefGenerated
18268
+ briefGenerated,
18269
+ review
16953
18270
  };
16954
18271
  }
16955
18272
  async function reloadManagedSources(rootDir, options = {}) {
16956
18273
  const compileRequested = options.compile ?? true;
16957
18274
  const briefRequested = options.brief ?? true;
18275
+ const reviewRequested = options.review ?? false;
16958
18276
  const sources = await loadManagedSources(rootDir);
16959
18277
  const selected = options.all || !options.id ? sources : sources.filter((source) => source.id === options.id);
16960
18278
  if (!selected.length) {
@@ -16990,10 +18308,14 @@ async function reloadManagedSources(rootDir, options = {}) {
16990
18308
  };
16991
18309
  });
16992
18310
  await saveManagedSources(rootDir, nextSources);
18311
+ const reviews = reviewRequested ? await Promise.all(
18312
+ nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)).filter((source) => source.status === "ready").map(async (source) => await stageSourceReviewForScope(rootDir, scopeFromManagedSource(source)))
18313
+ ) : [];
16993
18314
  return {
16994
18315
  sources: nextSources.filter((source) => selected.some((candidate) => candidate.id === source.id)),
16995
18316
  compile,
16996
- briefPaths: [...briefPaths.values()]
18317
+ briefPaths: [...briefPaths.values()],
18318
+ reviews
16997
18319
  };
16998
18320
  }
16999
18321
  async function deleteManagedSource(rootDir, id) {
@@ -17928,10 +19250,13 @@ export {
17928
19250
  rejectApproval,
17929
19251
  reloadManagedSources,
17930
19252
  resolvePaths,
19253
+ reviewManagedSource,
19254
+ reviewSourceScope,
17931
19255
  runSchedule,
17932
19256
  runWatchCycle,
17933
19257
  searchVault,
17934
19258
  serveSchedules,
19259
+ stageGeneratedOutputPages,
17935
19260
  startGraphServer,
17936
19261
  startMcpServer,
17937
19262
  syncTrackedRepos,