syntaur 0.26.0 → 0.27.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 (62) hide show
  1. package/dashboard/dist/assets/{_basePickBy-jPItyrQO.js → _basePickBy-DPBuiT9A.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-pEwUwurC.js → _baseUniq-B5Q4dkW3.js} +1 -1
  3. package/dashboard/dist/assets/{arc-ZZtp507S.js → arc-Bp71QC_v.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BNUerPqd.js → architectureDiagram-2XIMDMQ5-CWHBISZ5.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-CQyovXFv.js → blockDiagram-WCTKOSBZ-D0txIHgi.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-wNQ6EHeF.js → c4Diagram-IC4MRINW-D_Hpnc38.js} +1 -1
  7. package/dashboard/dist/assets/channel-D41AslDq.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-ZaueC30R.js → chunk-4BX2VUAB-D0A_A8qn.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-BjsRB0t8.js → chunk-55IACEB6-DuK8QvrD.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-BHuSr-Tl.js → chunk-FMBD7UC4-B5WfIDS6.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-SHNJA0es.js → chunk-JSJVCQXG-D3jB_ZJP.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-JXFPjeo4.js → chunk-KX2RTZJC-DtxN1mOD.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-BiJqWT0B.js → chunk-NQ4KR5QH-4fQpgivN.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-DoXWBqP2.js → chunk-QZHKN3VN-BOf9TZCT.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-Dqtf_5it.js → chunk-WL4C6EOR-D9HeEPWL.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BnKy62Yt.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BnKy62Yt.js +1 -0
  18. package/dashboard/dist/assets/clone-Cz7h9axV.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-Cr6bkSKq.js → cose-bilkent-S5V4N54A-CpzWcyB7.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-oXpXFuJQ.js → dagre-KLK3FWXG-CC9-omFF.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-Bq_xdDbg.js → diagram-E7M64L7V-q_F9KKPz.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-N7Er4Dui.js → diagram-IFDJBPK2-CbYvNpQB.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-BU0Zm2Fn.js → diagram-P4PSJMXO-q8XUUKRC.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BSgZb5me.js → erDiagram-INFDFZHY-Q-oL35fO.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Bn7pEu0U.js → flowDiagram-PKNHOUZH-Cptj-2yF.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B8Xq9tyM.js → ganttDiagram-A5KZAMGK-BYmgXBad.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-BoLUjYDa.js → gitGraphDiagram-K3NZZRJ6-DHF3w-Cn.js} +1 -1
  28. package/dashboard/dist/assets/{graph-Pde_ni_y.js → graph-Br4uG9xg.js} +1 -1
  29. package/dashboard/dist/assets/index-Ds1-e_jv.css +1 -0
  30. package/dashboard/dist/assets/index-dyJ_mu3x.js +555 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Brv2khjP.js → infoDiagram-LFFYTUFH-Ckb3YLUI.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-D5hxQ0Ke.js → ishikawaDiagram-PHBUUO56-DSXXm4hL.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CUevv5jA.js → journeyDiagram-4ABVD52K-D4JJ4wn_.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cf6XyrAC.js → kanban-definition-K7BYSVSG-DZeWPcIi.js} +1 -1
  35. package/dashboard/dist/assets/{layout-Bc8RP2w3.js → layout-DU5mcBKh.js} +1 -1
  36. package/dashboard/dist/assets/{linear-Cd_XUbl7.js → linear-h7AvdT63.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-Bx8MuMEM.js → mermaid.core-DIOnVuDB.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-D_4Pl3Mu.js → mindmap-definition-YRQLILUH-BVSORv6W.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-DRVbjwxO.js → pieDiagram-SKSYHLDU-BEdO084J.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BciLlBMH.js → quadrantDiagram-337W2JSQ-3Dc5mQ7q.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-Bprwe8Z2.js → requirementDiagram-Z7DCOOCP-eu-8doSY.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DI0t8Uiu.js → sankeyDiagram-WA2Y5GQK-jA292hzv.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CpCLCs5J.js → sequenceDiagram-2WXFIKYE-et31a6Tg.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-V-1VCApT.js → stateDiagram-RAJIS63D-D6MtTWaR.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-sYL-A3ib.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DCAo6tA7.js → timeline-definition-YZTLITO2-Oa_SYaCP.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-CKlbZ6Y_.js → treemap-KZPCXAKY-vrIbKmuv.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-CJSijre_.js → vennDiagram-LZ73GAT5-B3UlkEHW.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DXd1BBmK.js → xychartDiagram-JWTSCODW-BLiVVy6A.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.js +612 -225
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +1204 -827
  54. package/dist/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/dashboard/dist/assets/channel-BYnzdl2x.js +0 -1
  57. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BnPZbM4g.js +0 -1
  58. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BnPZbM4g.js +0 -1
  59. package/dashboard/dist/assets/clone-DYNFxLr3.js +0 -1
  60. package/dashboard/dist/assets/index-7rNWNKq7.css +0 -1
  61. package/dashboard/dist/assets/index-Nc9kfSW-.js +0 -550
  62. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-B6S2ctrX.js +0 -1
@@ -169,8 +169,8 @@ function extractFrontmatter(fileContent) {
169
169
  const body = fileContent.slice(match[0].length);
170
170
  return [frontmatterBlock, body];
171
171
  }
172
- function parseSimpleValue(raw) {
173
- const trimmed = raw.trim();
172
+ function parseSimpleValue(raw2) {
173
+ const trimmed = raw2.trim();
174
174
  if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
175
175
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
176
176
  return trimmed.slice(1, -1);
@@ -468,8 +468,8 @@ function extractFrontmatter2(fileContent) {
468
468
  const body = fileContent.slice(match[0].length).trim();
469
469
  return [frontmatterBlock, body];
470
470
  }
471
- function parseSimpleValue2(raw) {
472
- const trimmed = raw.trim();
471
+ function parseSimpleValue2(raw2) {
472
+ const trimmed = raw2.trim();
473
473
  if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
474
474
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
475
475
  return trimmed.slice(1, -1);
@@ -1500,8 +1500,8 @@ ${key}: ${formatted}${content.slice(closingIdx)}`;
1500
1500
  function readFrontmatterField(content, key) {
1501
1501
  const match = content.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
1502
1502
  if (!match) return null;
1503
- const raw = match[1].trim().replace(/^['"]|['"]$/g, "");
1504
- return raw === "" || raw === "null" ? null : raw;
1503
+ const raw2 = match[1].trim().replace(/^['"]|['"]$/g, "");
1504
+ return raw2 === "" || raw2 === "null" ? null : raw2;
1505
1505
  }
1506
1506
  async function migrateLegacyArchivedProjects(projectsDir) {
1507
1507
  const result = { reconciled: [] };
@@ -1771,9 +1771,9 @@ function normalizeHiddenList(input) {
1771
1771
  if (!Array.isArray(input)) return [];
1772
1772
  const seen = /* @__PURE__ */ new Set();
1773
1773
  const out = [];
1774
- for (const raw of input) {
1775
- if (typeof raw !== "string") continue;
1776
- const name = raw.trim();
1774
+ for (const raw2 of input) {
1775
+ if (typeof raw2 !== "string") continue;
1776
+ const name = raw2.trim();
1777
1777
  if (name.length === 0) continue;
1778
1778
  if (name.length > MAX_WORKSPACE_NAME_LENGTH) continue;
1779
1779
  if (/[\r\n]/.test(name)) continue;
@@ -2152,13 +2152,13 @@ function parsePlaybooksConfig(fmBlock) {
2152
2152
  continue;
2153
2153
  }
2154
2154
  if (currentSection === "disabled" && indent >= 4 && trimmed.startsWith("- ")) {
2155
- const raw = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
2156
- if (raw.length === 0) continue;
2157
- if (/\s/.test(raw)) {
2158
- console.warn(`Warning: config.md playbooks.disabled entry "${raw}" contains whitespace, ignoring`);
2155
+ const raw2 = trimmed.slice(2).trim().replace(/^["']|["']$/g, "");
2156
+ if (raw2.length === 0) continue;
2157
+ if (/\s/.test(raw2)) {
2158
+ console.warn(`Warning: config.md playbooks.disabled entry "${raw2}" contains whitespace, ignoring`);
2159
2159
  continue;
2160
2160
  }
2161
- disabled.push(raw);
2161
+ disabled.push(raw2);
2162
2162
  continue;
2163
2163
  }
2164
2164
  }
@@ -2438,9 +2438,9 @@ function serializeHotkeyBindingsConfig(cfg) {
2438
2438
  async function writeHotkeyBindingsConfig(cfg) {
2439
2439
  const cleaned = {};
2440
2440
  for (const kind of BINDABLE_ACTION_KINDS) {
2441
- const raw = cfg.bindings[kind];
2442
- if (typeof raw !== "string" || raw.trim() === "") continue;
2443
- const canonical = canonicalizeCombo(raw);
2441
+ const raw2 = cfg.bindings[kind];
2442
+ if (typeof raw2 !== "string" || raw2.trim() === "") continue;
2443
+ const canonical = canonicalizeCombo(raw2);
2444
2444
  if (!canonical) continue;
2445
2445
  if (isReservedCombo(canonical)) continue;
2446
2446
  cleaned[kind] = canonical;
@@ -3237,8 +3237,8 @@ async function resolvePlaybookSlug(playbooksDir2, slug) {
3237
3237
  for (const entry of entries) {
3238
3238
  if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
3239
3239
  const filePath = resolve7(playbooksDir2, entry.name);
3240
- const raw = await readFile6(filePath, "utf-8");
3241
- const parsed = parsePlaybook(raw);
3240
+ const raw2 = await readFile6(filePath, "utf-8");
3241
+ const parsed = parsePlaybook(raw2);
3242
3242
  const canonical = parsed.slug || entry.name.replace(/\.md$/, "");
3243
3243
  if (canonical === slug) {
3244
3244
  return { filename: entry.name, slug: canonical, parsed };
@@ -3285,8 +3285,8 @@ async function rebuildPlaybookManifest(playbooksDir2) {
3285
3285
  const rows = [];
3286
3286
  for (const entry of entries) {
3287
3287
  if (!isVisiblePlaybookFile(entry.name, entry.isFile())) continue;
3288
- const raw = await readFile6(resolve7(playbooksDir2, entry.name), "utf-8");
3289
- const parsed = parsePlaybook(raw);
3288
+ const raw2 = await readFile6(resolve7(playbooksDir2, entry.name), "utf-8");
3289
+ const parsed = parsePlaybook(raw2);
3290
3290
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
3291
3291
  if (disabledSet.has(slug)) continue;
3292
3292
  rows.push({
@@ -3363,8 +3363,8 @@ async function renamePlaybook(playbooksDir2, oldSlug, newSlug) {
3363
3363
  );
3364
3364
  }
3365
3365
  }
3366
- const raw = await readFile6(oldPath, "utf-8");
3367
- let next = setFrontmatterField2(raw, "slug", newSlug);
3366
+ const raw2 = await readFile6(oldPath, "utf-8");
3367
+ let next = setFrontmatterField2(raw2, "slug", newSlug);
3368
3368
  next = setFrontmatterField2(next, "updated", `"${nowTimestamp()}"`);
3369
3369
  await writeFileForce(newPath, next);
3370
3370
  if (!renamedInPlace) {
@@ -4090,9 +4090,9 @@ async function migrateFromMarkdown(projectsDir) {
4090
4090
  }
4091
4091
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
4092
4092
  const { readFile: readFile21 } = await import("fs/promises");
4093
- const raw = await readFile21(filePath, "utf-8");
4093
+ const raw2 = await readFile21(filePath, "utf-8");
4094
4094
  const sessions = [];
4095
- const lines = raw.split("\n");
4095
+ const lines = raw2.split("\n");
4096
4096
  let inTable = false;
4097
4097
  let headerSeen = false;
4098
4098
  for (const line of lines) {
@@ -4248,8 +4248,8 @@ async function deleteSessions(sessionIds) {
4248
4248
  }
4249
4249
  async function readAssignmentStatusFromPath(assignmentMdPath) {
4250
4250
  if (!await fileExists(assignmentMdPath)) return null;
4251
- const raw = await readFile8(assignmentMdPath, "utf-8");
4252
- const match = raw.match(/^status:\s*(.+)$/m);
4251
+ const raw2 = await readFile8(assignmentMdPath, "utf-8");
4252
+ const match = raw2.match(/^status:\s*(.+)$/m);
4253
4253
  return match ? match[1].trim() : null;
4254
4254
  }
4255
4255
  async function readAssignmentStatus(projectDir, assignmentSlug) {
@@ -4393,8 +4393,8 @@ async function listSessionFiles(dir) {
4393
4393
  async function readSessionFile(dir, name) {
4394
4394
  const filePath = resolve11(dir, `${sanitizeSessionName(name)}.md`);
4395
4395
  if (!await fileExists(filePath)) return null;
4396
- const raw = await readFile9(filePath, "utf-8");
4397
- const [frontmatter] = extractFrontmatter2(raw);
4396
+ const raw2 = await readFile9(filePath, "utf-8");
4397
+ const [frontmatter] = extractFrontmatter2(raw2);
4398
4398
  if (!frontmatter) return null;
4399
4399
  const session = getField(frontmatter, "session") ?? name;
4400
4400
  const registered = getField(frontmatter, "registered") ?? "";
@@ -4522,8 +4522,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
4522
4522
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
4523
4523
  }
4524
4524
  function delay(ms) {
4525
- return new Promise((resolve29) => {
4526
- const timer2 = setTimeout(resolve29, ms);
4525
+ return new Promise((resolve30) => {
4526
+ const timer2 = setTimeout(resolve30, ms);
4527
4527
  if (typeof timer2.unref === "function") {
4528
4528
  timer2.unref();
4529
4529
  }
@@ -5130,8 +5130,8 @@ async function listProjects(projectsDir) {
5130
5130
  async function readWorkspaceRegistry(projectsDir) {
5131
5131
  const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
5132
5132
  try {
5133
- const raw = await readFile10(registryPath, "utf-8");
5134
- const parsed = JSON.parse(raw);
5133
+ const raw2 = await readFile10(registryPath, "utf-8");
5134
+ const parsed = JSON.parse(raw2);
5135
5135
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
5136
5136
  } catch {
5137
5137
  return [];
@@ -5219,8 +5219,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
5219
5219
  const timestamp = nowTimestamp();
5220
5220
  for (const slug of projectsReferencing) {
5221
5221
  const path = resolve13(projectsDir, slug, "project.md");
5222
- const raw = await readFile10(path, "utf-8");
5223
- let next = clearFrontmatterField(raw, "workspace");
5222
+ const raw2 = await readFile10(path, "utf-8");
5223
+ let next = clearFrontmatterField(raw2, "workspace");
5224
5224
  next = setUpdatedField(next, timestamp);
5225
5225
  await writeFileForce(path, next);
5226
5226
  rewroteFiles = true;
@@ -5228,8 +5228,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
5228
5228
  for (const id of standalonesReferencing) {
5229
5229
  if (!opts.assignmentsDir) break;
5230
5230
  const path = resolve13(opts.assignmentsDir, id, "assignment.md");
5231
- const raw = await readFile10(path, "utf-8");
5232
- let next = clearFrontmatterField(raw, "workspaceGroup");
5231
+ const raw2 = await readFile10(path, "utf-8");
5232
+ let next = clearFrontmatterField(raw2, "workspaceGroup");
5233
5233
  next = setUpdatedField(next, timestamp);
5234
5234
  await writeFileForce(path, next);
5235
5235
  rewroteFiles = true;
@@ -6679,8 +6679,8 @@ async function listPlaybooks(playbooksDir2) {
6679
6679
  for (const entry of entries) {
6680
6680
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
6681
6681
  const filePath = resolve13(playbooksDir2, entry.name);
6682
- const raw = await readFile10(filePath, "utf-8");
6683
- const parsed = parsePlaybook(raw);
6682
+ const raw2 = await readFile10(filePath, "utf-8");
6683
+ const parsed = parsePlaybook(raw2);
6684
6684
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
6685
6685
  playbooks.push({
6686
6686
  slug,
@@ -6904,8 +6904,8 @@ init_assignment_resolver();
6904
6904
  init_agent_sessions();
6905
6905
  import express from "express";
6906
6906
  import { createServer } from "http";
6907
- import { resolve as resolve28 } from "path";
6908
- import { writeFile as writeFile6, unlink as unlink6 } from "fs/promises";
6907
+ import { resolve as resolve29 } from "path";
6908
+ import { writeFile as writeFile7, unlink as unlink7 } from "fs/promises";
6909
6909
  import { WebSocketServer, WebSocket } from "ws";
6910
6910
 
6911
6911
  // src/dashboard/session-liveness.ts
@@ -7379,15 +7379,15 @@ async function readViewPrefsFile() {
7379
7379
  if (!await fileExists(path)) {
7380
7380
  return { ...DEFAULT_VIEW_PREFS_FILE };
7381
7381
  }
7382
- let raw;
7382
+ let raw2;
7383
7383
  try {
7384
- raw = await readFile11(path, "utf-8");
7384
+ raw2 = await readFile11(path, "utf-8");
7385
7385
  } catch {
7386
7386
  return { ...DEFAULT_VIEW_PREFS_FILE };
7387
7387
  }
7388
7388
  let parsed;
7389
7389
  try {
7390
- parsed = JSON.parse(raw);
7390
+ parsed = JSON.parse(raw2);
7391
7391
  } catch {
7392
7392
  await backupCorrupt(path);
7393
7393
  return { ...DEFAULT_VIEW_PREFS_FILE };
@@ -7592,15 +7592,15 @@ async function readSavedViewsFile() {
7592
7592
  if (!await fileExists(path)) {
7593
7593
  return cloneDefault();
7594
7594
  }
7595
- let raw;
7595
+ let raw2;
7596
7596
  try {
7597
- raw = await readFile12(path, "utf-8");
7597
+ raw2 = await readFile12(path, "utf-8");
7598
7598
  } catch {
7599
7599
  return cloneDefault();
7600
7600
  }
7601
7601
  let parsed;
7602
7602
  try {
7603
- parsed = JSON.parse(raw);
7603
+ parsed = JSON.parse(raw2);
7604
7604
  } catch {
7605
7605
  await backupCorrupt2(path);
7606
7606
  return cloneDefault();
@@ -8145,8 +8145,8 @@ async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
8145
8145
  const projectPath = resolve17(projectsDir, projectSlug, "project.md");
8146
8146
  if (await fileExists(projectPath)) {
8147
8147
  const project = parseProject(await readFile14(projectPath, "utf-8"));
8148
- for (const raw of project.repositories) {
8149
- const path = raw.trim();
8148
+ for (const raw2 of project.repositories) {
8149
+ const path = raw2.trim();
8150
8150
  if (!path) continue;
8151
8151
  const abs = resolve17(path);
8152
8152
  if (seen.has(abs)) continue;
@@ -11459,8 +11459,8 @@ function mapAgentErrorToFieldErrors(err) {
11459
11459
  }
11460
11460
  return { error: message };
11461
11461
  }
11462
- function coerceAgentRow(raw, index) {
11463
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
11462
+ function coerceAgentRow(raw2, index) {
11463
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) {
11464
11464
  return {
11465
11465
  ok: false,
11466
11466
  status: 400,
@@ -11470,7 +11470,7 @@ function coerceAgentRow(raw, index) {
11470
11470
  }
11471
11471
  };
11472
11472
  }
11473
- const entry = raw;
11473
+ const entry = raw2;
11474
11474
  if (typeof entry.id !== "string" || entry.id.length === 0) {
11475
11475
  return {
11476
11476
  ok: false,
@@ -11579,14 +11579,14 @@ function createAgentsRouter() {
11579
11579
  });
11580
11580
  router.put("/", async (req, res) => {
11581
11581
  try {
11582
- const raw = req.body && typeof req.body === "object" ? req.body : {};
11583
- if (!Array.isArray(raw.agents)) {
11582
+ const raw2 = req.body && typeof req.body === "object" ? req.body : {};
11583
+ if (!Array.isArray(raw2.agents)) {
11584
11584
  res.status(400).json({ error: "agents must be an array" });
11585
11585
  return;
11586
11586
  }
11587
11587
  const cleaned = [];
11588
- for (let i = 0; i < raw.agents.length; i++) {
11589
- const result = coerceAgentRow(raw.agents[i], i);
11588
+ for (let i = 0; i < raw2.agents.length; i++) {
11589
+ const result = coerceAgentRow(raw2.agents[i], i);
11590
11590
  if (!result.ok) {
11591
11591
  res.status(result.status).json(result.body);
11592
11592
  return;
@@ -12186,18 +12186,18 @@ function buildAffectedResponse(id, list) {
12186
12186
  function isString(x) {
12187
12187
  return typeof x === "string" && x.length > 0;
12188
12188
  }
12189
- function parseResolutions(raw) {
12190
- if (raw === void 0) {
12189
+ function parseResolutions(raw2) {
12190
+ if (raw2 === void 0) {
12191
12191
  return { resolutions: [], malformed: null, duplicateIds: null };
12192
12192
  }
12193
- if (!Array.isArray(raw)) {
12193
+ if (!Array.isArray(raw2)) {
12194
12194
  return { resolutions: [], malformed: "resolutions must be an array", duplicateIds: null };
12195
12195
  }
12196
12196
  const out = [];
12197
12197
  const seen = /* @__PURE__ */ new Set();
12198
12198
  const dups = /* @__PURE__ */ new Set();
12199
- for (let i = 0; i < raw.length; i++) {
12200
- const r = raw[i];
12199
+ for (let i = 0; i < raw2.length; i++) {
12200
+ const r = raw2[i];
12201
12201
  if (!r || typeof r !== "object") {
12202
12202
  return { resolutions: [], malformed: `resolutions[${i}] must be an object`, duplicateIds: null };
12203
12203
  }
@@ -13173,10 +13173,10 @@ init_fs_migration();
13173
13173
  init_parser2();
13174
13174
  init_fs();
13175
13175
  init_paths();
13176
- import { Router as Router13 } from "express";
13177
- import { readdir as readdir10 } from "fs/promises";
13178
- import { resolve as resolvePath, dirname as dirname5 } from "path";
13179
- import { rename as rename5, mkdir as mkdir2 } from "fs/promises";
13176
+ import { Router as Router14 } from "express";
13177
+ import { readdir as readdir11 } from "fs/promises";
13178
+ import { resolve as resolvePath, dirname as dirname6 } from "path";
13179
+ import { rename as rename6, mkdir as mkdir3 } from "fs/promises";
13180
13180
  init_slug();
13181
13181
 
13182
13182
  // src/utils/promote-todos.ts
@@ -13515,6 +13515,331 @@ async function promoteTodosToNewAssignment(groups, options) {
13515
13515
 
13516
13516
  // src/dashboard/api-todos.ts
13517
13517
  init_api();
13518
+
13519
+ // src/dashboard/todo-attachments-routes.ts
13520
+ import { raw } from "express";
13521
+
13522
+ // src/todos/attachments.ts
13523
+ import { mkdir as mkdir2, readdir as readdir10, stat, rename as rename5, rm as rm3, unlink as unlink5, writeFile as writeFile5, cp } from "fs/promises";
13524
+ import { resolve as resolve25, basename as basename4, dirname as dirname5, extname } from "path";
13525
+
13526
+ // src/utils/proof-artifact-id.ts
13527
+ import { randomBytes as randomBytes2 } from "crypto";
13528
+ function generateArtifactId() {
13529
+ const ts = Date.now().toString(36);
13530
+ const rand = randomBytes2(2).toString("hex");
13531
+ return `${ts}-${rand}`;
13532
+ }
13533
+
13534
+ // src/todos/attachments.ts
13535
+ var SCOPE_RE = /^[a-z0-9_][a-z0-9-]*$/;
13536
+ var TODO_ID_RE = /^[a-f0-9]{4}$/;
13537
+ var ATTACHMENT_ID_RE = /^[a-z0-9]+-[0-9a-f]{4}$/;
13538
+ var AttachmentValidationError = class extends Error {
13539
+ constructor(message) {
13540
+ super(message);
13541
+ this.name = "AttachmentValidationError";
13542
+ }
13543
+ };
13544
+ function assertScope(scopeId) {
13545
+ if (!SCOPE_RE.test(scopeId)) throw new AttachmentValidationError(`Invalid scope id: "${scopeId}"`);
13546
+ }
13547
+ function assertTodoId(todoId) {
13548
+ if (!TODO_ID_RE.test(todoId)) throw new AttachmentValidationError(`Invalid todo id: "${todoId}"`);
13549
+ }
13550
+ function assertAttachmentId(attachmentId) {
13551
+ if (!ATTACHMENT_ID_RE.test(attachmentId)) {
13552
+ throw new AttachmentValidationError(`Invalid attachment id: "${attachmentId}"`);
13553
+ }
13554
+ }
13555
+ var EXT_MIME = {
13556
+ png: "image/png",
13557
+ jpg: "image/jpeg",
13558
+ jpeg: "image/jpeg",
13559
+ gif: "image/gif",
13560
+ webp: "image/webp",
13561
+ bmp: "image/bmp",
13562
+ ico: "image/x-icon",
13563
+ svg: "image/svg+xml",
13564
+ pdf: "application/pdf",
13565
+ txt: "text/plain",
13566
+ log: "text/plain",
13567
+ md: "text/markdown",
13568
+ json: "application/json",
13569
+ csv: "text/csv",
13570
+ html: "text/html",
13571
+ htm: "text/html",
13572
+ xml: "application/xml",
13573
+ mp4: "video/mp4",
13574
+ mov: "video/quicktime",
13575
+ webm: "video/webm",
13576
+ mp3: "audio/mpeg",
13577
+ wav: "audio/wav",
13578
+ m4a: "audio/mp4",
13579
+ zip: "application/zip"
13580
+ };
13581
+ function mimeForName(name) {
13582
+ const ext = extname(name).slice(1).toLowerCase();
13583
+ return EXT_MIME[ext] ?? "application/octet-stream";
13584
+ }
13585
+ var SAFE_INLINE_MIME = /* @__PURE__ */ new Set([
13586
+ "image/png",
13587
+ "image/jpeg",
13588
+ "image/gif",
13589
+ "image/webp",
13590
+ "application/pdf",
13591
+ "text/plain"
13592
+ ]);
13593
+ function isSafeInlineMime(mime) {
13594
+ return SAFE_INLINE_MIME.has(mime);
13595
+ }
13596
+ function sanitizeAttachmentName(name) {
13597
+ let n = basename4(name || "").replace(/["'\\/]/g, "_");
13598
+ n = Array.from(n, (ch) => {
13599
+ const code = ch.charCodeAt(0);
13600
+ return code < 32 || code === 127 ? "_" : ch;
13601
+ }).join("");
13602
+ n = n.trim();
13603
+ if (!n || n === "." || n === "..") n = "file";
13604
+ if (n.length > 120) {
13605
+ const ext = extname(n);
13606
+ n = n.slice(0, Math.max(1, 120 - ext.length)) + ext;
13607
+ }
13608
+ return n;
13609
+ }
13610
+ function attachmentsRootDir(todosDir2) {
13611
+ return resolve25(todosDir2, "attachments");
13612
+ }
13613
+ function attachmentDirFor(todosDir2, scopeId, todoId) {
13614
+ assertScope(scopeId);
13615
+ assertTodoId(todoId);
13616
+ return resolve25(attachmentsRootDir(todosDir2), scopeId, todoId);
13617
+ }
13618
+ async function dirExists(p) {
13619
+ try {
13620
+ return (await stat(p)).isDirectory();
13621
+ } catch {
13622
+ return false;
13623
+ }
13624
+ }
13625
+ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes) {
13626
+ const dir = attachmentDirFor(todosDir2, scopeId, todoId);
13627
+ await mkdir2(dir, { recursive: true });
13628
+ const id = generateArtifactId();
13629
+ const filename = sanitizeAttachmentName(originalName);
13630
+ await writeFile5(resolve25(dir, `${id}__${filename}`), bytes);
13631
+ return {
13632
+ id,
13633
+ filename,
13634
+ mime: mimeForName(filename),
13635
+ size: bytes.length,
13636
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
13637
+ };
13638
+ }
13639
+ async function listAttachments(todosDir2, scopeId, todoId) {
13640
+ const dir = attachmentDirFor(todosDir2, scopeId, todoId);
13641
+ let names;
13642
+ try {
13643
+ names = await readdir10(dir);
13644
+ } catch {
13645
+ return [];
13646
+ }
13647
+ const out = [];
13648
+ for (const stored of names) {
13649
+ const sep2 = stored.indexOf("__");
13650
+ if (sep2 <= 0) continue;
13651
+ const id = stored.slice(0, sep2);
13652
+ if (!ATTACHMENT_ID_RE.test(id)) continue;
13653
+ const filename = stored.slice(sep2 + 2);
13654
+ try {
13655
+ const st = await stat(resolve25(dir, stored));
13656
+ if (!st.isFile()) continue;
13657
+ out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
13658
+ } catch {
13659
+ }
13660
+ }
13661
+ out.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
13662
+ return out;
13663
+ }
13664
+ async function readScopeAttachments(todosDir2, scopeId) {
13665
+ assertScope(scopeId);
13666
+ const scopeDir = resolve25(attachmentsRootDir(todosDir2), scopeId);
13667
+ let todoIds;
13668
+ try {
13669
+ todoIds = await readdir10(scopeDir);
13670
+ } catch {
13671
+ return {};
13672
+ }
13673
+ const result = {};
13674
+ for (const todoId of todoIds) {
13675
+ if (!TODO_ID_RE.test(todoId)) continue;
13676
+ const list = await listAttachments(todosDir2, scopeId, todoId);
13677
+ if (list.length) result[todoId] = list;
13678
+ }
13679
+ return result;
13680
+ }
13681
+ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
13682
+ assertAttachmentId(attachmentId);
13683
+ const dir = attachmentDirFor(todosDir2, scopeId, todoId);
13684
+ let names;
13685
+ try {
13686
+ names = await readdir10(dir);
13687
+ } catch {
13688
+ return null;
13689
+ }
13690
+ const prefix = `${attachmentId}__`;
13691
+ const stored = names.find((n) => n.startsWith(prefix));
13692
+ if (!stored) return null;
13693
+ const filename = stored.slice(prefix.length);
13694
+ return { path: resolve25(dir, stored), filename, mime: mimeForName(filename) };
13695
+ }
13696
+ async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
13697
+ const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
13698
+ if (!resolved) return false;
13699
+ await unlink5(resolved.path);
13700
+ return true;
13701
+ }
13702
+ async function deleteAllAttachments(todosDir2, scopeId, todoId) {
13703
+ await rm3(attachmentDirFor(todosDir2, scopeId, todoId), { recursive: true, force: true });
13704
+ }
13705
+ async function attachmentMoveConflict(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId, todoId) {
13706
+ const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
13707
+ const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
13708
+ return await dirExists(src) && await dirExists(dst);
13709
+ }
13710
+ async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId, todoId) {
13711
+ const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
13712
+ if (!await dirExists(src)) return;
13713
+ const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
13714
+ await mkdir2(dirname5(dst), { recursive: true });
13715
+ try {
13716
+ await rename5(src, dst);
13717
+ } catch (err) {
13718
+ if (err?.code === "EXDEV") {
13719
+ await cp(src, dst, { recursive: true });
13720
+ await rm3(src, { recursive: true, force: true });
13721
+ } else {
13722
+ throw err;
13723
+ }
13724
+ }
13725
+ }
13726
+
13727
+ // src/dashboard/todo-attachments-routes.ts
13728
+ var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
13729
+ function headerValue(req, name) {
13730
+ const v = req.headers[name];
13731
+ return Array.isArray(v) ? v[0] : v;
13732
+ }
13733
+ function paramStr(v) {
13734
+ return Array.isArray(v) ? v[0] ?? "" : v ?? "";
13735
+ }
13736
+ function sendError(res, err) {
13737
+ if (err instanceof AttachmentValidationError) {
13738
+ res.status(400).json({ error: err.message });
13739
+ return;
13740
+ }
13741
+ res.status(500).json({ error: err instanceof Error ? err.message : "Attachment operation failed" });
13742
+ }
13743
+ function contentDisposition(filename, inline) {
13744
+ const disp = inline ? "inline" : "attachment";
13745
+ const asciiFallback = Array.from(filename, (ch) => {
13746
+ const code = ch.charCodeAt(0);
13747
+ return code >= 32 && code <= 126 && ch !== '"' && ch !== "\\" ? ch : "_";
13748
+ }).join("");
13749
+ return `${disp}; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
13750
+ }
13751
+ function installTodoAttachmentRoutes(router, prefix, opts) {
13752
+ router.post(
13753
+ `${prefix}/attachments`,
13754
+ raw({ type: () => true, limit: MAX_UPLOAD_BYTES }),
13755
+ async (req, res) => {
13756
+ try {
13757
+ const rawName = headerValue(req, "x-attachment-filename");
13758
+ let filename = "file";
13759
+ if (rawName) {
13760
+ try {
13761
+ filename = decodeURIComponent(rawName);
13762
+ } catch {
13763
+ res.status(400).json({ error: "Invalid x-attachment-filename header" });
13764
+ return;
13765
+ }
13766
+ }
13767
+ const body = req.body;
13768
+ if (!Buffer.isBuffer(body) || body.length === 0) {
13769
+ res.status(400).json({ error: "Empty upload body" });
13770
+ return;
13771
+ }
13772
+ const scope = opts.resolveScope(req);
13773
+ const result = await opts.withScopeLock(req, async () => {
13774
+ if (!await opts.todoExists(scope)) return null;
13775
+ return writeAttachment(scope.todosDir, scope.scopeId, scope.todoId, filename, body);
13776
+ });
13777
+ if (!result) {
13778
+ res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
13779
+ return;
13780
+ }
13781
+ opts.onChange(req);
13782
+ res.status(201).json(result);
13783
+ } catch (err) {
13784
+ sendError(res, err);
13785
+ }
13786
+ }
13787
+ );
13788
+ router.get(`${prefix}/attachments/:attachmentId`, async (req, res) => {
13789
+ try {
13790
+ const scope = opts.resolveScope(req);
13791
+ const attachmentId = paramStr(req.params.attachmentId);
13792
+ const resolved = await opts.withScopeLock(req, async () => {
13793
+ if (!await opts.todoExists(scope)) return { notFound: true };
13794
+ return {
13795
+ file: await resolveAttachmentFile(scope.todosDir, scope.scopeId, scope.todoId, attachmentId)
13796
+ };
13797
+ });
13798
+ if ("notFound" in resolved) {
13799
+ res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
13800
+ return;
13801
+ }
13802
+ if (!resolved.file) {
13803
+ res.status(404).json({ error: `Attachment "${attachmentId}" not found` });
13804
+ return;
13805
+ }
13806
+ const { path, filename, mime } = resolved.file;
13807
+ const inline = isSafeInlineMime(mime);
13808
+ res.setHeader("X-Content-Type-Options", "nosniff");
13809
+ res.setHeader("Content-Type", inline ? mime : "application/octet-stream");
13810
+ res.setHeader("Content-Disposition", contentDisposition(filename, inline));
13811
+ res.sendFile(path, (err) => {
13812
+ if (err && !res.headersSent) res.status(500).end();
13813
+ });
13814
+ } catch (err) {
13815
+ sendError(res, err);
13816
+ }
13817
+ });
13818
+ router.delete(`${prefix}/attachments/:attachmentId`, async (req, res) => {
13819
+ try {
13820
+ const scope = opts.resolveScope(req);
13821
+ const attachmentId = paramStr(req.params.attachmentId);
13822
+ const result = await opts.withScopeLock(req, async () => {
13823
+ if (!await opts.todoExists(scope)) return { notFound: true };
13824
+ return { deleted: await deleteAttachment(scope.todosDir, scope.scopeId, scope.todoId, attachmentId) };
13825
+ });
13826
+ if ("notFound" in result) {
13827
+ res.status(404).json({ error: `Todo "${scope.todoId}" not found` });
13828
+ return;
13829
+ }
13830
+ if (!result.deleted) {
13831
+ res.status(404).json({ error: `Attachment "${attachmentId}" not found` });
13832
+ return;
13833
+ }
13834
+ opts.onChange(req);
13835
+ res.json({ deleted: attachmentId });
13836
+ } catch (err) {
13837
+ sendError(res, err);
13838
+ }
13839
+ });
13840
+ }
13841
+
13842
+ // src/dashboard/api-todos.ts
13518
13843
  var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
13519
13844
  function getWorkspaceParam(value) {
13520
13845
  if (Array.isArray(value)) {
@@ -13528,7 +13853,7 @@ function touchItem3(item) {
13528
13853
  item.updatedAt = now;
13529
13854
  }
13530
13855
  function createTodosRouter(todosDir2, broadcast, projectsDir) {
13531
- const router = Router13();
13856
+ const router = Router14();
13532
13857
  installRecordsInvalidation(router);
13533
13858
  function broadcastUpdate() {
13534
13859
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -13545,6 +13870,19 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13545
13870
  next();
13546
13871
  }
13547
13872
  router.param("workspace", validateWorkspace);
13873
+ installTodoAttachmentRoutes(router, "/:workspace/:id", {
13874
+ resolveScope: (req) => ({
13875
+ todosDir: todosDir2,
13876
+ scopeId: getWorkspaceParam(req.params.workspace),
13877
+ todoId: getWorkspaceParam(req.params.id)
13878
+ }),
13879
+ withScopeLock: (req, fn) => wsLock(getWorkspaceParam(req.params.workspace), fn),
13880
+ todoExists: async (scope) => {
13881
+ const checklist = await readChecklist(scope.todosDir, scope.scopeId);
13882
+ return checklist.items.some((i) => i.id === scope.todoId);
13883
+ },
13884
+ onChange: () => broadcastUpdate()
13885
+ });
13548
13886
  router.post("/promote-bulk", async (req, res) => {
13549
13887
  try {
13550
13888
  const { groups, mode, target, title, type, priority, keepSource } = req.body ?? {};
@@ -13649,17 +13987,18 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13649
13987
  router.get("/", async (_req, res) => {
13650
13988
  try {
13651
13989
  await ensureDir(todosDir2);
13652
- const files = await readdir10(todosDir2).catch(() => []);
13990
+ const files = await readdir11(todosDir2).catch(() => []);
13653
13991
  const workspaces = [];
13654
13992
  for (const file of files) {
13655
13993
  if (typeof file !== "string") continue;
13656
13994
  if (!file.endsWith(".md") || file.endsWith("-log.md")) continue;
13657
13995
  const workspace = file.replace(".md", "");
13658
13996
  const checklist = await readChecklist(todosDir2, workspace);
13997
+ const attachmentsByTodo = await readScopeAttachments(todosDir2, checklist.workspace);
13659
13998
  workspaces.push({
13660
13999
  workspace: checklist.workspace,
13661
14000
  archiveInterval: checklist.archiveInterval,
13662
- items: checklist.items,
14001
+ items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
13663
14002
  counts: computeCounts(checklist.items)
13664
14003
  });
13665
14004
  }
@@ -13672,10 +14011,11 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13672
14011
  try {
13673
14012
  const workspace = getWorkspaceParam(req.params.workspace);
13674
14013
  const checklist = await readChecklist(todosDir2, workspace);
14014
+ const attachmentsByTodo = await readScopeAttachments(todosDir2, workspace);
13675
14015
  res.json({
13676
14016
  workspace: checklist.workspace,
13677
14017
  archiveInterval: checklist.archiveInterval,
13678
- items: checklist.items,
14018
+ items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
13679
14019
  counts: computeCounts(checklist.items)
13680
14020
  });
13681
14021
  } catch (error) {
@@ -13763,63 +14103,68 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
13763
14103
  router.post("/:workspace/archive", async (req, res) => {
13764
14104
  try {
13765
14105
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
13766
- const { resolve: resolve29 } = await import("path");
14106
+ const { resolve: resolve30 } = await import("path");
13767
14107
  const { readFile: readFile21 } = await import("fs/promises");
13768
14108
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
13769
14109
  const workspace = getWorkspaceParam(req.params.workspace);
13770
- const checklist = await readChecklist(todosDir2, workspace);
13771
- const log = await readLog(todosDir2, workspace);
13772
- const completedIds = new Set(
13773
- checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
13774
- );
13775
- if (completedIds.size === 0) {
13776
- res.json({ archived: 0, message: "No completed items to archive" });
13777
- return;
13778
- }
13779
- const toArchive = log.entries.filter(
13780
- (e) => e.itemIds.every((id) => completedIds.has(id))
13781
- );
13782
- const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
13783
- await ensureDir(resolve29(todosDir2, "archive"));
13784
- let archContent = "";
13785
- if (await fileExists(archFile)) {
13786
- archContent = await readFile21(archFile, "utf-8");
13787
- archContent = archContent.trimEnd() + "\n\n";
13788
- } else {
13789
- archContent = `---
14110
+ const outcome = await wsLock(workspace, async () => {
14111
+ const checklist = await readChecklist(todosDir2, workspace);
14112
+ const log = await readLog(todosDir2, workspace);
14113
+ const completedIds = new Set(
14114
+ checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
14115
+ );
14116
+ if (completedIds.size === 0) {
14117
+ return { archived: 0, message: "No completed items to archive" };
14118
+ }
14119
+ const toArchive = log.entries.filter(
14120
+ (e) => e.itemIds.every((id) => completedIds.has(id))
14121
+ );
14122
+ const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
14123
+ await ensureDir(resolve30(todosDir2, "archive"));
14124
+ let archContent = "";
14125
+ if (await fileExists(archFile)) {
14126
+ archContent = await readFile21(archFile, "utf-8");
14127
+ archContent = archContent.trimEnd() + "\n\n";
14128
+ } else {
14129
+ archContent = `---
13790
14130
  workspace: ${workspace}
13791
14131
  ---
13792
14132
 
13793
14133
  # Archive
13794
14134
 
13795
14135
  `;
13796
- }
13797
- const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
13798
- for (const item of completedItems) {
13799
- archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
14136
+ }
14137
+ const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
14138
+ for (const item of completedItems) {
14139
+ archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
13800
14140
  `;
13801
- }
13802
- archContent += "\n";
13803
- for (const entry of toArchive) {
13804
- archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
14141
+ }
14142
+ archContent += "\n";
14143
+ for (const entry of toArchive) {
14144
+ archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
13805
14145
  `;
13806
- if (entry.items) archContent += `**Items:** ${entry.items}
14146
+ if (entry.items) archContent += `**Items:** ${entry.items}
13807
14147
  `;
13808
- if (entry.session) archContent += `**Session:** ${entry.session}
14148
+ if (entry.session) archContent += `**Session:** ${entry.session}
13809
14149
  `;
13810
- if (entry.branch) archContent += `**Branch:** ${entry.branch}
14150
+ if (entry.branch) archContent += `**Branch:** ${entry.branch}
13811
14151
  `;
13812
- if (entry.summary) archContent += `**Summary:** ${entry.summary}
14152
+ if (entry.summary) archContent += `**Summary:** ${entry.summary}
13813
14153
  `;
13814
- if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
14154
+ if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
13815
14155
  `;
13816
- archContent += "\n";
13817
- }
13818
- await writeFileForce2(archFile, archContent);
13819
- checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
13820
- await writeChecklist(todosDir2, checklist);
13821
- broadcastUpdate();
13822
- res.json({ archived: completedIds.size, logEntries: toArchive.length });
14156
+ archContent += "\n";
14157
+ }
14158
+ await writeFileForce2(archFile, archContent);
14159
+ checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
14160
+ await writeChecklist(todosDir2, checklist);
14161
+ for (const id of completedIds) {
14162
+ await deleteAllAttachments(todosDir2, workspace, id);
14163
+ }
14164
+ return { archived: completedIds.size, logEntries: toArchive.length };
14165
+ });
14166
+ if (outcome.archived > 0) broadcastUpdate();
14167
+ res.json(outcome);
13823
14168
  } catch (error) {
13824
14169
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to archive" });
13825
14170
  }
@@ -13844,7 +14189,8 @@ workspace: ${workspace}
13844
14189
  }
13845
14190
  const log = await readLog(todosDir2, workspace);
13846
14191
  const logEntries = log.entries.filter((e) => e.itemIds.includes(req.params.id));
13847
- res.json({ ...item, log: logEntries });
14192
+ const attachments = await listAttachments(todosDir2, workspace, item.id);
14193
+ res.json({ ...item, attachments, log: logEntries });
13848
14194
  } catch (error) {
13849
14195
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
13850
14196
  }
@@ -13881,6 +14227,7 @@ workspace: ${workspace}
13881
14227
  if (idx === -1) return false;
13882
14228
  checklist.items.splice(idx, 1);
13883
14229
  await writeChecklist(todosDir2, checklist);
14230
+ await deleteAllAttachments(todosDir2, workspace, req.params.id);
13884
14231
  return true;
13885
14232
  });
13886
14233
  if (!deleted) {
@@ -14192,15 +14539,22 @@ workspace: ${workspace}
14192
14539
  return { status: 409, error: "id already exists in target" };
14193
14540
  }
14194
14541
  const item = sourceChecklist.items[idx];
14542
+ let newPlanDir = null;
14195
14543
  if (item.planDir) {
14196
- const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
14544
+ newPlanDir = todoPlanDir(target.todosPath, target.id, id);
14197
14545
  if (await fileExists(newPlanDir)) {
14198
14546
  return { status: 409, error: "plan dir already exists in target" };
14199
14547
  }
14200
- await mkdir2(dirname5(newPlanDir), { recursive: true });
14201
- await rename5(item.planDir, newPlanDir);
14548
+ }
14549
+ if (await attachmentMoveConflict(todosDir2, sourceWs, target.todosPath, target.id, id)) {
14550
+ return { status: 409, error: "attachments already exist in target" };
14551
+ }
14552
+ if (item.planDir && newPlanDir) {
14553
+ await mkdir3(dirname6(newPlanDir), { recursive: true });
14554
+ await rename6(item.planDir, newPlanDir);
14202
14555
  item.planDir = newPlanDir;
14203
14556
  }
14557
+ await moveAttachments(todosDir2, sourceWs, target.todosPath, target.id, id);
14204
14558
  sourceChecklist.items.splice(idx, 1);
14205
14559
  targetChecklist.items.push(item);
14206
14560
  await writeChecklist(todosDir2, sourceChecklist);
@@ -14273,9 +14627,9 @@ init_parser2();
14273
14627
  init_fs();
14274
14628
  init_paths();
14275
14629
  init_slug();
14276
- import { Router as Router14 } from "express";
14277
- import { mkdir as mkdir3, readFile as readFile18, rename as rename6 } from "fs/promises";
14278
- import { resolve as resolve25, dirname as dirname6 } from "path";
14630
+ import { Router as Router15 } from "express";
14631
+ import { mkdir as mkdir4, readFile as readFile18, rename as rename7 } from "fs/promises";
14632
+ import { resolve as resolve26, dirname as dirname7 } from "path";
14279
14633
  init_api();
14280
14634
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
14281
14635
  function touchItem4(item) {
@@ -14291,12 +14645,12 @@ function params(req) {
14291
14645
  return req.params;
14292
14646
  }
14293
14647
  async function projectExists(projectsDir, slug) {
14294
- return fileExists(resolve25(projectsDir, slug, "project.md"));
14648
+ return fileExists(resolve26(projectsDir, slug, "project.md"));
14295
14649
  }
14296
14650
  async function ensureProjectTodosDir(projectsDir, slug) {
14297
14651
  const todosDir2 = projectTodosDir(projectsDir, slug);
14298
14652
  try {
14299
- await mkdir3(todosDir2, { recursive: false });
14653
+ await mkdir4(todosDir2, { recursive: false });
14300
14654
  } catch (err) {
14301
14655
  const code = err.code;
14302
14656
  if (code === "EEXIST") return;
@@ -14308,7 +14662,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
14308
14662
  throw err;
14309
14663
  }
14310
14664
  try {
14311
- await mkdir3(resolve25(todosDir2, "archive"), { recursive: false });
14665
+ await mkdir4(resolve26(todosDir2, "archive"), { recursive: false });
14312
14666
  } catch (err) {
14313
14667
  const code = err.code;
14314
14668
  if (code === "EEXIST") return;
@@ -14324,7 +14678,7 @@ function notFound(res, slug) {
14324
14678
  res.status(404).json({ error: `Project "${slug}" not found` });
14325
14679
  }
14326
14680
  function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14327
- const router = Router14({ mergeParams: true });
14681
+ const router = Router15({ mergeParams: true });
14328
14682
  installRecordsInvalidation(router);
14329
14683
  function broadcastUpdate(projectSlug) {
14330
14684
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -14341,6 +14695,18 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14341
14695
  next();
14342
14696
  }
14343
14697
  router.use(validateProjectId);
14698
+ installTodoAttachmentRoutes(router, "/:id", {
14699
+ resolveScope: (req) => {
14700
+ const slug = getProjectIdParam(params(req).projectId);
14701
+ return { todosDir: projectTodosDir(projectsDir, slug), scopeId: slug, todoId: params(req).id ?? "" };
14702
+ },
14703
+ withScopeLock: (req, fn) => projLock(getProjectIdParam(params(req).projectId), fn),
14704
+ todoExists: async (scope) => {
14705
+ const checklist = await readChecklist(scope.todosDir, scope.scopeId);
14706
+ return checklist.items.some((i) => i.id === scope.todoId);
14707
+ },
14708
+ onChange: (req) => broadcastUpdate(getProjectIdParam(params(req).projectId))
14709
+ });
14344
14710
  router.get("/", async (req, res) => {
14345
14711
  try {
14346
14712
  const slug = getProjectIdParam(params(req).projectId);
@@ -14350,10 +14716,11 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14350
14716
  }
14351
14717
  const todosDir2 = projectTodosDir(projectsDir, slug);
14352
14718
  const checklist = await readChecklist(todosDir2, slug);
14719
+ const attachmentsByTodo = await readScopeAttachments(todosDir2, slug);
14353
14720
  res.json({
14354
14721
  workspace: checklist.workspace,
14355
14722
  archiveInterval: checklist.archiveInterval,
14356
- items: checklist.items,
14723
+ items: checklist.items.map((i) => ({ ...i, attachments: attachmentsByTodo[i.id] ?? [] })),
14357
14724
  counts: computeCounts(checklist.items)
14358
14725
  });
14359
14726
  } catch (error) {
@@ -14481,61 +14848,71 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
14481
14848
  notFound(res, slug);
14482
14849
  return;
14483
14850
  }
14484
- const todosDir2 = projectTodosDir(projectsDir, slug);
14485
- await ensureProjectTodosDir(projectsDir, slug);
14486
- const checklist = await readChecklist(todosDir2, slug);
14487
- const log = await readLog(todosDir2, slug);
14488
- const completedIds = new Set(
14489
- checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
14490
- );
14491
- if (completedIds.size === 0) {
14492
- res.json({ archived: 0, message: "No completed items to archive" });
14493
- return;
14494
- }
14495
- const toArchive = log.entries.filter(
14496
- (e) => e.itemIds.every((id) => completedIds.has(id))
14497
- );
14498
- const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
14499
- let archContent = "";
14500
- if (await fileExists(archFile)) {
14501
- archContent = await readFile18(archFile, "utf-8");
14502
- archContent = archContent.trimEnd() + "\n\n";
14503
- } else {
14504
- archContent = `---
14851
+ const outcome = await projLock(slug, async () => {
14852
+ if (!await projectExists(projectsDir, slug)) return "gone";
14853
+ await ensureProjectTodosDir(projectsDir, slug);
14854
+ const todosDir2 = projectTodosDir(projectsDir, slug);
14855
+ const checklist = await readChecklist(todosDir2, slug);
14856
+ const log = await readLog(todosDir2, slug);
14857
+ const completedIds = new Set(
14858
+ checklist.items.filter((i) => i.status === "completed").map((i) => i.id)
14859
+ );
14860
+ if (completedIds.size === 0) {
14861
+ return { archived: 0, message: "No completed items to archive" };
14862
+ }
14863
+ const toArchive = log.entries.filter(
14864
+ (e) => e.itemIds.every((id) => completedIds.has(id))
14865
+ );
14866
+ const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
14867
+ let archContent = "";
14868
+ if (await fileExists(archFile)) {
14869
+ archContent = await readFile18(archFile, "utf-8");
14870
+ archContent = archContent.trimEnd() + "\n\n";
14871
+ } else {
14872
+ archContent = `---
14505
14873
  workspace: ${slug}
14506
14874
  ---
14507
14875
 
14508
14876
  # Archive
14509
14877
 
14510
14878
  `;
14511
- }
14512
- const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
14513
- for (const item of completedItems) {
14514
- archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
14879
+ }
14880
+ const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
14881
+ for (const item of completedItems) {
14882
+ archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
14515
14883
  `;
14516
- }
14517
- archContent += "\n";
14518
- for (const entry of toArchive) {
14519
- archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
14884
+ }
14885
+ archContent += "\n";
14886
+ for (const entry of toArchive) {
14887
+ archContent += `### ${entry.timestamp} \u2014 ${entry.itemIds.map((i) => `t:${i}`).join(", ")}
14520
14888
  `;
14521
- if (entry.items) archContent += `**Items:** ${entry.items}
14889
+ if (entry.items) archContent += `**Items:** ${entry.items}
14522
14890
  `;
14523
- if (entry.session) archContent += `**Session:** ${entry.session}
14891
+ if (entry.session) archContent += `**Session:** ${entry.session}
14524
14892
  `;
14525
- if (entry.branch) archContent += `**Branch:** ${entry.branch}
14893
+ if (entry.branch) archContent += `**Branch:** ${entry.branch}
14526
14894
  `;
14527
- if (entry.summary) archContent += `**Summary:** ${entry.summary}
14895
+ if (entry.summary) archContent += `**Summary:** ${entry.summary}
14528
14896
  `;
14529
- if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
14897
+ if (entry.blockers) archContent += `**Blockers:** ${entry.blockers}
14530
14898
  `;
14531
- archContent += "\n";
14899
+ archContent += "\n";
14900
+ }
14901
+ await writeFileForce(archFile, archContent);
14902
+ checklist.workspace = slug;
14903
+ checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
14904
+ await writeChecklist(todosDir2, checklist);
14905
+ for (const id of completedIds) {
14906
+ await deleteAllAttachments(todosDir2, slug, id);
14907
+ }
14908
+ return { archived: completedIds.size, logEntries: toArchive.length };
14909
+ });
14910
+ if (outcome === "gone") {
14911
+ notFound(res, slug);
14912
+ return;
14532
14913
  }
14533
- await writeFileForce(archFile, archContent);
14534
- checklist.workspace = slug;
14535
- checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
14536
- await writeChecklist(todosDir2, checklist);
14537
- broadcastUpdate(slug);
14538
- res.json({ archived: completedIds.size, logEntries: toArchive.length });
14914
+ if (outcome.archived > 0) broadcastUpdate(slug);
14915
+ res.json(outcome);
14539
14916
  } catch (error) {
14540
14917
  if (error.code === "PROJECT_GONE") {
14541
14918
  notFound(res, getProjectIdParam(params(req).projectId));
@@ -14575,7 +14952,8 @@ workspace: ${slug}
14575
14952
  }
14576
14953
  const log = await readLog(todosDir2, slug);
14577
14954
  const logEntries = log.entries.filter((e) => e.itemIds.includes(params(req).id ?? ""));
14578
- res.json({ ...item, log: logEntries });
14955
+ const attachments = await listAttachments(todosDir2, slug, item.id);
14956
+ res.json({ ...item, attachments, log: logEntries });
14579
14957
  } catch (error) {
14580
14958
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to get todo" });
14581
14959
  }
@@ -14631,11 +15009,13 @@ workspace: ${slug}
14631
15009
  await ensureProjectTodosDir(projectsDir, slug);
14632
15010
  const todosDir2 = projectTodosDir(projectsDir, slug);
14633
15011
  const checklist = await readChecklist(todosDir2, slug);
14634
- const idx = checklist.items.findIndex((i) => i.id === (params(req).id ?? ""));
15012
+ const targetId = params(req).id ?? "";
15013
+ const idx = checklist.items.findIndex((i) => i.id === targetId);
14635
15014
  if (idx === -1) return false;
14636
15015
  checklist.items.splice(idx, 1);
14637
15016
  checklist.workspace = slug;
14638
15017
  await writeChecklist(todosDir2, checklist);
15018
+ await deleteAllAttachments(todosDir2, slug, targetId);
14639
15019
  return true;
14640
15020
  });
14641
15021
  if (deleted === "gone") {
@@ -14945,15 +15325,15 @@ workspace: ${slug}
14945
15325
  if (tg.includes("/")) {
14946
15326
  const parts = tg.split("/");
14947
15327
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
14948
- assignmentDir = resolve25(projectsDir, parts[0], "assignments", parts[1]);
15328
+ assignmentDir = resolve26(projectsDir, parts[0], "assignments", parts[1]);
14949
15329
  assignmentRef = `${parts[0]}/${parts[1]}`;
14950
15330
  } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
14951
- assignmentDir = resolve25(assignmentsDirFn(), tg);
15331
+ assignmentDir = resolve26(assignmentsDirFn(), tg);
14952
15332
  assignmentRef = tg;
14953
15333
  } else {
14954
15334
  return { error: `Invalid target.assignment "${tg}"` };
14955
15335
  }
14956
- const assignmentMdPath = resolve25(assignmentDir, "assignment.md");
15336
+ const assignmentMdPath = resolve26(assignmentDir, "assignment.md");
14957
15337
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
14958
15338
  let content = await readFile18(assignmentMdPath, "utf-8");
14959
15339
  content = appendTodosToAssignmentBody2(
@@ -15083,15 +15463,22 @@ workspace: ${slug}
15083
15463
  return { status: 409, error: "id already exists in target" };
15084
15464
  }
15085
15465
  const item = sourceChecklist.items[idx];
15466
+ let newPlanDir = null;
15086
15467
  if (item.planDir) {
15087
- const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
15468
+ newPlanDir = todoPlanDir(target.todosPath, target.id, id);
15088
15469
  if (await fileExists(newPlanDir)) {
15089
15470
  return { status: 409, error: "plan dir already exists in target" };
15090
15471
  }
15091
- await mkdir3(dirname6(newPlanDir), { recursive: true });
15092
- await rename6(item.planDir, newPlanDir);
15472
+ }
15473
+ if (await attachmentMoveConflict(sourceTodosDir, sourceSlug, target.todosPath, target.id, id)) {
15474
+ return { status: 409, error: "attachments already exist in target" };
15475
+ }
15476
+ if (item.planDir && newPlanDir) {
15477
+ await mkdir4(dirname7(newPlanDir), { recursive: true });
15478
+ await rename7(item.planDir, newPlanDir);
15093
15479
  item.planDir = newPlanDir;
15094
15480
  }
15481
+ await moveAttachments(sourceTodosDir, sourceSlug, target.todosPath, target.id, id);
15095
15482
  sourceChecklist.items.splice(idx, 1);
15096
15483
  targetChecklist.items.push(item);
15097
15484
  sourceChecklist.workspace = sourceSlug;
@@ -15150,33 +15537,33 @@ workspace: ${slug}
15150
15537
  }
15151
15538
 
15152
15539
  // src/dashboard/api-bundles.ts
15153
- import { Router as Router15 } from "express";
15154
- import { readdir as readdir11 } from "fs/promises";
15540
+ import { Router as Router16 } from "express";
15541
+ import { readdir as readdir12 } from "fs/promises";
15155
15542
 
15156
15543
  // src/todos/bundle-parser.ts
15157
15544
  init_parser();
15158
15545
  init_fs();
15159
15546
  init_paths();
15160
15547
  init_parser2();
15161
- import { randomBytes as randomBytes2 } from "crypto";
15548
+ import { randomBytes as randomBytes3 } from "crypto";
15162
15549
  import { readFile as readFile19 } from "fs/promises";
15163
15550
  var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
15164
15551
  var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
15165
15552
  var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
15166
15553
  var BUNDLE_LINE_REGEX = /^- b:([a-f0-9]{4})\s+<([^>]*)>\s*$/;
15167
- function parseScopeToken(raw) {
15168
- const idx = raw.indexOf(":");
15554
+ function parseScopeToken(raw2) {
15555
+ const idx = raw2.indexOf(":");
15169
15556
  if (idx < 0) return null;
15170
- const scopeRaw = raw.slice(0, idx);
15171
- const scopeId = raw.slice(idx + 1);
15557
+ const scopeRaw = raw2.slice(0, idx);
15558
+ const scopeId = raw2.slice(idx + 1);
15172
15559
  if (!SCOPE_VALUES.has(scopeRaw)) return null;
15173
15560
  if (!scopeId) return null;
15174
15561
  if (!SCOPE_ID_REGEX.test(scopeId)) return null;
15175
15562
  return { scope: scopeRaw, scopeId };
15176
15563
  }
15177
- function parseTodosToken(raw) {
15178
- if (!raw) return [];
15179
- return raw.split(",").map((s) => s.trim()).filter((s) => BUNDLE_ID_REGEX.test(s));
15564
+ function parseTodosToken(raw2) {
15565
+ if (!raw2) return [];
15566
+ return raw2.split(",").map((s) => s.trim()).filter((s) => BUNDLE_ID_REGEX.test(s));
15180
15567
  }
15181
15568
  function parseBundleLine(line) {
15182
15569
  const match = line.match(BUNDLE_LINE_REGEX);
@@ -15261,7 +15648,7 @@ function annotate(bundle, items) {
15261
15648
  }
15262
15649
  function createBundlesRouter(todosDir2, broadcast) {
15263
15650
  void broadcast;
15264
- const router = Router15();
15651
+ const router = Router16();
15265
15652
  function validateWorkspace(req, res, next) {
15266
15653
  const workspace = getWorkspaceParam2(req.params.workspace);
15267
15654
  if (workspace && !WORKSPACE_REGEX3.test(workspace)) {
@@ -15275,7 +15662,7 @@ function createBundlesRouter(todosDir2, broadcast) {
15275
15662
  try {
15276
15663
  await ensureDir(todosDir2);
15277
15664
  const bundles = await readBundles(todosDir2);
15278
- const workspaceFiles = await readdir11(todosDir2).catch(() => []);
15665
+ const workspaceFiles = await readdir12(todosDir2).catch(() => []);
15279
15666
  const itemsByKey = /* @__PURE__ */ new Map();
15280
15667
  for (const f of workspaceFiles) {
15281
15668
  if (typeof f !== "string") continue;
@@ -15327,8 +15714,8 @@ function createBundlesRouter(todosDir2, broadcast) {
15327
15714
  init_fs();
15328
15715
  init_paths();
15329
15716
  init_slug();
15330
- import { Router as Router16 } from "express";
15331
- import { resolve as resolve26 } from "path";
15717
+ import { Router as Router17 } from "express";
15718
+ import { resolve as resolve27 } from "path";
15332
15719
  init_parser2();
15333
15720
  function deriveStatus2(bundle, items) {
15334
15721
  const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
@@ -15357,7 +15744,7 @@ function notFound2(res, slug) {
15357
15744
  }
15358
15745
  function createProjectBundlesRouter(projectsDir, broadcast) {
15359
15746
  void broadcast;
15360
- const router = Router16({ mergeParams: true });
15747
+ const router = Router17({ mergeParams: true });
15361
15748
  function validateProjectId(req, res, next) {
15362
15749
  const slug = getProjectIdParam2(req.params.projectId);
15363
15750
  if (!slug || !isValidSlug(slug)) {
@@ -15370,7 +15757,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
15370
15757
  router.get("/", async (req, res) => {
15371
15758
  try {
15372
15759
  const slug = getProjectIdParam2(req.params.projectId);
15373
- const projectMd = resolve26(projectsDir, slug, "project.md");
15760
+ const projectMd = resolve27(projectsDir, slug, "project.md");
15374
15761
  if (!await fileExists(projectMd)) {
15375
15762
  notFound2(res, slug);
15376
15763
  return;
@@ -15391,7 +15778,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
15391
15778
  init_config2();
15392
15779
  init_api();
15393
15780
  init_scanner();
15394
- import { Router as Router17 } from "express";
15781
+ import { Router as Router18 } from "express";
15395
15782
 
15396
15783
  // src/utils/github-backup.ts
15397
15784
  init_paths();
@@ -15399,8 +15786,8 @@ init_fs();
15399
15786
  init_config2();
15400
15787
  import { execFile as execFile2 } from "child_process";
15401
15788
  import { promisify as promisify2 } from "util";
15402
- import { cp, mkdtemp, rm as rm3, readFile as readFile20, writeFile as writeFile5, unlink as unlink5, stat, open as open2, rename as rename7 } from "fs/promises";
15403
- import { resolve as resolve27, join as join3 } from "path";
15789
+ import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile20, writeFile as writeFile6, unlink as unlink6, stat as stat2, open as open2, rename as rename8 } from "fs/promises";
15790
+ import { resolve as resolve28, join as join3 } from "path";
15404
15791
  import { tmpdir } from "os";
15405
15792
  var exec2 = promisify2(execFile2);
15406
15793
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -15440,7 +15827,7 @@ async function resolveCategoryPath(category) {
15440
15827
  case "servers":
15441
15828
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
15442
15829
  case "config":
15443
- return { sourcePath: resolve27(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
15830
+ return { sourcePath: resolve28(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
15444
15831
  }
15445
15832
  }
15446
15833
  async function checkGitInstalled() {
@@ -15451,7 +15838,7 @@ async function checkGitInstalled() {
15451
15838
  }
15452
15839
  }
15453
15840
  async function acquireLock() {
15454
- const lockPath = resolve27(syntaurRoot(), LOCK_FILE_NAME);
15841
+ const lockPath = resolve28(syntaurRoot(), LOCK_FILE_NAME);
15455
15842
  await ensureDir(syntaurRoot());
15456
15843
  try {
15457
15844
  const handle = await open2(lockPath, "wx");
@@ -15470,7 +15857,7 @@ async function acquireLock() {
15470
15857
  }
15471
15858
  async function releaseLock(lockPath) {
15472
15859
  try {
15473
- await unlink5(lockPath);
15860
+ await unlink6(lockPath);
15474
15861
  } catch {
15475
15862
  }
15476
15863
  }
@@ -15493,13 +15880,13 @@ async function cloneOrInit(repoUrl, destDir) {
15493
15880
  }
15494
15881
  async function copyRecursive(src, dest) {
15495
15882
  if (!await fileExists(src)) return;
15496
- const s = await stat(src);
15883
+ const s = await stat2(src);
15497
15884
  if (s.isDirectory()) {
15498
15885
  await ensureDir(dest);
15499
- await cp(src, dest, { recursive: true, force: true });
15886
+ await cp2(src, dest, { recursive: true, force: true });
15500
15887
  } else {
15501
- await ensureDir(resolve27(dest, ".."));
15502
- await cp(src, dest, { force: true });
15888
+ await ensureDir(resolve28(dest, ".."));
15889
+ await cp2(src, dest, { force: true });
15503
15890
  }
15504
15891
  }
15505
15892
  function resolveCategoriesStrict(csv) {
@@ -15536,9 +15923,9 @@ async function backupToGithub(overrides) {
15536
15923
  const { sourcePath, repoPath, isFile } = await resolveCategoryPath(category);
15537
15924
  const destPath = join3(tmpDir, repoPath);
15538
15925
  if (isFile) {
15539
- await rm3(destPath, { force: true });
15926
+ await rm4(destPath, { force: true });
15540
15927
  } else {
15541
- await rm3(destPath, { recursive: true, force: true });
15928
+ await rm4(destPath, { recursive: true, force: true });
15542
15929
  }
15543
15930
  if (!await fileExists(sourcePath)) {
15544
15931
  console.warn(`Category "${category}": no local data at ${sourcePath}; backup will reflect deletion.`);
@@ -15546,8 +15933,8 @@ async function backupToGithub(overrides) {
15546
15933
  }
15547
15934
  if (category === "config") {
15548
15935
  const sanitized = await readSanitizedConfig(sourcePath);
15549
- await ensureDir(resolve27(destPath, ".."));
15550
- await writeFile5(destPath, sanitized, "utf-8");
15936
+ await ensureDir(resolve28(destPath, ".."));
15937
+ await writeFile6(destPath, sanitized, "utf-8");
15551
15938
  } else {
15552
15939
  await copyRecursive(sourcePath, destPath);
15553
15940
  }
@@ -15592,7 +15979,7 @@ async function backupToGithub(overrides) {
15592
15979
  };
15593
15980
  } finally {
15594
15981
  if (tmpDir) {
15595
- await rm3(tmpDir, { recursive: true, force: true }).catch(() => {
15982
+ await rm4(tmpDir, { recursive: true, force: true }).catch(() => {
15596
15983
  });
15597
15984
  }
15598
15985
  await releaseLock(lockPath);
@@ -15600,18 +15987,18 @@ async function backupToGithub(overrides) {
15600
15987
  }
15601
15988
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
15602
15989
  if (isFile) {
15603
- await ensureDir(resolve27(localPath, ".."));
15604
- await cp(repoSrcPath, localPath, { force: true });
15990
+ await ensureDir(resolve28(localPath, ".."));
15991
+ await cp2(repoSrcPath, localPath, { force: true });
15605
15992
  return;
15606
15993
  }
15607
15994
  const stagingPath = `${localPath}.syntaur-restore-staging`;
15608
15995
  const backupPath = `${localPath}.syntaur-restore-backup`;
15609
- await rm3(stagingPath, { recursive: true, force: true });
15996
+ await rm4(stagingPath, { recursive: true, force: true });
15610
15997
  const backupExistsBefore = await fileExists(backupPath);
15611
15998
  const localExistsBefore = await fileExists(localPath);
15612
15999
  if (backupExistsBefore) {
15613
16000
  if (!localExistsBefore) {
15614
- await rename7(backupPath, localPath);
16001
+ await rename8(backupPath, localPath);
15615
16002
  } else {
15616
16003
  throw new Error(
15617
16004
  `Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
@@ -15620,21 +16007,21 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
15620
16007
  }
15621
16008
  let localMovedAside = false;
15622
16009
  try {
15623
- await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
16010
+ await cp2(repoSrcPath, stagingPath, { recursive: true, force: true });
15624
16011
  const localExists = await fileExists(localPath);
15625
16012
  if (localExists) {
15626
- await rename7(localPath, backupPath);
16013
+ await rename8(localPath, backupPath);
15627
16014
  localMovedAside = true;
15628
16015
  }
15629
- await rename7(stagingPath, localPath);
15630
- await rm3(backupPath, { recursive: true, force: true }).catch(() => {
16016
+ await rename8(stagingPath, localPath);
16017
+ await rm4(backupPath, { recursive: true, force: true }).catch(() => {
15631
16018
  });
15632
16019
  } catch (err) {
15633
16020
  if (localMovedAside && await fileExists(backupPath)) {
15634
- await rename7(backupPath, localPath).catch(() => {
16021
+ await rename8(backupPath, localPath).catch(() => {
15635
16022
  });
15636
16023
  }
15637
- await rm3(stagingPath, { recursive: true, force: true }).catch(() => {
16024
+ await rm4(stagingPath, { recursive: true, force: true }).catch(() => {
15638
16025
  });
15639
16026
  throw err;
15640
16027
  }
@@ -15693,7 +16080,7 @@ async function restoreFromGithub(overrides) {
15693
16080
  };
15694
16081
  } finally {
15695
16082
  if (tmpDir) {
15696
- await rm3(tmpDir, { recursive: true, force: true }).catch(() => {
16083
+ await rm4(tmpDir, { recursive: true, force: true }).catch(() => {
15697
16084
  });
15698
16085
  }
15699
16086
  await releaseLock(lockPath);
@@ -15701,7 +16088,7 @@ async function restoreFromGithub(overrides) {
15701
16088
  }
15702
16089
  async function getBackupStatus() {
15703
16090
  const config = await readConfig();
15704
- const lockPath = resolve27(syntaurRoot(), LOCK_FILE_NAME);
16091
+ const lockPath = resolve28(syntaurRoot(), LOCK_FILE_NAME);
15705
16092
  const locked = await fileExists(lockPath);
15706
16093
  return {
15707
16094
  repo: config.backup?.repo ?? null,
@@ -15714,7 +16101,7 @@ async function getBackupStatus() {
15714
16101
 
15715
16102
  // src/dashboard/api-backup.ts
15716
16103
  function createBackupRouter() {
15717
- const router = Router17();
16104
+ const router = Router18();
15718
16105
  router.get("/", async (_req, res) => {
15719
16106
  try {
15720
16107
  const status = await getBackupStatus();
@@ -16049,7 +16436,7 @@ function createDashboardServer(options) {
16049
16436
  (async () => {
16050
16437
  try {
16051
16438
  const configResult = await migrateLegacyConfig(
16052
- resolve28(syntaurRoot(), "config.md")
16439
+ resolve29(syntaurRoot(), "config.md")
16053
16440
  );
16054
16441
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
16055
16442
  const summary = summarizeMigration(projectResult, configResult);
@@ -16150,8 +16537,8 @@ function createDashboardServer(options) {
16150
16537
  });
16151
16538
  app.put("/api/config/hotkeys", async (req, res) => {
16152
16539
  try {
16153
- const raw = req.body && typeof req.body === "object" ? req.body : {};
16154
- const incoming = raw.bindings;
16540
+ const raw2 = req.body && typeof req.body === "object" ? req.body : {};
16541
+ const incoming = raw2.bindings;
16155
16542
  if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) {
16156
16543
  res.status(400).json({ error: "bindings must be an object keyed by action kind" });
16157
16544
  return;
@@ -16567,14 +16954,14 @@ function createDashboardServer(options) {
16567
16954
  app.use("/api/backup", createBackupRouter());
16568
16955
  if (serveStaticUi && dashboardDistPath) {
16569
16956
  const sendOpts = { dotfiles: "allow" };
16570
- app.use("/assets", express.static(resolve28(dashboardDistPath, "assets"), sendOpts));
16957
+ app.use("/assets", express.static(resolve29(dashboardDistPath, "assets"), sendOpts));
16571
16958
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
16572
16959
  app.get("{*path}", async (req, res) => {
16573
16960
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
16574
16961
  res.status(404).json({ error: "Not Found" });
16575
16962
  return;
16576
16963
  }
16577
- const indexPath = resolve28(dashboardDistPath, "index.html");
16964
+ const indexPath = resolve29(dashboardDistPath, "index.html");
16578
16965
  if (!await fileExists(indexPath)) {
16579
16966
  res.status(503).send(
16580
16967
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -16598,7 +16985,7 @@ function createDashboardServer(options) {
16598
16985
  serversDir: serversDir2,
16599
16986
  playbooksDir: playbooksDir2,
16600
16987
  todosDir: todosDir2,
16601
- dbPath: resolve28(syntaurRoot(), "syntaur.db"),
16988
+ dbPath: resolve29(syntaurRoot(), "syntaur.db"),
16602
16989
  onMessage: broadcast
16603
16990
  });
16604
16991
  startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
@@ -16613,8 +17000,8 @@ function createDashboardServer(options) {
16613
17000
  }
16614
17001
  });
16615
17002
  server.listen(port, () => {
16616
- const portFile = resolve28(syntaurRoot(), "dashboard-port");
16617
- writeFile6(portFile, String(port), "utf-8").catch(() => {
17003
+ const portFile = resolve29(syntaurRoot(), "dashboard-port");
17004
+ writeFile7(portFile, String(port), "utf-8").catch(() => {
16618
17005
  });
16619
17006
  resolvePromise();
16620
17007
  });
@@ -16632,8 +17019,8 @@ function createDashboardServer(options) {
16632
17019
  client.terminate();
16633
17020
  }
16634
17021
  clients.clear();
16635
- const portFile = resolve28(syntaurRoot(), "dashboard-port");
16636
- await unlink6(portFile).catch(() => {
17022
+ const portFile = resolve29(syntaurRoot(), "dashboard-port");
17023
+ await unlink7(portFile).catch(() => {
16637
17024
  });
16638
17025
  server.closeAllConnections?.();
16639
17026
  return new Promise((resolvePromise) => {