@tekmidian/pai 0.16.0 → 0.17.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 (123) hide show
  1. package/README.md +11 -3
  2. package/dist/auto-route-DkIRampF.mjs +86 -0
  3. package/dist/auto-route-DkIRampF.mjs.map +1 -0
  4. package/dist/checkpoint-block-Bl0HD3U2.mjs.map +1 -1
  5. package/dist/checkpoint-block-C05sK_HK.mjs +1056 -0
  6. package/dist/checkpoint-block-C05sK_HK.mjs.map +1 -0
  7. package/dist/checkpoint-block-CUCG10Uh.mjs +1022 -0
  8. package/dist/checkpoint-block-CUCG10Uh.mjs.map +1 -0
  9. package/dist/checkpoint-block-D5IyLLTr.mjs +1062 -0
  10. package/dist/checkpoint-block-D5IyLLTr.mjs.map +1 -0
  11. package/dist/cli/index.mjs +6 -6
  12. package/dist/cli/program.mjs +7 -7
  13. package/dist/cli/program.mjs.map +1 -1
  14. package/dist/daemon/index.mjs +9 -9
  15. package/dist/daemon-9AL6Pwh-.mjs +1581 -0
  16. package/dist/daemon-9AL6Pwh-.mjs.map +1 -0
  17. package/dist/daemon-Bbk-_rV6.mjs +1581 -0
  18. package/dist/daemon-Bbk-_rV6.mjs.map +1 -0
  19. package/dist/daemon-BvcaMk4g.mjs +1581 -0
  20. package/dist/daemon-BvcaMk4g.mjs.map +1 -0
  21. package/dist/daemon-CTDEfwcq.mjs +1576 -0
  22. package/dist/daemon-CTDEfwcq.mjs.map +1 -0
  23. package/dist/daemon-ClbfaF60.mjs +1576 -0
  24. package/dist/daemon-ClbfaF60.mjs.map +1 -0
  25. package/dist/daemon-DDQNDgYF.mjs +1576 -0
  26. package/dist/daemon-DDQNDgYF.mjs.map +1 -0
  27. package/dist/daemon-DU8yDWlQ.mjs +1576 -0
  28. package/dist/daemon-DU8yDWlQ.mjs.map +1 -0
  29. package/dist/daemon-NJr4iSW3.mjs +1576 -0
  30. package/dist/daemon-NJr4iSW3.mjs.map +1 -0
  31. package/dist/detector-Bwzm3E_x.mjs +74 -0
  32. package/dist/detector-Bwzm3E_x.mjs.map +1 -0
  33. package/dist/embeddings-Bn86ssxR.mjs +119 -0
  34. package/dist/embeddings-Bn86ssxR.mjs.map +1 -0
  35. package/dist/factory-0-57Ac-7.mjs +72 -0
  36. package/dist/factory-0-57Ac-7.mjs.map +1 -0
  37. package/dist/factory-PDXQdkLO.mjs +72 -0
  38. package/dist/factory-PDXQdkLO.mjs.map +1 -0
  39. package/dist/hooks/context-compression-hook.mjs +36 -4
  40. package/dist/hooks/context-compression-hook.mjs.map +3 -3
  41. package/dist/hooks/load-project-context.mjs +188 -6
  42. package/dist/hooks/load-project-context.mjs.map +4 -4
  43. package/dist/hooks/stop-hook.mjs +36 -4
  44. package/dist/hooks/stop-hook.mjs.map +3 -3
  45. package/dist/index.mjs +3 -3
  46. package/dist/indexer-backend-BmaS2VcD.mjs +299 -0
  47. package/dist/indexer-backend-BmaS2VcD.mjs.map +1 -0
  48. package/dist/indexer-backend-COrJ5rjS.mjs +299 -0
  49. package/dist/indexer-backend-COrJ5rjS.mjs.map +1 -0
  50. package/dist/indexer-backend-NwD7TBGh.mjs +301 -0
  51. package/dist/indexer-backend-NwD7TBGh.mjs.map +1 -0
  52. package/dist/latent-ideas-BwM3WdBp.mjs +191 -0
  53. package/dist/latent-ideas-BwM3WdBp.mjs.map +1 -0
  54. package/dist/neighborhood-DZY0wkpE.mjs +135 -0
  55. package/dist/neighborhood-DZY0wkpE.mjs.map +1 -0
  56. package/dist/pick-B0q5cy79.mjs +11785 -0
  57. package/dist/pick-B0q5cy79.mjs.map +1 -0
  58. package/dist/pick-BHE9_iA0.mjs +11947 -0
  59. package/dist/pick-BHE9_iA0.mjs.map +1 -0
  60. package/dist/pick-BaTjoNqM.mjs +11544 -0
  61. package/dist/pick-BaTjoNqM.mjs.map +1 -0
  62. package/dist/pick-C6JbZfN4.mjs +11893 -0
  63. package/dist/pick-C6JbZfN4.mjs.map +1 -0
  64. package/dist/pick-CK-bJyjp.mjs +11883 -0
  65. package/dist/pick-CK-bJyjp.mjs.map +1 -0
  66. package/dist/pick-CLBkGHyI.mjs +11826 -0
  67. package/dist/pick-CLBkGHyI.mjs.map +1 -0
  68. package/dist/pick-CTOif21h.mjs +11544 -0
  69. package/dist/pick-CTOif21h.mjs.map +1 -0
  70. package/dist/pick-Ck2cdQgD.mjs +11893 -0
  71. package/dist/pick-Ck2cdQgD.mjs.map +1 -0
  72. package/dist/pick-Cr4nJzHA.mjs +11914 -0
  73. package/dist/pick-Cr4nJzHA.mjs.map +1 -0
  74. package/dist/pick-D4dFfKAu.mjs +11893 -0
  75. package/dist/pick-D4dFfKAu.mjs.map +1 -0
  76. package/dist/pick-D7-lYiGX.mjs +11543 -0
  77. package/dist/pick-D7-lYiGX.mjs.map +1 -0
  78. package/dist/pick-DMjCY0np.mjs +11875 -0
  79. package/dist/pick-DMjCY0np.mjs.map +1 -0
  80. package/dist/pick-DbX4OrCP.mjs +11543 -0
  81. package/dist/pick-DbX4OrCP.mjs.map +1 -0
  82. package/dist/pick-iCiPjlnC.mjs +11893 -0
  83. package/dist/pick-iCiPjlnC.mjs.map +1 -0
  84. package/dist/pick-sF9GnQUS.mjs +11893 -0
  85. package/dist/pick-sF9GnQUS.mjs.map +1 -0
  86. package/dist/pick-yHKUrnfG.mjs +11793 -0
  87. package/dist/pick-yHKUrnfG.mjs.map +1 -0
  88. package/dist/postgres-BJxiqtqg.mjs +891 -0
  89. package/dist/postgres-BJxiqtqg.mjs.map +1 -0
  90. package/dist/search-CpTv1I24.mjs +298 -0
  91. package/dist/search-CpTv1I24.mjs.map +1 -0
  92. package/dist/skills/Pause/SKILL.md +11 -4
  93. package/dist/sqlite-CZ0LFili.mjs +271 -0
  94. package/dist/sqlite-CZ0LFili.mjs.map +1 -0
  95. package/dist/sqlite-Nw8ZW5Rj.mjs +271 -0
  96. package/dist/sqlite-Nw8ZW5Rj.mjs.map +1 -0
  97. package/dist/sync--BoxBBok.mjs +310 -0
  98. package/dist/sync--BoxBBok.mjs.map +1 -0
  99. package/dist/themes-BBOlGXAg.mjs +148 -0
  100. package/dist/themes-BBOlGXAg.mjs.map +1 -0
  101. package/dist/tools-B6BmaAvz.mjs +1939 -0
  102. package/dist/tools-B6BmaAvz.mjs.map +1 -0
  103. package/dist/tools-CAIY8TAb.mjs +1939 -0
  104. package/dist/tools-CAIY8TAb.mjs.map +1 -0
  105. package/dist/work-queue-worker-BoHIH70K.mjs +1856 -0
  106. package/dist/work-queue-worker-BoHIH70K.mjs.map +1 -0
  107. package/dist/work-queue-worker-CZGcQoLw.mjs +1856 -0
  108. package/dist/work-queue-worker-CZGcQoLw.mjs.map +1 -0
  109. package/dist/work-queue-worker-D05Ze9e9.mjs +1856 -0
  110. package/dist/work-queue-worker-D05Ze9e9.mjs.map +1 -0
  111. package/dist/work-queue-worker-DdXFINfH.mjs +1856 -0
  112. package/dist/work-queue-worker-DdXFINfH.mjs.map +1 -0
  113. package/dist/zettelkasten-D0N_A_OE.mjs +1063 -0
  114. package/dist/zettelkasten-D0N_A_OE.mjs.map +1 -0
  115. package/docs/commands/README.md +2 -1
  116. package/docs/commands/pause.md +1 -1
  117. package/docs/commands/session.md +27 -3
  118. package/package.json +1 -1
  119. package/plugins/context-preservation/hooks/hooks.json +12 -1
  120. package/scripts/build-hooks.mjs +1 -0
  121. package/src/hooks/session-autosave.sh +69 -0
  122. package/src/hooks/session-stop.sh +32 -1
  123. package/src/hooks/ts/session-start/load-project-context.ts +44 -0
@@ -0,0 +1,72 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
+
3
+ //#region src/storage/factory.ts
4
+ var factory_exports = /* @__PURE__ */ __exportAll({ createStorageBackend: () => createStorageBackend });
5
+ /** Backoff schedule (ms) for the bounded CLI retry path. */
6
+ const CLI_RETRY_DELAYS_MS = [
7
+ 500,
8
+ 1e3,
9
+ 2e3
10
+ ];
11
+ /** Backoff cap (ms) for the daemon's infinite retry path. */
12
+ const DAEMON_RETRY_CAP_MS = 15e3;
13
+ /**
14
+ * Create and return the configured StorageBackend.
15
+ *
16
+ * Auto-behaviour:
17
+ * - storageBackend = "sqlite" → SQLiteBackend always
18
+ * - storageBackend = "postgres" → PostgresBackend (retried; never falls back)
19
+ */
20
+ async function createStorageBackend(config, opts = {}) {
21
+ if (config.storageBackend === "postgres") return await connectPostgres(config, opts.waitForPostgres ?? false);
22
+ return createSQLiteBackend();
23
+ }
24
+ /**
25
+ * Attempt a single Postgres connection (ensure DB + test). Returns the live
26
+ * backend on success, or an error string describing why it failed.
27
+ */
28
+ async function attemptPostgres(config) {
29
+ const { PostgresBackend } = await import("./postgres-BJxiqtqg.mjs");
30
+ const pgConfig = config.postgres ?? {};
31
+ let backend = null;
32
+ try {
33
+ await PostgresBackend.ensureDatabase(pgConfig);
34
+ backend = new PostgresBackend(pgConfig);
35
+ const err = await backend.testConnection();
36
+ if (err) {
37
+ await backend.close().catch(() => {});
38
+ return { error: err };
39
+ }
40
+ return { backend };
41
+ } catch (e) {
42
+ if (backend) await backend.close().catch(() => {});
43
+ return { error: e instanceof Error ? e.message : String(e) };
44
+ }
45
+ }
46
+ async function connectPostgres(config, waitForever) {
47
+ let attempt = 0;
48
+ let lastError = "unknown error";
49
+ while (true) {
50
+ attempt++;
51
+ const result = await attemptPostgres(config);
52
+ if ("backend" in result) {
53
+ if (attempt > 1) process.stderr.write(`[pai-daemon] Connected to PostgreSQL backend (after ${attempt} attempts).\n`);
54
+ else process.stderr.write("[pai-daemon] Connected to PostgreSQL backend.\n");
55
+ return result.backend;
56
+ }
57
+ lastError = result.error;
58
+ if (!waitForever && attempt > CLI_RETRY_DELAYS_MS.length) throw new Error(`Postgres backend unreachable after ${attempt} attempts: ${lastError}. Is Docker Desktop / Postgres running? Refusing to fall back to SQLite (would split the corpus). Start Postgres and retry.`);
59
+ const delayMs = waitForever ? Math.min(DAEMON_RETRY_CAP_MS, 1e3 * 2 ** Math.min(attempt - 1, 4)) : CLI_RETRY_DELAYS_MS[attempt - 1];
60
+ process.stderr.write(`[pai-daemon] Postgres unavailable (${lastError}). Retry ${attempt}${waitForever ? "" : `/${CLI_RETRY_DELAYS_MS.length + 1}`} in ${delayMs}ms...\n`);
61
+ await new Promise((r) => setTimeout(r, delayMs));
62
+ }
63
+ }
64
+ async function createSQLiteBackend() {
65
+ const { openFederation } = await import("./db-CYmBWcjh.mjs").then((n) => n.t);
66
+ const { SQLiteBackend } = await import("./sqlite-Nw8ZW5Rj.mjs");
67
+ return new SQLiteBackend(openFederation());
68
+ }
69
+
70
+ //#endregion
71
+ export { factory_exports as n, createStorageBackend as t };
72
+ //# sourceMappingURL=factory-PDXQdkLO.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory-PDXQdkLO.mjs","names":[],"sources":["../src/storage/factory.ts"],"sourcesContent":["/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n *\n * When Postgres is the configured backend we NEVER silently fall back to\n * SQLite — doing so would split the corpus across two databases. Instead:\n * - Daemon (waitForPostgres: true) retries Postgres forever with capped\n * backoff until it comes up (handles the boot race where launchd starts\n * the daemon before Docker Desktop / Postgres is ready).\n * - CLI / one-shot callers (default) retry a few times, then throw a clear\n * error rather than returning a wrong/empty SQLite database.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\n\nexport interface StorageBackendOptions {\n /**\n * When true, retry Postgres indefinitely instead of giving up. Used by the\n * long-lived daemon so a not-yet-ready Postgres at boot is tolerated.\n * Defaults to false (one-shot CLI behaviour: bounded retries, then throw).\n */\n waitForPostgres?: boolean;\n}\n\n/** Backoff schedule (ms) for the bounded CLI retry path. */\nconst CLI_RETRY_DELAYS_MS = [500, 1_000, 2_000];\n\n/** Backoff cap (ms) for the daemon's infinite retry path. */\nconst DAEMON_RETRY_CAP_MS = 15_000;\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend (retried; never falls back)\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig,\n opts: StorageBackendOptions = {}\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await connectPostgres(config, opts.waitForPostgres ?? false);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\n/**\n * Attempt a single Postgres connection (ensure DB + test). Returns the live\n * backend on success, or an error string describing why it failed.\n */\nasync function attemptPostgres(\n config: PaiDaemonConfig\n): Promise<{ backend: StorageBackend } | { error: string }> {\n const { PostgresBackend } = await import(\"./postgres.js\");\n const pgConfig = config.postgres ?? {};\n\n let backend: InstanceType<typeof PostgresBackend> | null = null;\n try {\n // Ensure the per-user database exists and has the schema applied.\n await PostgresBackend.ensureDatabase(pgConfig);\n\n backend = new PostgresBackend(pgConfig);\n const err = await backend.testConnection();\n if (err) {\n await backend.close().catch(() => {});\n return { error: err };\n }\n return { backend };\n } catch (e) {\n if (backend) await backend.close().catch(() => {});\n return { error: e instanceof Error ? e.message : String(e) };\n }\n}\n\nasync function connectPostgres(\n config: PaiDaemonConfig,\n waitForever: boolean\n): Promise<StorageBackend> {\n let attempt = 0;\n let lastError = \"unknown error\";\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n attempt++;\n const result = await attemptPostgres(config);\n if (\"backend\" in result) {\n if (attempt > 1) {\n process.stderr.write(\n `[pai-daemon] Connected to PostgreSQL backend (after ${attempt} attempts).\\n`\n );\n } else {\n process.stderr.write(\"[pai-daemon] Connected to PostgreSQL backend.\\n\");\n }\n return result.backend;\n }\n\n lastError = result.error;\n\n if (!waitForever && attempt > CLI_RETRY_DELAYS_MS.length) {\n // Bounded CLI path exhausted — fail loudly, never silently use SQLite.\n throw new Error(\n `Postgres backend unreachable after ${attempt} attempts: ${lastError}. ` +\n `Is Docker Desktop / Postgres running? Refusing to fall back to SQLite ` +\n `(would split the corpus). Start Postgres and retry.`\n );\n }\n\n const delayMs = waitForever\n ? Math.min(DAEMON_RETRY_CAP_MS, 1_000 * 2 ** Math.min(attempt - 1, 4))\n : CLI_RETRY_DELAYS_MS[attempt - 1];\n\n process.stderr.write(\n `[pai-daemon] Postgres unavailable (${lastError}). ` +\n `Retry ${attempt}${waitForever ? \"\" : `/${CLI_RETRY_DELAYS_MS.length + 1}`} ` +\n `in ${delayMs}ms...\\n`\n );\n await new Promise((r) => setTimeout(r, delayMs));\n }\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const { openFederation } = await import(\"../memory/db.js\");\n const { SQLiteBackend } = await import(\"./sqlite.js\");\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n"],"mappings":";;;;;AA2BA,MAAM,sBAAsB;CAAC;CAAK;CAAO;CAAM;;AAG/C,MAAM,sBAAsB;;;;;;;;AAS5B,eAAsB,qBACpB,QACA,OAA8B,EAAE,EACP;AACzB,KAAI,OAAO,mBAAmB,WAC5B,QAAO,MAAM,gBAAgB,QAAQ,KAAK,mBAAmB,MAAM;AAIrE,QAAO,qBAAqB;;;;;;AAO9B,eAAe,gBACb,QAC0D;CAC1D,MAAM,EAAE,oBAAoB,MAAM,OAAO;CACzC,MAAM,WAAW,OAAO,YAAY,EAAE;CAEtC,IAAI,UAAuD;AAC3D,KAAI;AAEF,QAAM,gBAAgB,eAAe,SAAS;AAE9C,YAAU,IAAI,gBAAgB,SAAS;EACvC,MAAM,MAAM,MAAM,QAAQ,gBAAgB;AAC1C,MAAI,KAAK;AACP,SAAM,QAAQ,OAAO,CAAC,YAAY,GAAG;AACrC,UAAO,EAAE,OAAO,KAAK;;AAEvB,SAAO,EAAE,SAAS;UACX,GAAG;AACV,MAAI,QAAS,OAAM,QAAQ,OAAO,CAAC,YAAY,GAAG;AAClD,SAAO,EAAE,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,EAAE;;;AAIhE,eAAe,gBACb,QACA,aACyB;CACzB,IAAI,UAAU;CACd,IAAI,YAAY;AAGhB,QAAO,MAAM;AACX;EACA,MAAM,SAAS,MAAM,gBAAgB,OAAO;AAC5C,MAAI,aAAa,QAAQ;AACvB,OAAI,UAAU,EACZ,SAAQ,OAAO,MACb,uDAAuD,QAAQ,eAChE;OAED,SAAQ,OAAO,MAAM,kDAAkD;AAEzE,UAAO,OAAO;;AAGhB,cAAY,OAAO;AAEnB,MAAI,CAAC,eAAe,UAAU,oBAAoB,OAEhD,OAAM,IAAI,MACR,sCAAsC,QAAQ,aAAa,UAAU,6HAGtE;EAGH,MAAM,UAAU,cACZ,KAAK,IAAI,qBAAqB,MAAQ,KAAK,KAAK,IAAI,UAAU,GAAG,EAAE,CAAC,GACpE,oBAAoB,UAAU;AAElC,UAAQ,OAAO,MACb,sCAAsC,UAAU,WACrC,UAAU,cAAc,KAAK,IAAI,oBAAoB,SAAS,IAAI,MACrE,QAAQ,SACjB;AACD,QAAM,IAAI,SAAS,MAAM,WAAW,GAAG,QAAQ,CAAC;;;AAIpD,eAAe,sBAA+C;CAC5D,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cADA,gBAAgB,CACC"}
@@ -490,13 +490,13 @@ var BOILERPLATE_PATTERNS = [
490
490
  /^>\s*Working directory:/,
491
491
  /^>\s*Resume with:/,
492
492
  /^>\s*_No checkpoint body was recorded/,
493
- /^>?\s*$/,
494
493
  /^-{3,}$/
495
494
  ];
495
+ var BLANK_LINE = /^>?\s*$/;
496
496
  function isBoilerplateOnly(lines) {
497
497
  return lines.every((line) => {
498
498
  const t = line.trim();
499
- return BOILERPLATE_PATTERNS.some((re) => re.test(t));
499
+ return BLANK_LINE.test(t) || BOILERPLATE_PATTERNS.some((re) => re.test(t));
500
500
  });
501
501
  }
502
502
  function extractSectionContent(lines) {
@@ -506,7 +506,25 @@ function extractSectionContent(lines) {
506
506
  if (BOILERPLATE_PATTERNS.some((re) => re.test(t))) continue;
507
507
  kept.push(line);
508
508
  }
509
- return kept.join("\n").trim();
509
+ return trimBlankEdges(collapseBlankRuns(kept)).join("\n");
510
+ }
511
+ function trimBlankEdges(lines) {
512
+ let start = 0;
513
+ let end = lines.length;
514
+ while (start < end && BLANK_LINE.test(lines[start].trim())) start += 1;
515
+ while (end > start && BLANK_LINE.test(lines[end - 1].trim())) end -= 1;
516
+ return lines.slice(start, end);
517
+ }
518
+ function collapseBlankRuns(lines) {
519
+ const out = [];
520
+ let lastWasBlank = false;
521
+ for (const line of lines) {
522
+ const isBlank = BLANK_LINE.test(line.trim());
523
+ if (isBlank && lastWasBlank) continue;
524
+ out.push(line);
525
+ lastWasBlank = isBlank;
526
+ }
527
+ return out;
510
528
  }
511
529
  function locateContinue(content) {
512
530
  const lines = content.split("\n");
@@ -609,6 +627,12 @@ function buildContinueBlock(opts) {
609
627
  parts.push("", MARKER_CLOSE, "", "---", "");
610
628
  return parts.join("\n");
611
629
  }
630
+ function isSameSession(meta, opts) {
631
+ if (meta.sessionId && opts.sessionId) {
632
+ return meta.sessionId === opts.sessionId;
633
+ }
634
+ return meta.session === opts.sessionLine;
635
+ }
612
636
  function applyContinue(opts) {
613
637
  const target = resolveTodoTarget(opts.rootPath, { create: !opts.dryRun });
614
638
  if (!target) {
@@ -623,7 +647,7 @@ function applyContinue(opts) {
623
647
  let carriedForward = false;
624
648
  let effectiveBody = opts.body;
625
649
  if (opts.authored === "auto" && existing) {
626
- if (existing.meta?.authored === "model" && existing.meta.session === opts.sessionLine) {
650
+ if (existing.meta?.authored === "model" && isSameSession(existing.meta, opts)) {
627
651
  return {
628
652
  action: "preserved",
629
653
  path: target.path,
@@ -631,6 +655,14 @@ function applyContinue(opts) {
631
655
  preservedMeta: existing.meta
632
656
  };
633
657
  }
658
+ if (existing.meta && !(opts.body ?? "").trim() && !isBoilerplateOnly(existing.lines)) {
659
+ return {
660
+ action: "preserved",
661
+ path: target.path,
662
+ block: buildContinueBlock(opts),
663
+ preservedMeta: existing.meta ?? void 0
664
+ };
665
+ }
634
666
  if (!existing.meta && !isBoilerplateOnly(existing.lines)) {
635
667
  const salvaged = extractSectionContent(existing.lines);
636
668
  if (salvaged) {