@rafiqui987/markode 0.2.7 → 0.3.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 (110) hide show
  1. package/README.md +239 -108
  2. package/dist/cli.js +1289 -639
  3. package/dist/web/assets/{abnfDiagram-WEYOIA3C-DpbJC7Xp.js → abnfDiagram-WEYOIA3C-uyv195L6.js} +1 -1
  4. package/dist/web/assets/{arc-DC4i8MWg.js → arc-CnHtXLAe.js} +1 -1
  5. package/dist/web/assets/{architecture-WOLXFQ4H-eBViYpWx.js → architecture-WOLXFQ4H-tPJ2P-p7.js} +1 -1
  6. package/dist/web/assets/{architectureDiagram-ZHVYKT53-BUdoz7It.js → architectureDiagram-ZHVYKT53-C-WCNTHv.js} +1 -1
  7. package/dist/web/assets/{blockDiagram-RJAE7QA3-g9DAd_oM.js → blockDiagram-RJAE7QA3-DRgAHQJ9.js} +1 -1
  8. package/dist/web/assets/{c4Diagram-XDPPPE54-DRNtmahu.js → c4Diagram-XDPPPE54-DBBsZpdC.js} +1 -1
  9. package/dist/web/assets/channel-COq7V7WB.js +1 -0
  10. package/dist/web/assets/{chunk-2E7WHVMD-pLVU_fk3.js → chunk-2E7WHVMD-CQSFul1o.js} +1 -1
  11. package/dist/web/assets/{chunk-4EMNKR2T-CILPvkQp.js → chunk-4EMNKR2T-DHeQ05G5.js} +1 -1
  12. package/dist/web/assets/{chunk-55XCU76K-D3MGCCrv.js → chunk-55XCU76K-C9qNkR8H.js} +1 -1
  13. package/dist/web/assets/{chunk-7S6XSTFO-CN1GsbIT.js → chunk-7S6XSTFO-C_2BTQ4f.js} +1 -1
  14. package/dist/web/assets/{chunk-AQDTSKZB-CUWKsFsd.js → chunk-AQDTSKZB-DZLt6T8h.js} +1 -1
  15. package/dist/web/assets/{chunk-DSNLSHJZ-CwfiRg01.js → chunk-DSNLSHJZ-6EnWiUHw.js} +1 -1
  16. package/dist/web/assets/{chunk-HAGFOTED-Do-8YoBu.js → chunk-HAGFOTED-C4TrjvWC.js} +1 -1
  17. package/dist/web/assets/{chunk-HNE4Q36P-1-JjBgqP.js → chunk-HNE4Q36P-Bhz3xzgF.js} +1 -1
  18. package/dist/web/assets/{chunk-NQOKRVIF-BEHeLMtz.js → chunk-NQOKRVIF-fn9o0p_a.js} +1 -1
  19. package/dist/web/assets/{chunk-UHW7PFBP-CC1DKtp5.js → chunk-UHW7PFBP-y_Cnbpso.js} +1 -1
  20. package/dist/web/assets/{chunk-VQ5W62QI-CWY0eXsE.js → chunk-VQ5W62QI-BDGHstYR.js} +1 -1
  21. package/dist/web/assets/{chunk-WQQL4CDK-BJl_ufDd.js → chunk-WQQL4CDK-DVgns4LS.js} +1 -1
  22. package/dist/web/assets/{chunk-XXDRQBXY-DKVbKHOf.js → chunk-XXDRQBXY-z-xFeaXQ.js} +1 -1
  23. package/dist/web/assets/{chunk-YZO67HFG-dhkHuKWt.js → chunk-YZO67HFG-O3X4dDhT.js} +1 -1
  24. package/dist/web/assets/{chunk-ZAUX2LTX-CA6m357j.js → chunk-ZAUX2LTX-dMpe16to.js} +1 -1
  25. package/dist/web/assets/{chunk-ZCHMQSOR-DUBbqrP7.js → chunk-ZCHMQSOR-DmywVAUg.js} +1 -1
  26. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-PXy4ulRK.js → classDiagram-v2-4MYWR3UA-DNFxIeoH.js} +1 -1
  27. package/dist/web/assets/{cose-bilkent-JH36ORCC-BDYEjy3u.js → cose-bilkent-JH36ORCC-C78U5xAn.js} +1 -1
  28. package/dist/web/assets/{cynefin-EF2NZ3EQ-BOMUQs4F.js → cynefin-EF2NZ3EQ-53hzJKH9.js} +1 -1
  29. package/dist/web/assets/{cynefinDiagram-WF24UDRP-cX0NpQ7L.js → cynefinDiagram-WF24UDRP-D7qakSAO.js} +1 -1
  30. package/dist/web/assets/{dagre-NK4GIZVX-BAP6Gj-L.js → dagre-NK4GIZVX-JnnDdmH0.js} +1 -1
  31. package/dist/web/assets/{diagram-CBK3MWTC-B5AONjPA.js → diagram-CBK3MWTC-C9dCT7nt.js} +1 -1
  32. package/dist/web/assets/{diagram-I5XIIV2D-BkwhoH5d.js → diagram-I5XIIV2D-BODrA_Tz.js} +1 -1
  33. package/dist/web/assets/{diagram-ODIL2UCW-DUFps_5l.js → diagram-ODIL2UCW-ByZtzdTi.js} +1 -1
  34. package/dist/web/assets/{diagram-PFBEJUXE-Dj94WcA4.js → diagram-PFBEJUXE-Dc3cgafk.js} +1 -1
  35. package/dist/web/assets/{diagram-RJA3I4VV-BRhr049b.js → diagram-RJA3I4VV-BnTNuzvk.js} +1 -1
  36. package/dist/web/assets/{diagram-TYUNFSNR-C9EtKuKD.js → diagram-TYUNFSNR-fbw92djE.js} +1 -1
  37. package/dist/web/assets/{dist-DTXSqpYP.js → dist-8bzcdWlP.js} +1 -1
  38. package/dist/web/assets/{dist-DMEslBFn.js → dist-BPKiY16E.js} +1 -1
  39. package/dist/web/assets/{dist-6_cnylgs.js → dist-BeUBXLX5.js} +1 -1
  40. package/dist/web/assets/{dist-SlIdOLRu.js → dist-BfedOYsq.js} +1 -1
  41. package/dist/web/assets/{dist-uYDRxuA0.js → dist-BruzXrfs.js} +1 -1
  42. package/dist/web/assets/{dist-CXAToOJL.js → dist-BwF8pAVO.js} +1 -1
  43. package/dist/web/assets/{dist-DfKlgEia.js → dist-CAttduN3.js} +1 -1
  44. package/dist/web/assets/{dist-CRKS1wXG.js → dist-CC7LN-Hh.js} +1 -1
  45. package/dist/web/assets/{dist-B-4tAY5T.js → dist-CFUj2tCy.js} +2 -2
  46. package/dist/web/assets/{dist-C6oRdq_x.js → dist-CM1-Tp30.js} +1 -1
  47. package/dist/web/assets/{dist-B91b1nu8.js → dist-CfeLR_Q4.js} +1 -1
  48. package/dist/web/assets/{dist-BhvCwF6v.js → dist-CrUcXP5H.js} +1 -1
  49. package/dist/web/assets/{dist-D5_2d6Ki.js → dist-CubY9gnU.js} +1 -1
  50. package/dist/web/assets/{dist-CxtEXJtW.js → dist-D0R-qVeY.js} +1 -1
  51. package/dist/web/assets/{dist-CUajnf5G.js → dist-D5EOvBuF.js} +1 -1
  52. package/dist/web/assets/{dist-C7gsSbg8.js → dist-DcGC8X1E.js} +1 -1
  53. package/dist/web/assets/{dist-CMFTG-Cz.js → dist-EljmWMub.js} +1 -1
  54. package/dist/web/assets/{dist-hiB1k3yb.js → dist-ga2OvaLo.js} +1 -1
  55. package/dist/web/assets/{dist-BOIkyUDE.js → dist-tv7yScoP.js} +1 -1
  56. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-hUPVHW_L.js → ebnfDiagram-V4P3TTPT-DPsfv1uf.js} +1 -1
  57. package/dist/web/assets/{elk-QONTBCTQ-CK3_-qCI.js → elk-QONTBCTQ-DoINDZCB.js} +1 -1
  58. package/dist/web/assets/{erDiagram-KYQIGQAA-BnzshuRd.js → erDiagram-KYQIGQAA-ChPWyTRi.js} +1 -1
  59. package/dist/web/assets/{eventmodeling-K75KTNOO-YpRkYpNJ.js → eventmodeling-K75KTNOO-B1BhcpV6.js} +1 -1
  60. package/dist/web/assets/flowDiagram-IUGLLWBU-DZzMwlmj.js +1 -0
  61. package/dist/web/assets/{ganttDiagram-R7GPNTJY-Cof-TrXj.js → ganttDiagram-R7GPNTJY-BpChuBNI.js} +1 -1
  62. package/dist/web/assets/{gitGraph-VSP46ZUC-ClyrQrUV.js → gitGraph-VSP46ZUC-BhIFPres.js} +1 -1
  63. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-DcHBmgeW.js → gitGraphDiagram-PDYL7XHY-CHejLr2o.js} +1 -1
  64. package/dist/web/assets/index-CQGURl2V.css +1 -0
  65. package/dist/web/assets/index-CafleYLD.js +132 -0
  66. package/dist/web/assets/{info-OHQRW6UA-DJuVnR49.js → info-OHQRW6UA-p_XOG9kY.js} +1 -1
  67. package/dist/web/assets/{infoDiagram-MVDRKAX5-SPivATVi.js → infoDiagram-MVDRKAX5-BjEI8OE0.js} +1 -1
  68. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-luEQCi0p.js → ishikawaDiagram-HPEHV2GE-B_7LEH0q.js} +1 -1
  69. package/dist/web/assets/{journeyDiagram-BOSHW7O7-CDWNQ0FV.js → journeyDiagram-BOSHW7O7-DprR7PC1.js} +1 -1
  70. package/dist/web/assets/{kanban-definition-FNSA4HC5-kCg2Ntni.js → kanban-definition-FNSA4HC5-Dgn1OKTQ.js} +1 -1
  71. package/dist/web/assets/{line-Cs-2UOsh.js → line-CsP7ezWR.js} +1 -1
  72. package/dist/web/assets/{linear-Dn78ogyi.js → linear-DutM4-eX.js} +1 -1
  73. package/dist/web/assets/{mermaid-parser.core-XUq3kRg7.js → mermaid-parser.core-CRHYFOhU.js} +3 -3
  74. package/dist/web/assets/{mermaid.core-KP0iScKW.js → mermaid.core-C-ITLWC9.js} +4 -4
  75. package/dist/web/assets/{mindmap-definition-6OY57UG3-DYfhBOEV.js → mindmap-definition-6OY57UG3-B3gANyDJ.js} +1 -1
  76. package/dist/web/assets/{packet-JDAUHWVQ-m_mxaPh2.js → packet-JDAUHWVQ-0H1cA20q.js} +1 -1
  77. package/dist/web/assets/{pegDiagram-ECIUJNBX-C57XlI0z.js → pegDiagram-ECIUJNBX-BUxriBaM.js} +1 -1
  78. package/dist/web/assets/{pie-XZMESJXO-COU-cPpy.js → pie-XZMESJXO-CSOHv2zM.js} +1 -1
  79. package/dist/web/assets/{pieDiagram-GNMPY74T-Cgv_MwBq.js → pieDiagram-GNMPY74T-B7B_ijMh.js} +1 -1
  80. package/dist/web/assets/{quadrantDiagram-D57K4MC3-DLP-IJBg.js → quadrantDiagram-D57K4MC3-CuDX_hzm.js} +1 -1
  81. package/dist/web/assets/{radar-ABXABTNO-DVnhW8Tu.js → radar-ABXABTNO-Bhdhmyn3.js} +1 -1
  82. package/dist/web/assets/{railroad-I3PHUGI6-CbNbR4f3.js → railroad-I3PHUGI6-D8J0asfg.js} +1 -1
  83. package/dist/web/assets/{railroad-abnf-LNEFI6M7-QapD3MRY.js → railroad-abnf-LNEFI6M7-DSkwXAS5.js} +1 -1
  84. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-CSlvjrRs.js → railroad-ebnf-DWYD2UWJ-CUA5ZluT.js} +1 -1
  85. package/dist/web/assets/{railroad-peg-JR7BVNR7-CabNa_kN.js → railroad-peg-JR7BVNR7-DOmtcJJ_.js} +1 -1
  86. package/dist/web/assets/{railroadDiagram-Y72NRBL7-DnlOXd0I.js → railroadDiagram-Y72NRBL7-Bfa59bDk.js} +1 -1
  87. package/dist/web/assets/{requirementDiagram-EQYSW5RA-Dq1MJEdy.js → requirementDiagram-EQYSW5RA-TqWBSttg.js} +1 -1
  88. package/dist/web/assets/{sankeyDiagram-SY24JHO5--LVb7hhV.js → sankeyDiagram-SY24JHO5-DXW4vZ49.js} +1 -1
  89. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-Cg8W2Kc8.js → sequenceDiagram-BLEHNHQU-bR6akpDb.js} +1 -1
  90. package/dist/web/assets/{src-w1h_Rr-8.js → src-DWy4o1sh.js} +1 -1
  91. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-ck5PGLTp.js → stateDiagram-v2-A6IESKWZ-G-L9fY1U.js} +1 -1
  92. package/dist/web/assets/{swimlanes-B3ENRKFQ--RxIRIY6.js → swimlanes-B3ENRKFQ-Bvz0uaIY.js} +1 -1
  93. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-D8Xb8aOe.js +8 -0
  94. package/dist/web/assets/{timeline-definition-JXYKGLGB-Cgtckb20.js → timeline-definition-JXYKGLGB-CLuowMKW.js} +1 -1
  95. package/dist/web/assets/{treeView-D4JQ5SDB-Byi5cU2l.js → treeView-D4JQ5SDB-P9KKVCsV.js} +1 -1
  96. package/dist/web/assets/{treemap-SAJKECNS-DJ7_3_F8.js → treemap-SAJKECNS-CJHxQeBA.js} +1 -1
  97. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-DkTE13SV.js → usecaseDiagram-WPOLBNNX-Dp-yumhB.js} +1 -1
  98. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-C5G521q2.js → vennDiagram-Q3Y3IDCW-WYfAKKah.js} +1 -1
  99. package/dist/web/assets/{wardley-7MLQ67FV-PChC3W6T.js → wardley-7MLQ67FV-16kL0OjA.js} +1 -1
  100. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-B9sJfj7x.js → wardleyDiagram-IVAIDKWA-CKpO66Ll.js} +1 -1
  101. package/dist/web/assets/{xychartDiagram-4URNHDU4-CmuAuTkp.js → xychartDiagram-4URNHDU4-BAlKKoEI.js} +1 -1
  102. package/dist/web/index.html +3 -3
  103. package/package.json +4 -2
  104. package/templates/proposals/SKILL.md +14 -6
  105. package/templates/skill/SKILL.md +18 -0
  106. package/dist/web/assets/channel-B4Lj3Yy_.js +0 -1
  107. package/dist/web/assets/flowDiagram-IUGLLWBU-DA73lQLy.js +0 -1
  108. package/dist/web/assets/index-CFg9w5Ih.js +0 -63
  109. package/dist/web/assets/index-CT9Wzb6W.css +0 -1
  110. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-CeoYxgix.js +0 -8
package/dist/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import fs17 from "fs";
5
- import path17 from "path";
4
+ import fs20 from "fs";
5
+ import path20 from "path";
6
6
 
7
- // src/core/check.ts
8
- import fs8 from "fs";
9
- import path8 from "path";
7
+ // src/core/changes.ts
8
+ import crypto from "crypto";
9
+ import fs2 from "fs";
10
+ import path2 from "path";
10
11
 
11
12
  // src/core/config.ts
12
13
  import fs from "fs";
@@ -53,7 +54,10 @@ function missingMainDoc(dir) {
53
54
  }
54
55
  function linkMainDoc(dir) {
55
56
  const missing = missingMainDoc(dir);
56
- if (missing) fs.writeFileSync(path.join(dir, missing), POINTERS[missing]);
57
+ if (missing) {
58
+ assertInside(path.join(dir, missing));
59
+ fs.writeFileSync(path.join(dir, missing), POINTERS[missing]);
60
+ }
57
61
  return missing;
58
62
  }
59
63
  var linkAgentsDoc = (dir) => missingMainDoc(dir) === "CLAUDE.md" && !!linkMainDoc(dir);
@@ -71,18 +75,57 @@ function readConfig(root2) {
71
75
  try {
72
76
  const raw = JSON.parse(fs.readFileSync(configFile(root2), "utf8"));
73
77
  const langs = Array.isArray(raw.langs) ? raw.langs : raw.lang ? [raw.lang] : [];
74
- return { mainDoc: raw.mainDoc ?? null, langs: langs.filter(isLangCode), langNames: raw.langNames, workflow: raw.workflow, proposals: raw.proposals, skipLink: raw.skipLink === true || void 0, initializedAt: raw.initializedAt };
78
+ const mainDoc = MAIN_DOCS.includes(raw.mainDoc) ? raw.mainDoc : null;
79
+ const workflow = ["none", "markode", "openspec", "speckit"].includes(raw.workflow) ? raw.workflow : void 0;
80
+ const proposals = raw.proposals === "general" || raw.proposals === "structured" ? raw.proposals : void 0;
81
+ const langNames = raw.langNames && typeof raw.langNames === "object" ? Object.fromEntries(Object.entries(raw.langNames).filter(([k, v]) => isLangCode(k) && typeof v === "string").map(([k, v]) => [k, v.slice(0, 40)])) : void 0;
82
+ const flags = raw.features && typeof raw.features === "object" ? Object.fromEntries(["rules", "translations", "dashboard", "skills"].filter((k) => typeof raw.features[k] === "boolean").map((k) => [k, raw.features[k]])) : void 0;
83
+ const savedRules = Array.isArray(raw.savedRules) ? raw.savedRules.filter((id) => typeof id === "string" && /^[\w-]+$/.test(id)) : void 0;
84
+ return { mainDoc, langs: langs.filter(isLangCode), langNames, workflow, proposals, skipLink: raw.skipLink === true || void 0, features: flags, savedRules, savedSourceLang: isLang(raw.savedSourceLang) ? raw.savedSourceLang : void 0, initializedAt: typeof raw.initializedAt === "string" ? raw.initializedAt : void 0 };
75
85
  } catch {
76
86
  return null;
77
87
  }
78
88
  }
79
89
  function writeConfig(root2, config) {
90
+ assertInside(configFile(root2));
80
91
  fs.mkdirSync(stateDir(root2), { recursive: true });
81
92
  fs.writeFileSync(configFile(root2), JSON.stringify(config, null, 2) + "\n");
82
93
  }
94
+ var isInside = (abs, base) => abs === base || abs.startsWith(base + path.sep);
95
+ function realOf(abs) {
96
+ let probe = abs;
97
+ const rest = [];
98
+ for (; ; ) {
99
+ try {
100
+ fs.lstatSync(probe);
101
+ break;
102
+ } catch {
103
+ const up = path.dirname(probe);
104
+ if (up === probe) return null;
105
+ rest.unshift(path.basename(probe));
106
+ probe = up;
107
+ }
108
+ }
109
+ try {
110
+ return path.join(fs.realpathSync(probe), ...rest);
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+ var projectRoot = null;
116
+ function setProjectRoot(root2) {
117
+ projectRoot = fs.realpathSync(root2);
118
+ }
119
+ function assertInside(abs, root2 = projectRoot) {
120
+ if (!root2) return;
121
+ const base = fs.realpathSync(root2);
122
+ const real = realOf(abs);
123
+ if (!real || !isInside(real, base)) throw new Error(`Refusing to write outside the project (symlink?): ${abs}`);
124
+ }
83
125
  function safeResolve(root2, rel) {
84
126
  const abs = path.resolve(root2, rel);
85
- if (abs !== root2 && !abs.startsWith(root2 + path.sep)) throw new Error(`Path outside project: ${rel}`);
127
+ if (!isInside(abs, root2)) throw new Error(`Path outside project: ${rel}`);
128
+ assertInside(abs, root2);
86
129
  return abs;
87
130
  }
88
131
  var toPosix = (p3) => p3.split(path.sep).join("/");
@@ -102,28 +145,183 @@ function writeSettings(patch) {
102
145
  return next;
103
146
  }
104
147
 
148
+ // src/core/changes.ts
149
+ var logDir = (root2) => path2.join(GLOBAL_DIR, "projects", crypto.createHash("sha256").update(fs2.realpathSync(root2)).digest("hex").slice(0, 16));
150
+ var logFile = (root2) => path2.join(logDir(root2), "activity.jsonl");
151
+ var cursorFile = (root2) => path2.join(logDir(root2), "activity-cursor.json");
152
+ var MAX_LINES = 1e3;
153
+ var ENTRY_HEAD = /^<!-- (comment|reply) by ([^,\n]+), (\d{4}-\d{2}-\d{2})(?:, on "((?:[^"\\\n]|\\.)*)")?:\s?/;
154
+ var HEADING = /^#{1,6}\s+(.*?)\s*#*\s*$/;
155
+ var MAX_ENTRY = 2e4;
156
+ function allEntries(text3) {
157
+ const out = [];
158
+ for (let i = 0; ; ) {
159
+ const s = text3.indexOf("<!--", i);
160
+ const e = s < 0 ? -1 : text3.indexOf("-->", s + 4);
161
+ if (e < 0) return out;
162
+ i = e + 3;
163
+ if (e - s > MAX_ENTRY) continue;
164
+ const raw = text3.slice(s, i);
165
+ const m = ENTRY_HEAD.exec(raw);
166
+ if (m) out.push({ raw, index: s, author: m[2], quote: m[4] ?? "", text: raw.slice(m[0].length, -3).trim() });
167
+ }
168
+ }
169
+ var entries = (text3) => allEntries(text3).filter((e) => e.author !== "AI" && e.text);
170
+ function stripEntries(text3) {
171
+ let out = "";
172
+ let from = 0;
173
+ for (const e of allEntries(text3)) {
174
+ const end = e.index + e.raw.length;
175
+ const lineStart = text3.lastIndexOf("\n", e.index - 1) + 1;
176
+ const nl = text3.indexOf("\n", end);
177
+ const lineEnd = nl < 0 ? text3.length : nl;
178
+ const alone = !text3.slice(Math.max(lineStart, from), e.index).trim() && !text3.slice(end, lineEnd).trim();
179
+ if (alone && lineStart >= from) {
180
+ out += text3.slice(from, lineStart);
181
+ from = nl < 0 ? text3.length : nl + 1;
182
+ } else {
183
+ out += text3.slice(from, e.index);
184
+ from = end;
185
+ }
186
+ }
187
+ return out + text3.slice(from);
188
+ }
189
+ function headings(text3) {
190
+ const out = [];
191
+ let index = 0;
192
+ for (const line of text3.split("\n")) {
193
+ const h = HEADING.exec(line);
194
+ if (h) out.push({ index, title: h[1] });
195
+ index += line.length + 1;
196
+ }
197
+ return out;
198
+ }
199
+ function sectionAt(list, index) {
200
+ let lo = 0;
201
+ let hi = list.length - 1;
202
+ let found;
203
+ while (lo <= hi) {
204
+ const mid = lo + hi >> 1;
205
+ if (list[mid].index <= index) found = list[mid].title, lo = mid + 1;
206
+ else hi = mid - 1;
207
+ }
208
+ return found;
209
+ }
210
+ var isProposal = (text3) => /^type:\s*proposal\s*$/m.test(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text3)?.[1] ?? "");
211
+ function logSave(root2, rel, before, after) {
212
+ if (before === after || isProposal(after)) return;
213
+ const at = (/* @__PURE__ */ new Date()).toISOString();
214
+ const old = new Set(entries(before).map((e) => e.raw));
215
+ const sections = headings(after);
216
+ const out = entries(after).filter((e) => !old.has(e.raw)).map((e) => ({ at, path: rel, kind: "comment", section: sectionAt(sections, e.index), author: e.author, quote: e.quote || void 0, text: e.text }));
217
+ const a = stripEntries(before).split("\n");
218
+ const b = stripEntries(after).split("\n");
219
+ if (a.join("\n") !== b.join("\n")) {
220
+ let i = 0;
221
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
222
+ out.push({ at, path: rel, kind: "edit", section: sectionAt(headings(b.join("\n")), b.slice(0, i + 1).join("\n").length) });
223
+ }
224
+ if (!out.length) return;
225
+ const file = logFile(root2);
226
+ fs2.mkdirSync(logDir(root2), { recursive: true });
227
+ fs2.appendFileSync(file, out.map((e) => JSON.stringify(e)).join("\n") + "\n");
228
+ const text3 = fs2.readFileSync(file, "utf8");
229
+ const lines = text3.split("\n").filter(Boolean);
230
+ if (lines.length > MAX_LINES) {
231
+ const kept = lines.slice(-MAX_LINES / 2).join("\n") + "\n";
232
+ fs2.writeFileSync(file, kept);
233
+ const removed = Buffer.byteLength(text3) - Buffer.byteLength(kept);
234
+ const cursor = readCursor(root2);
235
+ if (cursor) writeCursor(root2, { reported: Math.max(0, cursor.reported - removed) });
236
+ }
237
+ }
238
+ function readCursor(root2) {
239
+ try {
240
+ const c = JSON.parse(fs2.readFileSync(cursorFile(root2), "utf8"));
241
+ return Number.isSafeInteger(c?.reported) && c.reported >= 0 ? { reported: c.reported } : null;
242
+ } catch {
243
+ return null;
244
+ }
245
+ }
246
+ function writeCursor(root2, c) {
247
+ fs2.mkdirSync(logDir(root2), { recursive: true });
248
+ fs2.writeFileSync(cursorFile(root2), JSON.stringify(c) + "\n");
249
+ }
250
+ var logSize = (root2) => fs2.existsSync(logFile(root2)) ? fs2.statSync(logFile(root2)).size : 0;
251
+ function startSession(root2) {
252
+ writeCursor(root2, { reported: logSize(root2) });
253
+ }
254
+ function takeChanges(root2) {
255
+ const size = logSize(root2);
256
+ const cursor = readCursor(root2);
257
+ const from = cursor && cursor.reported <= size ? cursor.reported : size;
258
+ if (from === size) {
259
+ if (!cursor) writeCursor(root2, { reported: size });
260
+ return [];
261
+ }
262
+ const fd = fs2.openSync(logFile(root2), "r");
263
+ const buf = Buffer.alloc(size - from);
264
+ fs2.readSync(fd, buf, 0, buf.length, from);
265
+ fs2.closeSync(fd);
266
+ writeCursor(root2, { reported: size });
267
+ return buf.toString("utf8").split("\n").filter(Boolean).flatMap((l) => {
268
+ try {
269
+ const e = JSON.parse(l);
270
+ const str = (v) => typeof v === "string" ? v.slice(0, 2e3) : void 0;
271
+ if (e.kind !== "edit" && e.kind !== "comment" || typeof e.path !== "string") return [];
272
+ return [{ at: str(e.at) ?? "", path: e.path.slice(0, 500), kind: e.kind, section: str(e.section), author: str(e.author), quote: str(e.quote), text: str(e.text) }];
273
+ } catch {
274
+ return [];
275
+ }
276
+ });
277
+ }
278
+ function formatChanges(list) {
279
+ if (!list.length) return "";
280
+ const edits = /* @__PURE__ */ new Map();
281
+ for (const e of list.filter((e2) => e2.kind === "edit")) {
282
+ const sections = edits.get(e.path) ?? /* @__PURE__ */ new Set();
283
+ if (e.section) sections.add(e.section);
284
+ edits.set(e.path, sections);
285
+ }
286
+ const comments = list.filter((e) => e.kind === "comment");
287
+ const lines = ['markode: the user changed docs while you worked (see "While you work" in the markode skill).'];
288
+ if (edits.size) {
289
+ lines.push("", "Edited (re-read before working on that part; nothing to do otherwise):");
290
+ for (const [p3, s] of edits) lines.push(`- ${p3}${s.size ? ` (${[...s].map((x) => `"${x}"`).join(", ")})` : ""}`);
291
+ }
292
+ if (comments.length) {
293
+ lines.push("", "New comments (about your current work: reply below it and apply it; otherwise ask the user in one line whether to take it now):");
294
+ for (const c of comments) lines.push(`- ${c.path}${c.section ? ` \u203A "${c.section}"` : ""}${c.quote ? `, on "${c.quote}"` : ""}: ${c.text} (${c.author})`);
295
+ }
296
+ return lines.join("\n");
297
+ }
298
+
299
+ // src/core/check.ts
300
+ import fs10 from "fs";
301
+ import path10 from "path";
302
+
105
303
  // src/core/dashboard.ts
106
- import fs3 from "fs";
107
- import path3 from "path";
304
+ import fs4 from "fs";
305
+ import path4 from "path";
108
306
  import YAML from "yaml";
109
307
 
110
308
  // src/core/scan.ts
111
- import fs2 from "fs";
112
- import path2 from "path";
309
+ import fs3 from "fs";
310
+ import path3 from "path";
113
311
  import ignore from "ignore";
114
312
  var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".markode", "dist", "build", ".next", ".turbo", "coverage", ".venv"]);
115
313
  var IGNORE_FILE = ".markodeignore";
116
314
  function loadIgnore(root2, file) {
117
315
  const ig = ignore();
118
316
  try {
119
- ig.add(fs2.readFileSync(path2.join(root2, file), "utf8"));
317
+ ig.add(fs3.readFileSync(path3.join(root2, file), "utf8"));
120
318
  } catch {
121
319
  }
122
320
  return ig;
123
321
  }
124
322
  function readIgnoreList(root2) {
125
323
  try {
126
- return fs2.readFileSync(path2.join(root2, IGNORE_FILE), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
324
+ return fs3.readFileSync(path3.join(root2, IGNORE_FILE), "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
127
325
  } catch {
128
326
  return [];
129
327
  }
@@ -132,7 +330,8 @@ function setIgnored(root2, rel, kind, ignored) {
132
330
  const entry = kind === "dir" ? `${rel}/` : rel;
133
331
  const lines = readIgnoreList(root2).filter((l) => l !== entry && l !== rel);
134
332
  if (ignored) lines.push(entry);
135
- fs2.writeFileSync(path2.join(root2, IGNORE_FILE), lines.length ? lines.join("\n") + "\n" : "");
333
+ assertInside(path3.join(root2, IGNORE_FILE));
334
+ fs3.writeFileSync(path3.join(root2, IGNORE_FILE), lines.length ? lines.join("\n") + "\n" : "");
136
335
  }
137
336
  function watchSkips(root2) {
138
337
  const gitIgnore = loadIgnore(root2, ".gitignore");
@@ -143,41 +342,46 @@ function watchSkips(root2) {
143
342
  return isDir2 !== true && gitIgnore.ignores(rel);
144
343
  };
145
344
  }
345
+ var isArchive = (rel) => {
346
+ const parts = rel.split("/");
347
+ return parts[parts.length - 1] === "applied" && parts.slice(0, -1).includes("proposals");
348
+ };
146
349
  function scanTree(root2) {
147
350
  const gitIgnore = loadIgnore(root2, ".gitignore");
148
351
  const mdIgnore = loadIgnore(root2, IGNORE_FILE);
149
- function walk(dirAbs, parentIgnored) {
150
- let entries;
352
+ function walk(dirAbs, parentIgnored, parentArchived) {
353
+ let entries2;
151
354
  try {
152
- entries = fs2.readdirSync(dirAbs, { withFileTypes: true });
355
+ entries2 = fs3.readdirSync(dirAbs, { withFileTypes: true });
153
356
  } catch {
154
357
  return [];
155
358
  }
156
359
  const nodes = [];
157
- for (const e of entries) {
360
+ for (const e of entries2) {
158
361
  if (ALWAYS_SKIP.has(e.name)) continue;
159
- const abs = path2.join(dirAbs, e.name);
160
- const rel = toPosix(path2.relative(root2, abs));
362
+ const abs = path3.join(dirAbs, e.name);
363
+ const rel = toPosix(path3.relative(root2, abs));
161
364
  if (e.isDirectory()) {
162
365
  if (gitIgnore.ignores(rel + "/")) continue;
163
- const ignored = parentIgnored || mdIgnore.ignores(rel + "/");
164
- const children = walk(abs, ignored);
366
+ const archived = parentArchived || isArchive(rel);
367
+ const ignored = parentIgnored || archived || mdIgnore.ignores(rel + "/");
368
+ const children = walk(abs, ignored, archived);
165
369
  if (!children.length && !isEmptyDir(abs)) continue;
166
370
  const project = MAIN_DOCS.some((d) => children.some((c) => c.kind === "file" && c.name === d));
167
- nodes.push({ name: e.name, path: rel, kind: "dir", project: project || void 0, ignored: ignored || void 0, children });
371
+ nodes.push({ name: e.name, path: rel, kind: "dir", project: project || void 0, ignored: ignored || void 0, archived: archived || void 0, children });
168
372
  } else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
169
373
  if (gitIgnore.ignores(rel)) continue;
170
374
  const ignored = parentIgnored || mdIgnore.ignores(rel);
171
- nodes.push({ name: e.name, path: rel, kind: "file", ignored: ignored || void 0 });
375
+ nodes.push({ name: e.name, path: rel, kind: "file", ignored: ignored || void 0, archived: parentArchived || void 0 });
172
376
  }
173
377
  }
174
378
  return nodes.sort(compareNodes);
175
379
  }
176
- return walk(root2, false);
380
+ return walk(root2, false, false);
177
381
  }
178
382
  function isEmptyDir(abs) {
179
383
  try {
180
- return fs2.readdirSync(abs).length === 0;
384
+ return fs3.readdirSync(abs).length === 0;
181
385
  } catch {
182
386
  return false;
183
387
  }
@@ -219,12 +423,12 @@ var asText = (v) => v instanceof Date ? v.toISOString().slice(0, 10) : v == null
219
423
  function summarize(root2) {
220
424
  return flattenFiles(scanTree(root2)).flatMap((f) => {
221
425
  if (f.path.split("/").some((part) => part.startsWith("."))) return [];
222
- const text3 = fs3.readFileSync(path3.join(root2, f.path), "utf8");
426
+ const text3 = fs4.readFileSync(path4.join(root2, f.path), "utf8");
223
427
  const meta = parseFrontmatter(text3);
224
428
  if (meta.type === "task" && f.name.startsWith("_")) return [];
225
429
  const id = asText(meta.id);
226
- const total = text3.match(/^\s*[-*+] \[[ xX]\]/gm)?.length ?? 0;
227
- const done = text3.match(/^\s*[-*+] \[[xX]\]/gm)?.length ?? 0;
430
+ const total = text3.match(/^[ \t]*[-*+] \[[ xX]\]/gm)?.length ?? 0;
431
+ const done = text3.match(/^[ \t]*[-*+] \[[xX]\]/gm)?.length ?? 0;
228
432
  const heading2 = text3.match(/^#\s+(.+)$/m)?.[1];
229
433
  const title = asText(meta.title) ?? (heading2 && id ? heading2.replace(new RegExp(`^${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\xB7:-]\\s*`), "") : heading2);
230
434
  return {
@@ -242,13 +446,13 @@ function summarize(root2) {
242
446
  }
243
447
 
244
448
  // src/core/method.ts
245
- import fs7 from "fs";
246
- import path7 from "path";
247
- import YAML2 from "yaml";
449
+ import fs9 from "fs";
450
+ import path9 from "path";
451
+ import YAML3 from "yaml";
248
452
 
249
453
  // src/core/profile.ts
250
- import fs4 from "fs";
251
- import path4 from "path";
454
+ import fs5 from "fs";
455
+ import path5 from "path";
252
456
  var KIND_LABEL = { app: "App", library: "Library", mobile: "Mobile", data: "Data / ML", infra: "Infrastructure" };
253
457
  var KIND_HINT = {
254
458
  app: "web, API, CLI, desktop",
@@ -266,16 +470,16 @@ var EXPERIMENTS_LABEL = {
266
470
  var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", "vendor", "target", ".git", ".venv", "venv"]);
267
471
  var names = (abs) => {
268
472
  try {
269
- return fs4.readdirSync(abs, { withFileTypes: true });
473
+ return fs5.readdirSync(abs, { withFileTypes: true });
270
474
  } catch {
271
475
  return [];
272
476
  }
273
477
  };
274
478
  function find(abs, re, depth) {
275
479
  for (const e of names(abs)) {
276
- if (re.test(e.name)) return path4.join(abs, e.name);
480
+ if (re.test(e.name)) return path5.join(abs, e.name);
277
481
  if (e.isDirectory() && depth > 0 && !SKIP.has(e.name) && !e.name.startsWith(".")) {
278
- const hit = find(path4.join(abs, e.name), re, depth - 1);
482
+ const hit = find(path5.join(abs, e.name), re, depth - 1);
279
483
  if (hit) return hit;
280
484
  }
281
485
  }
@@ -283,14 +487,14 @@ function find(abs, re, depth) {
283
487
  }
284
488
  var read = (abs) => {
285
489
  try {
286
- return fs4.readFileSync(abs, "utf8");
490
+ return fs5.readFileSync(abs, "utf8");
287
491
  } catch {
288
492
  return "";
289
493
  }
290
494
  };
291
495
  var readJson = (abs) => {
292
496
  try {
293
- return JSON.parse(fs4.readFileSync(abs, "utf8"));
497
+ return JSON.parse(fs5.readFileSync(abs, "utf8"));
294
498
  } catch {
295
499
  return null;
296
500
  }
@@ -299,8 +503,8 @@ var APP_DEPS = ["next", "nuxt", "vite", "react-scripts", "@angular/core", "@svel
299
503
  function guessKind(dir) {
300
504
  const found = {};
301
505
  const add = (kind, why) => (found[kind] ??= []).push(why);
302
- const has2 = (name) => fs4.existsSync(path4.join(dir, name));
303
- const pkg = readJson(path4.join(dir, "package.json"));
506
+ const has2 = (name) => fs5.existsSync(path5.join(dir, name));
507
+ const pkg = readJson(path5.join(dir, "package.json"));
304
508
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
305
509
  if (has2("pubspec.yaml")) add("mobile", "Flutter (pubspec.yaml)");
306
510
  if (deps["react-native"] || deps.expo) add("mobile", "React Native");
@@ -312,7 +516,7 @@ function guessKind(dir) {
312
516
  if (has2("Pulumi.yaml")) add("infra", "Pulumi");
313
517
  if (find(dir, /^(Chart|kustomization)\.ya?ml$/, 2)) add("infra", "Helm / Kustomize");
314
518
  if (pkg && !pkg.private && !pkg.bin && (pkg.main || pkg.exports || pkg.module)) add("library", "package.json exports");
315
- if (/^\[lib\]/m.test(read(path4.join(dir, "Cargo.toml")))) add("library", "Cargo [lib]");
519
+ if (/^\[lib\]/m.test(read(path5.join(dir, "Cargo.toml")))) add("library", "Cargo [lib]");
316
520
  if (pkg?.bin) add("app", "CLI (bin)");
317
521
  const framework = APP_DEPS.find((d) => deps[d]);
318
522
  if (framework) add("app", framework);
@@ -330,13 +534,13 @@ function guessKind(dir) {
330
534
  function findContracts(root2) {
331
535
  const hit = find(root2, /(\.proto|^openapi.*\.ya?ml|^openapi.*\.json|^asyncapi.*\.ya?ml)$/i, 3);
332
536
  if (!hit) return null;
333
- const dir = path4.relative(root2, path4.dirname(hit)).split(path4.sep).join("/");
334
- return dir ? `${dir}/` : path4.basename(hit);
537
+ const dir = path5.relative(root2, path5.dirname(hit)).split(path5.sep).join("/");
538
+ return dir ? `${dir}/` : path5.basename(hit);
335
539
  }
336
540
  var looksLikeMicroservices = (root2, projects) => projects >= 2 && (!!findContracts(root2) || !!find(root2, /^(docker-)?compose(\.[\w-]+)?\.ya?ml$/, 0));
337
541
  function detectProfile(dir, kind) {
338
- const has2 = (name) => fs4.existsSync(path4.join(dir, name));
339
- const pkg = readJson(path4.join(dir, "package.json"));
542
+ const has2 = (name) => fs5.existsSync(path5.join(dir, name));
543
+ const pkg = readJson(path5.join(dir, "package.json"));
340
544
  if (kind === "library") return { kind, published: !!pkg?.publishConfig || !!pkg?.name && !pkg.private };
341
545
  if (kind === "mobile") {
342
546
  const platforms = ["ios", "android", "web"].filter((p3) => has2(p3));
@@ -349,7 +553,7 @@ function detectProfile(dir, kind) {
349
553
  }
350
554
  if (kind === "infra") {
351
555
  const envDir = ["envs", "environments", "env"].find((d) => has2(d));
352
- const envs = envDir ? names(path4.join(dir, envDir)).filter((e) => e.isDirectory()).map((e) => e.name) : [];
556
+ const envs = envDir ? names(path5.join(dir, envDir)).filter((e) => e.isDirectory()).map((e) => e.name) : [];
353
557
  return { kind, environments: envs.length ? envs.join(", ") : "dev, staging, prod" };
354
558
  }
355
559
  return { kind };
@@ -370,83 +574,88 @@ function kindGuidance(p3) {
370
574
  }
371
575
  var contractsGuidance = (contracts) => `Microservices: contracts (APIs, events) live in \`${contracts}\`. Changing one is a shared decision (\`docs/architecture/contracts.md\`) and every consumer gets a task.`;
372
576
  function existingDocs(root2) {
373
- const docs = path4.join(root2, "docs");
577
+ const docs = path5.join(root2, "docs");
374
578
  const out = [];
375
579
  for (const e of names(docs)) {
376
580
  if (e.name.startsWith(".") || e.name === "tasks" || e.name === "proposals") continue;
377
581
  if (e.isFile() && e.name.toLowerCase().endsWith(".md")) out.push(`docs/${e.name}`);
378
- else if (e.isDirectory() && find(path4.join(docs, e.name), /\.md$/i, 2)) out.push(`docs/${e.name}/`);
582
+ else if (e.isDirectory() && find(path5.join(docs, e.name), /\.md$/i, 2)) out.push(`docs/${e.name}/`);
379
583
  }
380
- for (const adr of ["adr", "decisions"]) if (find(path4.join(root2, adr), /\.md$/i, 1)) out.push(`${adr}/`);
584
+ for (const adr of ["adr", "decisions"]) if (find(path5.join(root2, adr), /\.md$/i, 1)) out.push(`${adr}/`);
381
585
  return out.sort();
382
586
  }
383
587
 
384
588
  // src/core/scaffold.ts
385
- import fs5 from "fs";
386
- import path5 from "path";
589
+ import fs6 from "fs";
590
+ import path6 from "path";
387
591
  var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
388
592
  function fill(text3, vars) {
389
593
  return text3.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? "");
390
594
  }
391
595
  function copyTree(src, dest, vars, created, root2) {
392
- for (const e of fs5.readdirSync(src, { withFileTypes: true })) {
393
- const from = path5.join(src, e.name);
394
- const to = path5.join(dest, fill(e.name, vars));
596
+ for (const e of fs6.readdirSync(src, { withFileTypes: true })) {
597
+ const from = path6.join(src, e.name);
598
+ const to = path6.join(dest, fill(e.name, vars));
599
+ assertInside(to);
395
600
  if (e.isDirectory()) {
396
- fs5.mkdirSync(to, { recursive: true });
601
+ fs6.mkdirSync(to, { recursive: true });
397
602
  copyTree(from, to, vars, created, root2);
398
- } else if (!fs5.existsSync(to)) {
399
- fs5.writeFileSync(to, fill(fs5.readFileSync(from, "utf8"), vars));
400
- created.push(toPosix(path5.relative(root2, to)));
603
+ } else if (!fs6.existsSync(to)) {
604
+ fs6.writeFileSync(to, fill(fs6.readFileSync(from, "utf8"), vars));
605
+ created.push(toPosix(path6.relative(root2, to)));
401
606
  }
402
607
  }
403
608
  }
404
609
  function createMainDoc(root2, name) {
405
- const file = path5.join(root2, name);
406
- if (fs5.existsSync(file)) return;
407
- const text3 = fs5.readFileSync(path5.join(TEMPLATES_DIR, "main.md"), "utf8");
408
- fs5.writeFileSync(file, fill(text3, { name: path5.basename(root2), date: today() }));
409
- }
410
- function installSkill(root2) {
411
- const dir = path5.join(root2, ".claude", "skills", "markode");
412
- fs5.mkdirSync(dir, { recursive: true });
413
- fs5.copyFileSync(path5.join(TEMPLATES_DIR, "skill", "SKILL.md"), path5.join(dir, "SKILL.md"));
414
- }
415
- function ensureGitignore(root2) {
416
- const file = path5.join(root2, ".gitignore");
417
- const text3 = fs5.existsSync(file) ? fs5.readFileSync(file, "utf8") : "";
418
- if (text3.split("\n").some((l) => [".markode", ".markode/", "/.markode", "/.markode/"].includes(l.trim()))) return;
419
- fs5.writeFileSync(file, text3 + (text3 && !text3.endsWith("\n") ? "\n" : "") + ".markode/\n");
610
+ const file = path6.join(root2, name);
611
+ if (fs6.existsSync(file)) return;
612
+ assertInside(file);
613
+ const text3 = fs6.readFileSync(path6.join(TEMPLATES_DIR, "main.md"), "utf8");
614
+ fs6.writeFileSync(file, fill(text3, { name: path6.basename(root2), date: today() }));
615
+ }
616
+ var vsCodeFile = (root2) => path6.join(root2, ".vscode", "settings.json");
617
+ var HIDDEN = "**/.markode";
618
+ function readVsCode(root2) {
619
+ if (!fs6.existsSync(vsCodeFile(root2))) return {};
620
+ try {
621
+ return JSON.parse(fs6.readFileSync(vsCodeFile(root2), "utf8"));
622
+ } catch {
623
+ return null;
624
+ }
420
625
  }
626
+ var isHiddenInVsCode = (root2) => readVsCode(root2)?.["files.exclude"]?.[HIDDEN] === true;
421
627
  function hideInVsCode(root2) {
422
- const file = path5.join(root2, ".vscode", "settings.json");
423
- let settings = {};
424
- if (fs5.existsSync(file)) {
425
- try {
426
- settings = JSON.parse(fs5.readFileSync(file, "utf8"));
427
- } catch {
428
- return false;
429
- }
430
- }
431
- settings["files.exclude"] = { ...settings["files.exclude"] ?? {}, "**/.markode": true };
432
- fs5.mkdirSync(path5.dirname(file), { recursive: true });
433
- fs5.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
628
+ const settings = readVsCode(root2);
629
+ if (!settings) return false;
630
+ assertInside(vsCodeFile(root2));
631
+ settings["files.exclude"] = { ...settings["files.exclude"] ?? {}, [HIDDEN]: true };
632
+ fs6.mkdirSync(path6.dirname(vsCodeFile(root2)), { recursive: true });
633
+ fs6.writeFileSync(vsCodeFile(root2), JSON.stringify(settings, null, 2) + "\n");
434
634
  return true;
435
635
  }
636
+ function showInVsCode(root2) {
637
+ const settings = readVsCode(root2);
638
+ if (!settings?.["files.exclude"]?.[HIDDEN]) return;
639
+ delete settings["files.exclude"][HIDDEN];
640
+ if (!Object.keys(settings["files.exclude"]).length) delete settings["files.exclude"];
641
+ assertInside(vsCodeFile(root2));
642
+ fs6.writeFileSync(vsCodeFile(root2), JSON.stringify(settings, null, 2) + "\n");
643
+ }
436
644
  function missingMainDocs(root2) {
437
645
  return ["", ...findSubprojects(scanTree(root2))].flatMap((dir) => {
438
- const doc = missingMainDoc(path5.join(root2, dir));
439
- return doc ? [{ path: path5.posix.join(dir, doc), pointsTo: doc === "CLAUDE.md" ? "AGENTS.md" : "CLAUDE.md" }] : [];
646
+ const doc = missingMainDoc(path6.join(root2, dir));
647
+ return doc ? [{ path: path6.posix.join(dir, doc), pointsTo: doc === "CLAUDE.md" ? "AGENTS.md" : "CLAUDE.md" }] : [];
440
648
  });
441
649
  }
442
650
  function linkMainDocs(root2) {
443
- return missingMainDocs(root2).flatMap((m) => linkMainDoc(path5.join(root2, path5.posix.dirname(m.path))) ? [m.path] : []);
651
+ return missingMainDocs(root2).flatMap((m) => linkMainDoc(path6.join(root2, path6.posix.dirname(m.path))) ? [m.path] : []);
444
652
  }
445
653
 
446
654
  // src/core/sdd.ts
447
655
  import { spawnSync } from "child_process";
448
- import fs6 from "fs";
449
- import path6 from "path";
656
+ import fs7 from "fs";
657
+ import os2 from "os";
658
+ import path7 from "path";
450
659
  var SDD_AGENTS = {
451
660
  claude: { label: "Claude Code", openspec: "claude", speckit: "claude" },
452
661
  codex: { label: "Codex", openspec: "codex", speckit: "codex" },
@@ -457,11 +666,11 @@ var SDD_AGENTS = {
457
666
  var OPENSPEC_PKG = "@fission-ai/openspec@latest";
458
667
  var SPECKIT_SRC = "git+https://github.com/github/spec-kit.git";
459
668
  function detectWorkflow(root2) {
460
- if (fs6.existsSync(path6.join(root2, "openspec"))) return "openspec";
461
- if (fs6.existsSync(path6.join(root2, ".specify"))) return "speckit";
669
+ if (fs7.existsSync(path7.join(root2, "openspec"))) return "openspec";
670
+ if (fs7.existsSync(path7.join(root2, ".specify"))) return "speckit";
462
671
  return null;
463
672
  }
464
- var has = (cmd) => spawnSync(cmd, ["--version"], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
673
+ var has = (cmd) => spawnSync(cmd, ["--version"], { cwd: os2.homedir(), stdio: "ignore", shell: process.platform === "win32" }).status === 0;
465
674
  var hasUv = () => has("uvx");
466
675
  function installCommand(workflow, agents) {
467
676
  if (workflow === "openspec") {
@@ -482,11 +691,11 @@ function installWorkflow(root2, workflow, agents, official) {
482
691
  else result.error = (run.error?.message ?? `${run.stderr ?? ""}
483
692
  ${run.stdout ?? ""}`).trim().split("\n").slice(-6).join("\n");
484
693
  }
485
- copyTree(path6.join(TEMPLATES_DIR, "sdd", workflow), root2, { date: today2() }, result.created, root2);
694
+ copyTree(path7.join(TEMPLATES_DIR, "sdd", workflow), root2, { date: today2() }, result.created, root2);
486
695
  const dirs2 = /* @__PURE__ */ new Set();
487
696
  for (const f of flattenFiles(scanTree(root2), true)) {
488
697
  if (before.has(f.path) || !f.path.startsWith(".") || f.path.startsWith(".specify/memory/")) continue;
489
- dirs2.add(path6.posix.dirname(f.path));
698
+ dirs2.add(path7.posix.dirname(f.path));
490
699
  }
491
700
  for (const dir of dirs2) setIgnored(root2, dir, "dir", true);
492
701
  result.ignored = [...dirs2].sort();
@@ -520,87 +729,89 @@ function applyWorkflowToFile(file, workflow, text3) {
520
729
  function writeManagedBlock(file, body, top = false, name = "workflow") {
521
730
  const START2 = `<!-- markode:${name}:start -->`;
522
731
  const END2 = `<!-- markode:${name}:end -->`;
523
- const text3 = fs6.readFileSync(file, "utf8");
732
+ assertInside(file);
733
+ const text3 = fs7.readFileSync(file, "utf8");
524
734
  const s = text3.indexOf(START2);
525
735
  const e = text3.indexOf(END2);
526
736
  const existing = s >= 0 && e > s ? text3.slice(s, e + END2.length) : null;
527
737
  if (body === null) {
528
- if (existing) fs6.writeFileSync(file, text3.replace(new RegExp(`\\n*${escapeRe(existing)}\\n?`), "\n"));
738
+ if (existing) fs7.writeFileSync(file, text3.replace(new RegExp(`\\n*${escapeRe(existing)}\\n?`), "\n"));
529
739
  return;
530
740
  }
531
741
  const section2 = `${START2}
532
742
  ${body}
533
743
  ${END2}`;
534
- if (existing) return fs6.writeFileSync(file, text3.replace(existing, () => section2));
744
+ if (existing === section2) return;
745
+ if (existing) return fs7.writeFileSync(file, text3.replace(existing, () => section2));
535
746
  const title = top ? /^# .*\n/m.exec(text3) : null;
536
- if (title) return fs6.writeFileSync(file, text3.slice(0, title.index + title[0].length) + "\n" + section2 + "\n" + text3.slice(title.index + title[0].length));
537
- fs6.writeFileSync(file, text3.replace(/\s*$/, "") + "\n\n" + section2 + "\n");
747
+ if (title) return fs7.writeFileSync(file, text3.slice(0, title.index + title[0].length) + "\n" + section2 + "\n" + text3.slice(title.index + title[0].length));
748
+ fs7.writeFileSync(file, text3.replace(/\s*$/, "") + "\n\n" + section2 + "\n");
538
749
  }
539
750
  var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
540
751
  var dirs = (abs) => {
541
752
  try {
542
- return fs6.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
753
+ return fs7.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
543
754
  } catch {
544
755
  return [];
545
756
  }
546
757
  };
547
758
  var read2 = (abs) => {
548
759
  try {
549
- return fs6.readFileSync(abs, "utf8");
760
+ return fs7.readFileSync(abs, "utf8");
550
761
  } catch {
551
762
  return null;
552
763
  }
553
764
  };
554
- var progress = (text3) => ({ done: text3?.match(/^\s*[-*+] \[[xX]\]/gm)?.length ?? 0, total: text3?.match(/^\s*[-*+] \[[ xX]\]/gm)?.length ?? 0 });
765
+ var progress = (text3) => ({ done: text3?.match(/^[ \t]*[-*+] \[[xX]\]/gm)?.length ?? 0, total: text3?.match(/^[ \t]*[-*+] \[[ xX]\]/gm)?.length ?? 0 });
555
766
  var heading = (text3) => text3?.match(/^#\s+(.+)$/m)?.[1].trim() ?? null;
556
767
  var countSpecs = (abs) => {
557
768
  try {
558
- return fs6.readdirSync(abs, { recursive: true }).filter((f) => String(f).endsWith("spec.md")).length;
769
+ return fs7.readdirSync(abs, { recursive: true }).filter((f) => String(f).endsWith("spec.md")).length;
559
770
  } catch {
560
771
  return 0;
561
772
  }
562
773
  };
563
774
  function summarizeSdd(root2) {
564
775
  const out = {};
565
- const os3 = path6.join(root2, "openspec");
566
- if (fs6.existsSync(os3)) {
567
- const rel = (...p3) => toPosix(path6.join("openspec", ...p3));
776
+ const os5 = path7.join(root2, "openspec");
777
+ if (fs7.existsSync(os5)) {
778
+ const rel = (...p3) => toPosix(path7.join("openspec", ...p3));
568
779
  out.openspec = {
569
- changes: dirs(path6.join(os3, "changes")).filter((id) => id !== "archive").map((id) => {
570
- const dir = path6.join(os3, "changes", id);
571
- const proposal = read2(path6.join(dir, "proposal.md"));
572
- const tasks = read2(path6.join(dir, "tasks.md"));
780
+ changes: dirs(path7.join(os5, "changes")).filter((id) => id !== "archive").map((id) => {
781
+ const dir = path7.join(os5, "changes", id);
782
+ const proposal = read2(path7.join(dir, "proposal.md"));
783
+ const tasks = read2(path7.join(dir, "tasks.md"));
573
784
  return {
574
785
  id,
575
786
  // "Proposal: Add login" → "Add login"
576
787
  title: heading(proposal)?.replace(/^(change\s+)?proposal:\s*/i, "") ?? id,
577
788
  path: rel("changes", id, proposal !== null ? "proposal.md" : "tasks.md"),
578
- artifacts: { proposal: proposal !== null, design: fs6.existsSync(path6.join(dir, "design.md")), tasks: tasks !== null, specs: countSpecs(path6.join(dir, "specs")) },
789
+ artifacts: { proposal: proposal !== null, design: fs7.existsSync(path7.join(dir, "design.md")), tasks: tasks !== null, specs: countSpecs(path7.join(dir, "specs")) },
579
790
  tasks: progress(tasks)
580
791
  };
581
792
  }),
582
- specs: dirs(path6.join(os3, "specs")).filter((id) => fs6.existsSync(path6.join(os3, "specs", id, "spec.md"))).map((id) => ({ id, path: rel("specs", id, "spec.md") })),
583
- archived: dirs(path6.join(os3, "changes", "archive")).reverse().slice(0, 12).map((id) => {
584
- const file = ["proposal.md", "tasks.md"].find((f) => fs6.existsSync(path6.join(os3, "changes", "archive", id, f)));
793
+ specs: dirs(path7.join(os5, "specs")).filter((id) => fs7.existsSync(path7.join(os5, "specs", id, "spec.md"))).map((id) => ({ id, path: rel("specs", id, "spec.md") })),
794
+ archived: dirs(path7.join(os5, "changes", "archive")).reverse().slice(0, 12).map((id) => {
795
+ const file = ["proposal.md", "tasks.md"].find((f) => fs7.existsSync(path7.join(os5, "changes", "archive", id, f)));
585
796
  return { id, path: file ? rel("changes", "archive", id, file) : "" };
586
797
  })
587
798
  };
588
799
  }
589
- if (fs6.existsSync(path6.join(root2, ".specify"))) {
590
- const constitution = read2(path6.join(root2, ".specify", "memory", "constitution.md"));
800
+ if (fs7.existsSync(path7.join(root2, ".specify"))) {
801
+ const constitution = read2(path7.join(root2, ".specify", "memory", "constitution.md"));
591
802
  out.speckit = {
592
803
  // The template is full of [PLACEHOLDERS] until /speckit-constitution fills it.
593
804
  constitution: constitution === null ? null : { path: ".specify/memory/constitution.md", filled: !/\[[A-Z_0-9]+\]/.test(constitution) },
594
- features: dirs(path6.join(root2, "specs")).map((id) => {
595
- const dir = path6.join(root2, "specs", id);
596
- const spec = read2(path6.join(dir, "spec.md"));
597
- const tasks = read2(path6.join(dir, "tasks.md"));
805
+ features: dirs(path7.join(root2, "specs")).map((id) => {
806
+ const dir = path7.join(root2, "specs", id);
807
+ const spec = read2(path7.join(dir, "spec.md"));
808
+ const tasks = read2(path7.join(dir, "tasks.md"));
598
809
  return {
599
810
  id,
600
811
  // "Feature Specification: Login" → "Login"
601
812
  title: heading(spec)?.replace(/^feature specification:\s*/i, "") ?? id,
602
- path: toPosix(path6.join("specs", id, spec !== null ? "spec.md" : "plan.md")),
603
- artifacts: { spec: spec !== null, plan: fs6.existsSync(path6.join(dir, "plan.md")), tasks: tasks !== null },
813
+ path: toPosix(path7.join("specs", id, spec !== null ? "spec.md" : "plan.md")),
814
+ artifacts: { spec: spec !== null, plan: fs7.existsSync(path7.join(dir, "plan.md")), tasks: tasks !== null },
604
815
  tasks: progress(tasks)
605
816
  };
606
817
  })
@@ -612,27 +823,181 @@ function sddPrefixes(summary) {
612
823
  return [...summary.openspec ? ["openspec/"] : [], ...summary.speckit ? ["specs/", ".specify/"] : []];
613
824
  }
614
825
 
826
+ // src/core/skills.ts
827
+ import fs8 from "fs";
828
+ import os3 from "os";
829
+ import path8 from "path";
830
+ import YAML2 from "yaml";
831
+ var SOURCE_DIRS = {
832
+ claude: [".claude", "skills"],
833
+ // Shared folder read by Codex, Copilot and Gemini CLI, and where `npx skills` installs.
834
+ agents: [".agents", "skills"],
835
+ codex: [".codex", "skills"],
836
+ gemini: [".gemini", "skills"],
837
+ copilot: [".copilot", "skills"],
838
+ cursor: [".cursor", "skills"]
839
+ };
840
+ var PROJECT_TARGETS = { claude: ".claude/skills", agents: ".agents/skills" };
841
+ var READS = { claude: ["claude"], agents: ["agents", "codex", "gemini", "copilot", "cursor"] };
842
+ var sourceDir = (source) => path8.join(os3.homedir(), ...SOURCE_DIRS[source]);
843
+ var display = (p3) => p3.startsWith(os3.homedir() + path8.sep) ? "~" + p3.slice(os3.homedir().length) : p3;
844
+ function skillSources() {
845
+ return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs8.existsSync(sourceDir(id)) }));
846
+ }
847
+ var tagsFile = () => path8.join(GLOBAL_DIR, "skills.json");
848
+ function readTags() {
849
+ try {
850
+ return JSON.parse(fs8.readFileSync(tagsFile(), "utf8")).tags ?? {};
851
+ } catch {
852
+ return {};
853
+ }
854
+ }
855
+ function setSkillTags(name, tags) {
856
+ const all = readTags();
857
+ const clean = [...new Set(tags.map((t) => t.trim().toLowerCase()).filter(Boolean))];
858
+ if (clean.length) all[name] = clean;
859
+ else delete all[name];
860
+ fs8.mkdirSync(GLOBAL_DIR, { recursive: true });
861
+ fs8.writeFileSync(tagsFile(), JSON.stringify({ tags: all }, null, 2) + "\n");
862
+ return clean;
863
+ }
864
+ function findSkills(dir, source, managed, depth = 0) {
865
+ let entries2;
866
+ try {
867
+ entries2 = fs8.readdirSync(dir, { withFileTypes: true });
868
+ } catch {
869
+ return [];
870
+ }
871
+ return entries2.flatMap((e) => {
872
+ const abs = path8.join(dir, e.name);
873
+ let real;
874
+ try {
875
+ if (!fs8.statSync(abs).isDirectory()) return [];
876
+ real = fs8.realpathSync(abs);
877
+ } catch {
878
+ return [];
879
+ }
880
+ if (fs8.existsSync(path8.join(abs, "SKILL.md"))) return [{ dir: abs, real, source, managed }];
881
+ return depth < 2 ? findSkills(abs, source, managed || e.name === "synced" || e.name.startsWith("."), depth + 1) : [];
882
+ });
883
+ }
884
+ function frontmatter(text3) {
885
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text3);
886
+ if (!m) return null;
887
+ try {
888
+ const data = YAML2.parse(m[1], { logLevel: "error" });
889
+ return data && typeof data === "object" ? data : null;
890
+ } catch {
891
+ return null;
892
+ }
893
+ }
894
+ function listFiles(dir) {
895
+ try {
896
+ return fs8.readdirSync(dir, { recursive: true, withFileTypes: true }).map((e) => ({ e, rel: path8.relative(dir, path8.join(e.parentPath ?? e.path ?? dir, e.name)).split(path8.sep).join("/") })).filter(({ e, rel }) => e.isFile() && rel !== "SKILL.md").map(({ rel }) => rel).sort().slice(0, 100);
897
+ } catch {
898
+ return [];
899
+ }
900
+ }
901
+ function listSkills(root2) {
902
+ const byReal = /* @__PURE__ */ new Map();
903
+ for (const source of Object.keys(SOURCE_DIRS)) {
904
+ for (const f of findSkills(sourceDir(source), source, false)) byReal.set(f.real, [...byReal.get(f.real) ?? [], f]);
905
+ }
906
+ const userTags = readTags();
907
+ const used = projectTargets(root2);
908
+ const skills = [];
909
+ for (const [real, found] of byReal) {
910
+ const text3 = fs8.readFileSync(path8.join(real, "SKILL.md"), "utf8");
911
+ const fm = frontmatter(text3);
912
+ const folder = path8.basename(found[0].dir);
913
+ const name = typeof fm?.name === "string" ? fm.name : folder;
914
+ const description = typeof fm?.description === "string" ? fm.description.trim() : "";
915
+ const issues = [];
916
+ if (!fm) issues.push("frontmatter");
917
+ else {
918
+ if (typeof fm.name !== "string" || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fm.name) || fm.name.length > 64) issues.push("name");
919
+ else if (fm.name !== folder) issues.push("name-dir");
920
+ if (!description) issues.push("description");
921
+ else if (description.length > 1024) issues.push("description-long");
922
+ }
923
+ const installed = Object.keys(PROJECT_TARGETS).filter((t) => fs8.existsSync(path8.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md")));
924
+ const declared = fm?.metadata?.tags;
925
+ const metaTags = Array.isArray(declared) ? declared.map(String) : typeof declared === "string" ? declared.split(",") : [];
926
+ skills.push({
927
+ key: real,
928
+ name,
929
+ folder,
930
+ description,
931
+ locations: found.map((f) => ({ source: f.source, path: display(f.dir) })),
932
+ managed: found.every((f) => f.managed),
933
+ files: listFiles(real),
934
+ tags: [.../* @__PURE__ */ new Set([...userTags[name] ?? [], ...metaTags.map((t) => t.trim().toLowerCase()).filter(Boolean)])],
935
+ issues,
936
+ installed,
937
+ status: Object.fromEntries(
938
+ used.map((t) => {
939
+ const global = found.some((f) => READS[t].includes(f.source));
940
+ const local = installed.includes(t);
941
+ return [t, global && local ? "both" : global ? "global" : local ? "project" : "missing"];
942
+ })
943
+ ),
944
+ differs: installed.filter((t) => {
945
+ try {
946
+ return fs8.readFileSync(path8.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md"), "utf8") !== text3;
947
+ } catch {
948
+ return false;
949
+ }
950
+ })
951
+ });
952
+ }
953
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
954
+ }
955
+ function getSkill(root2, key) {
956
+ const skill = typeof key === "string" ? listSkills(root2).find((s) => s.key === key) : void 0;
957
+ if (!skill) throw Object.assign(new Error("Skill not found"), { code: "ENOENT" });
958
+ return skill;
959
+ }
960
+ function projectTargets(root2) {
961
+ const has2 = (p3) => fs8.existsSync(path8.join(root2, p3));
962
+ const out = [];
963
+ if (has2(".claude") || has2("CLAUDE.md")) out.push("claude");
964
+ if (has2(".agents") || has2("AGENTS.md") || has2(".codex") || has2(".gemini") || has2(".github/skills")) out.push("agents");
965
+ return out.length ? out : ["claude"];
966
+ }
967
+ function installSkill(root2, skill, targets, overwrite) {
968
+ const dests = targets.map((t) => path8.join(root2, PROJECT_TARGETS[t], skill.folder));
969
+ const existing = dests.filter((d) => fs8.existsSync(d));
970
+ if (existing.length && !overwrite) return { conflict: existing.map((d) => path8.relative(root2, d).split(path8.sep).join("/")) };
971
+ for (const dest of dests) {
972
+ assertInside(dest, root2);
973
+ fs8.rmSync(dest, { recursive: true, force: true });
974
+ fs8.mkdirSync(path8.dirname(dest), { recursive: true });
975
+ fs8.cpSync(skill.key, dest, { recursive: true, dereference: true });
976
+ }
977
+ return { ok: true };
978
+ }
979
+
615
980
  // src/core/method.ts
616
981
  var SKIP2 = /* @__PURE__ */ new Set(["docs", "node_modules", "dist", "build", "coverage", "tmp", "vendor", "target"]);
617
982
  var MANIFESTS = ["package.json", "pyproject.toml", "go.mod", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts", "composer.json", "Gemfile", "deno.json", ".git"];
618
- var isDir = (abs) => !!fs7.statSync(abs, { throwIfNoEntry: false })?.isDirectory();
983
+ var isDir = (abs) => !!fs9.statSync(abs, { throwIfNoEntry: false })?.isDirectory();
619
984
  var subdirs = (abs) => {
620
985
  try {
621
- return fs7.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP2.has(e.name)).map((e) => e.name);
986
+ return fs9.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP2.has(e.name)).map((e) => e.name);
622
987
  } catch {
623
988
  return [];
624
989
  }
625
990
  };
626
- var isProject = (abs) => MANIFESTS.some((m) => fs7.existsSync(path7.join(abs, m)));
991
+ var isProject = (abs) => MANIFESTS.some((m) => fs9.existsSync(path9.join(abs, m)));
627
992
  function detectAreas(root2) {
628
993
  const globs = [];
629
994
  try {
630
- const ws = YAML2.parse(fs7.readFileSync(path7.join(root2, "pnpm-workspace.yaml"), "utf8"), { logLevel: "error" });
995
+ const ws = YAML3.parse(fs9.readFileSync(path9.join(root2, "pnpm-workspace.yaml"), "utf8"), { logLevel: "error" });
631
996
  if (Array.isArray(ws?.packages)) globs.push(...ws.packages);
632
997
  } catch {
633
998
  }
634
999
  try {
635
- const ws = JSON.parse(fs7.readFileSync(path7.join(root2, "package.json"), "utf8")).workspaces;
1000
+ const ws = JSON.parse(fs9.readFileSync(path9.join(root2, "package.json"), "utf8")).workspaces;
636
1001
  globs.push(...Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : []);
637
1002
  } catch {
638
1003
  }
@@ -643,25 +1008,25 @@ function detectAreas(root2) {
643
1008
  const base = g.replace(/\/\*\*?$/, "");
644
1009
  if (base.includes("*")) continue;
645
1010
  if (base === g) found.add(g);
646
- else for (const name of subdirs(path7.join(root2, base))) found.add(`${base}/${name}`);
1011
+ else for (const name of subdirs(path9.join(root2, base))) found.add(`${base}/${name}`);
647
1012
  }
648
1013
  if (!isProject(root2)) {
649
1014
  for (const top of subdirs(root2)) {
650
- if (isProject(path7.join(root2, top))) found.add(top);
651
- else for (const name of subdirs(path7.join(root2, top))) if (isProject(path7.join(root2, top, name))) found.add(`${top}/${name}`);
1015
+ if (isProject(path9.join(root2, top))) found.add(top);
1016
+ else for (const name of subdirs(path9.join(root2, top))) if (isProject(path9.join(root2, top, name))) found.add(`${top}/${name}`);
652
1017
  }
653
1018
  }
654
1019
  let unsure = false;
655
1020
  if (!globs.length && isProject(root2)) {
656
- const tops = subdirs(root2).filter((top) => isProject(path7.join(root2, top)));
1021
+ const tops = subdirs(root2).filter((top) => isProject(path9.join(root2, top)));
657
1022
  if (tops.length >= 2 && tops.some((t) => !found.has(t))) tops.forEach((t) => found.add(t)), unsure = true;
658
1023
  }
659
- const dirs2 = [...found].filter((d) => d && !d.split("/").some((part) => part.startsWith(".") || SKIP2.has(part)) && isDir(path7.join(root2, d)));
1024
+ const dirs2 = [...found].filter((d) => d && !d.split("/").some((part) => part.startsWith(".") || SKIP2.has(part)) && isDir(path9.join(root2, d)));
660
1025
  return { dirs: dirs2.filter((d) => !dirs2.some((o) => o.startsWith(d + "/"))).sort(), unsure };
661
1026
  }
662
1027
  function areaDescription(root2, dir) {
663
1028
  try {
664
- return String(JSON.parse(fs7.readFileSync(path7.join(root2, dir, "package.json"), "utf8")).description ?? "").trim();
1029
+ return String(JSON.parse(fs9.readFileSync(path9.join(root2, dir, "package.json"), "utf8")).description ?? "").trim();
665
1030
  } catch {
666
1031
  return "";
667
1032
  }
@@ -686,7 +1051,7 @@ var KNOWN_PREFIXES = {
686
1051
  scripts: "SCR"
687
1052
  };
688
1053
  function suggestPrefix(dir, used) {
689
- dir = path7.posix.basename(dir);
1054
+ dir = path9.posix.basename(dir);
690
1055
  const base = KNOWN_PREFIXES[dir.toLowerCase()] ?? (dir.replace(/[^a-z0-9]/gi, "").slice(0, 3).toUpperCase() || "AREA");
691
1056
  let prefix = base;
692
1057
  for (let i = 2; used.has(prefix) || prefix === "CORE"; i++) prefix = `${base}${i}`;
@@ -696,24 +1061,25 @@ function suggestPrefix(dir, used) {
696
1061
  var rootPrefix = (areas) => areas.length ? "CORE" : "T";
697
1062
  function scaffoldMethod(root2, mainDoc, areas) {
698
1063
  const created = [];
699
- const name = path7.basename(root2);
700
- copyTree(path7.join(TEMPLATES_DIR, "method", "root"), root2, { date: today(), name, prefix: rootPrefix(areas), area: areas.length ? "core" : name }, created, root2);
1064
+ const name = path9.basename(root2);
1065
+ copyTree(path9.join(TEMPLATES_DIR, "method", "root"), root2, { date: today(), name, prefix: rootPrefix(areas), area: areas.length ? "core" : name }, created, root2);
701
1066
  for (const a of areas) {
702
1067
  const vars = { date: today(), name, prefix: a.prefix, area: a.dir, up: "../".repeat(a.dir.split("/").length) };
703
- const dir = path7.join(root2, a.dir);
704
- copyTree(path7.join(TEMPLATES_DIR, "method", "area"), dir, vars, created, root2);
1068
+ const dir = path9.join(root2, a.dir);
1069
+ copyTree(path9.join(TEMPLATES_DIR, "method", "area"), dir, vars, created, root2);
705
1070
  let doc = contentDoc(dir);
706
1071
  if (!doc) {
707
- doc = path7.join(dir, mainDoc);
708
- fs7.writeFileSync(doc, fill(fs7.readFileSync(path7.join(TEMPLATES_DIR, "method", "area-main.md"), "utf8"), vars));
709
- created.push(path7.posix.join(a.dir, mainDoc));
1072
+ doc = path9.join(dir, mainDoc);
1073
+ assertInside(doc);
1074
+ fs9.writeFileSync(doc, fill(fs9.readFileSync(path9.join(TEMPLATES_DIR, "method", "area-main.md"), "utf8"), vars));
1075
+ created.push(path9.posix.join(a.dir, mainDoc));
710
1076
  }
711
- if (claudeUsed(root2, mainDoc) && linkAgentsDoc(dir)) created.push(path7.posix.join(a.dir, "CLAUDE.md"));
1077
+ if (claudeUsed(root2, mainDoc) && linkAgentsDoc(dir)) created.push(path9.posix.join(a.dir, "CLAUDE.md"));
712
1078
  writeManagedBlock(doc, areaBlock(a, vars.up, mainDoc), true);
713
1079
  }
714
1080
  return created;
715
1081
  }
716
- var claudeUsed = (root2, mainDoc) => mainDoc === "CLAUDE.md" || fs7.existsSync(path7.join(root2, "CLAUDE.md"));
1082
+ var claudeUsed = (root2, mainDoc) => mainDoc === "CLAUDE.md" || fs9.existsSync(path9.join(root2, "CLAUDE.md"));
717
1083
  function areaBlock(a, up, mainDoc) {
718
1084
  const guidance = a.profile && kindGuidance(a.profile);
719
1085
  return [
@@ -778,8 +1144,25 @@ function methodSection(areas, root2, ctx = {}) {
778
1144
  ].join("\n");
779
1145
  }
780
1146
  function installMethodSkills(root2) {
781
- const src = path7.join(TEMPLATES_DIR, "method", "skills");
782
- for (const name of fs7.readdirSync(src)) fs7.cpSync(path7.join(src, name), path7.join(root2, ".claude", "skills", name), { recursive: true });
1147
+ const src = path9.join(TEMPLATES_DIR, "method", "skills");
1148
+ for (const target of projectTargets(root2))
1149
+ for (const name of fs9.readdirSync(src)) {
1150
+ const dest = path9.join(root2, PROJECT_TARGETS[target], name);
1151
+ assertInside(dest);
1152
+ fs9.cpSync(path9.join(src, name), dest, { recursive: true });
1153
+ }
1154
+ }
1155
+ function setupMethod(root2, mainDoc, monorepo) {
1156
+ const used = /* @__PURE__ */ new Set();
1157
+ const areas = monorepo === "single" ? [] : detectAreas(root2).dirs.map((dir) => ({ dir, prefix: suggestPrefix(dir, used) }));
1158
+ for (const a of areas) a.profile = detectProfile(path9.join(root2, a.dir), guessKind(path9.join(root2, a.dir)).kind);
1159
+ const contracts = looksLikeMicroservices(root2, areas.length) ? findContracts(root2) : null;
1160
+ const profile = areas.length ? void 0 : detectProfile(root2, guessKind(root2).kind);
1161
+ const existing = existingDocs(root2);
1162
+ const created = scaffoldMethod(root2, mainDoc, areas);
1163
+ installMethodSkills(root2);
1164
+ applyWorkflowToFile(path9.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
1165
+ return { created, areas };
783
1166
  }
784
1167
 
785
1168
  // src/core/check.ts
@@ -788,13 +1171,13 @@ var LINK = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
788
1171
  function checkDocs(root2) {
789
1172
  const docs = summarize(root2);
790
1173
  const status = new Map(docs.map((d) => [d.path, d.status ?? ""]));
791
- const read3 = (rel) => fs8.readFileSync(path8.join(root2, rel), "utf8");
1174
+ const read3 = (rel) => fs10.readFileSync(path10.join(root2, rel), "utf8");
792
1175
  const phases = [];
793
1176
  for (const phase of docs.filter((d) => d.type === "phase")) {
794
1177
  for (const line of read3(phase.path).split("\n")) {
795
1178
  const m = TASK_LINE.exec(line);
796
1179
  if (!m) continue;
797
- const task = path8.posix.normalize(path8.posix.join(path8.posix.dirname(phase.path), m[2].split("#")[0]));
1180
+ const task = path10.posix.normalize(path10.posix.join(path10.posix.dirname(phase.path), m[2].split("#")[0]));
798
1181
  const s = status.get(task);
799
1182
  if (s === void 0) continue;
800
1183
  const ticked = m[1] !== " ";
@@ -811,7 +1194,7 @@ function checkDocs(root2) {
811
1194
  const target = m[1];
812
1195
  if (/^[a-z][\w+.-]*:|^#|^\/|[<{]/i.test(target)) continue;
813
1196
  const file = decodeURIComponent(target.split(/[#?]/)[0]);
814
- if (file && !fs8.existsSync(path8.join(root2, path8.dirname(doc.path), file))) links.push({ file: doc.path, line: i + 1, target });
1197
+ if (file && !fs10.existsSync(path10.join(root2, path10.dirname(doc.path), file))) links.push({ file: doc.path, line: i + 1, target });
815
1198
  }
816
1199
  });
817
1200
  }
@@ -820,49 +1203,83 @@ function checkDocs(root2) {
820
1203
  function projectsTable(root2) {
821
1204
  const doc = mainDocOf(root2);
822
1205
  if (!doc) return null;
823
- const text3 = fs8.readFileSync(path8.join(root2, doc), "utf8");
1206
+ const text3 = fs10.readFileSync(path10.join(root2, doc), "utf8");
824
1207
  const section2 = /^### Projects\n([\s\S]*?)(?=^#{1,3} |<!-- markode:workflow:end -->)/m.exec(text3)?.[1];
825
1208
  if (!section2) return null;
826
1209
  const listed = [...section2.matchAll(/^\|\s*`([^`]+?)\/?`\s*\|/gm)].map((m) => m[1]).filter((d) => d !== "docs");
827
1210
  const found = detectAreas(root2).dirs;
828
- return { missing: found.filter((d) => !listed.includes(d)), gone: listed.filter((d) => !fs8.existsSync(path8.join(root2, d))) };
1211
+ return { missing: found.filter((d) => !listed.includes(d)), gone: listed.filter((d) => !fs10.existsSync(path10.join(root2, d))) };
829
1212
  }
830
1213
 
831
1214
  // src/core/defaults.ts
832
- import fs9 from "fs";
833
- import path9 from "path";
834
- var BASE_DEFAULTS = {
835
- mode: "ask",
836
- mainDoc: "CLAUDE.md",
837
- workflow: "none",
838
- agents: ["claude"],
839
- monorepo: "per-project",
840
- sourceLang: "en",
841
- rules: [],
842
- rulesInSubprojects: false,
843
- hideInVsCode: true
1215
+ import fs15 from "fs";
1216
+ import path15 from "path";
1217
+
1218
+ // src/core/features.ts
1219
+ import fs14 from "fs";
1220
+ import path14 from "path";
1221
+
1222
+ // src/core/hooks.ts
1223
+ import fs11 from "fs";
1224
+ import path11 from "path";
1225
+ var EVENTS = {
1226
+ SessionStart: "changes --start",
1227
+ UserPromptSubmit: "changes --hook UserPromptSubmit",
1228
+ PostToolUse: "changes --hook PostToolUse"
844
1229
  };
845
- var defaultsFile = () => path9.join(GLOBAL_DIR, "defaults.json");
846
- function readDefaults() {
1230
+ var LOCAL = "settings.local.json";
1231
+ var SHARED = "settings.json";
1232
+ var settingsFile2 = (root2, name = LOCAL) => path11.join(root2, ".claude", name);
1233
+ function cli() {
1234
+ const dirs2 = (process.env.PATH ?? "").split(path11.delimiter).filter(Boolean);
1235
+ if (dirs2.some((d) => ["markode", "markode.cmd"].some((f) => fs11.existsSync(path11.join(d, f))))) return "markode";
1236
+ const name = JSON.parse(fs11.readFileSync(path11.join(PKG_ROOT, "package.json"), "utf8")).name;
1237
+ return `npx -y --prefer-offline ${name}`;
1238
+ }
1239
+ var isOurs = (entry) => Array.isArray(entry?.hooks) && entry.hooks.some((h) => typeof h?.command === "string" && / changes --(start|hook)\b/.test(h.command));
1240
+ function readSettings2(root2, name = LOCAL) {
1241
+ const file = settingsFile2(root2, name);
1242
+ if (!fs11.existsSync(file)) return {};
847
1243
  try {
848
- const saved = { ...BASE_DEFAULTS, ...JSON.parse(fs9.readFileSync(defaultsFile(), "utf8")) };
849
- if (!isLang(saved.sourceLang)) saved.sourceLang = BASE_DEFAULTS.sourceLang;
850
- if (!MAIN_DOCS.includes(saved.mainDoc)) saved.mainDoc = BASE_DEFAULTS.mainDoc;
851
- if (saved.monorepo !== "single") saved.monorepo = "per-project";
852
- return saved;
1244
+ return JSON.parse(fs11.readFileSync(file, "utf8"));
853
1245
  } catch {
854
1246
  return null;
855
1247
  }
856
1248
  }
857
- function writeDefaults(defaults2) {
858
- fs9.mkdirSync(GLOBAL_DIR, { recursive: true });
859
- fs9.writeFileSync(defaultsFile(), JSON.stringify(defaults2, null, 2) + "\n");
1249
+ function hasClaudeHooks(root2) {
1250
+ return [LOCAL, SHARED].some((name) => {
1251
+ const hooks = readSettings2(root2, name)?.hooks ?? {};
1252
+ return Object.keys(EVENTS).every((e) => (hooks[e] ?? []).some(isOurs));
1253
+ });
1254
+ }
1255
+ function writeHooks(root2, name, add) {
1256
+ const settings = readSettings2(root2, name);
1257
+ if (!settings) return false;
1258
+ if (!add && !fs11.existsSync(settingsFile2(root2, name))) return true;
1259
+ const base = cli();
1260
+ settings.hooks ??= {};
1261
+ for (const [event, args2] of Object.entries(EVENTS)) {
1262
+ const list = (settings.hooks[event] ?? []).filter((e) => !isOurs(e));
1263
+ if (add) list.push({ hooks: [{ type: "command", command: `${base} ${args2}` }] });
1264
+ if (list.length) settings.hooks[event] = list;
1265
+ else delete settings.hooks[event];
1266
+ }
1267
+ if (!Object.keys(settings.hooks).length) delete settings.hooks;
1268
+ assertInside(settingsFile2(root2, name));
1269
+ if (!Object.keys(settings).length) return fs11.rmSync(settingsFile2(root2, name), { force: true }), true;
1270
+ fs11.mkdirSync(path11.dirname(settingsFile2(root2, name)), { recursive: true });
1271
+ fs11.writeFileSync(settingsFile2(root2, name), JSON.stringify(settings, null, 2) + "\n");
1272
+ return true;
1273
+ }
1274
+ function installClaudeHooks(root2) {
1275
+ if (!writeHooks(root2, LOCAL, true)) return false;
1276
+ writeHooks(root2, SHARED, false);
1277
+ return true;
1278
+ }
1279
+ function removeClaudeHooks(root2) {
1280
+ writeHooks(root2, LOCAL, false);
1281
+ writeHooks(root2, SHARED, false);
860
1282
  }
861
-
862
- // src/core/i18n.ts
863
- import crypto from "crypto";
864
- import fs11 from "fs";
865
- import path11 from "path";
866
1283
 
867
1284
  // src/core/langdetect.ts
868
1285
  var STOPWORDS = {
@@ -874,8 +1291,20 @@ var STOPWORDS = {
874
1291
  it: ["il", "lo", "la", "gli", "le", "di", "del", "della", "che", "e", "\xE8", "un", "una", "per", "con", "non", "sono", "si", "come", "al", "alla", "pi\xF9", "ma", "suo", "questo", "questa", "quando", "ogni", "deve", "anche"]
875
1292
  };
876
1293
  var SETS = Object.fromEntries(Object.entries(STOPWORDS).map(([l, w]) => [l, new Set(w)]));
1294
+ function cut(text3, open2, close) {
1295
+ let out = "";
1296
+ let i = 0;
1297
+ for (; ; ) {
1298
+ const s = text3.indexOf(open2, i);
1299
+ const e = s < 0 ? -1 : text3.indexOf(close, s + open2.length);
1300
+ if (e < 0) return out + text3.slice(i);
1301
+ out += text3.slice(i, s) + " ";
1302
+ i = e + close.length;
1303
+ }
1304
+ }
1305
+ var SAMPLE = 2e5;
877
1306
  function prose(md) {
878
- return md.replace(/^---\r?\n[\s\S]*?\r?\n---/, " ").replace(/```[\s\S]*?```/g, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/`[^`]*`/g, " ").replace(/\]\([^)]*\)/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/[|#>*_\-[\]()]/g, " ");
1307
+ return cut(cut(md.slice(0, SAMPLE).replace(/^---\r?\n[\s\S]*?\r?\n---/, " "), "```", "```"), "<!--", "-->").replace(/`[^`]*`/g, " ").replace(/\]\([^)]*\)/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/[|#>*_\-[\]()]/g, " ");
879
1308
  }
880
1309
  function detectLang(md) {
881
1310
  const words = prose(md).toLowerCase().match(/[\p{L}]+/gu) ?? [];
@@ -887,43 +1316,146 @@ function detectLang(md) {
887
1316
  return best.lang;
888
1317
  }
889
1318
 
1319
+ // src/core/proposals.ts
1320
+ import fs12 from "fs";
1321
+ import path12 from "path";
1322
+
1323
+ // src/core/git.ts
1324
+ import { execFileSync } from "child_process";
1325
+ import os4 from "os";
1326
+ function runGit(root2, args2) {
1327
+ return execFileSync("git", ["-C", root2, "-c", "core.fsmonitor=false", "-c", "core.quotepath=off", ...args2], {
1328
+ cwd: os4.homedir(),
1329
+ encoding: "utf8",
1330
+ stdio: ["ignore", "pipe", "ignore"],
1331
+ maxBuffer: 32 * 1024 * 1024
1332
+ });
1333
+ }
1334
+ function git(root2, args2) {
1335
+ try {
1336
+ return runGit(root2, args2);
1337
+ } catch (err) {
1338
+ return typeof err?.stdout === "string" && err.stdout ? err.stdout : null;
1339
+ }
1340
+ }
1341
+ var isGitRepo = (root2) => git(root2, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
1342
+ function gitStatuses(root2) {
1343
+ const prefix = git(root2, ["rev-parse", "--show-prefix"])?.trim() ?? "";
1344
+ const out = git(root2, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."]);
1345
+ const result = {};
1346
+ if (!out) return result;
1347
+ const parts = out.split("\0");
1348
+ for (let i = 0; i < parts.length; i++) {
1349
+ const entry = parts[i];
1350
+ if (entry.length < 4) continue;
1351
+ const code = entry.slice(0, 2);
1352
+ let file = entry.slice(3);
1353
+ if (code[0] === "R" || code[0] === "C") i++;
1354
+ if (!file.toLowerCase().endsWith(".md")) continue;
1355
+ if (prefix && file.startsWith(prefix)) file = file.slice(prefix.length);
1356
+ if (file.startsWith(".markode/")) continue;
1357
+ result[file] = code === "??" ? "U" : code.includes("D") ? "D" : code.includes("A") ? "A" : "M";
1358
+ }
1359
+ return result;
1360
+ }
1361
+ function gitDiff(root2, rel) {
1362
+ const status = gitStatuses(root2)[rel];
1363
+ const diff = ["diff", "--no-ext-diff", "--no-textconv"];
1364
+ if (status === "U") return git(root2, [...diff, "--no-index", "--", "/dev/null", rel]) ?? "";
1365
+ const hasHead = git(root2, ["rev-parse", "--verify", "HEAD"]) !== null;
1366
+ return git(root2, hasHead ? [...diff, "HEAD", "--", rel] : [...diff, "--cached", "--", rel]) ?? "";
1367
+ }
1368
+
1369
+ // src/core/proposals.ts
1370
+ var SKILL = "markode-proposals";
1371
+ var skillDirs = (root2) => Object.values(PROJECT_TARGETS).map((t) => path12.join(root2, t, SKILL));
1372
+ function proposalsState(root2) {
1373
+ return { active: skillDirs(root2).some((d) => fs12.existsSync(path12.join(d, "SKILL.md"))), layout: readConfig(root2)?.proposals ?? "general" };
1374
+ }
1375
+ var WHERE = {
1376
+ general: "`docs/proposals/NNN-short-name.md` (NNN: the next number in that folder, counting `applied/`)",
1377
+ structured: "`docs/proposals/<topic>/NNN-short-name.md`, one folder per topic (e.g. `sidebar`, `auth`; NNN: the next number in it, counting its `applied/`); in a monorepo, a proposal for a single project goes in `<project>/docs/proposals/<topic>/` (e.g. `apps/web/docs/proposals/auth/`). Reuse an existing topic folder; ask the user before creating a new one"
1378
+ };
1379
+ function section(layout) {
1380
+ return [
1381
+ "## Proposals",
1382
+ "",
1383
+ "When an answer would be long, has options or a design to validate, or the user asks for one, write a",
1384
+ "temporary proposal the user answers in markode instead of a chat answer (skill: `markode-proposals`).",
1385
+ "",
1386
+ `- Where: ${WHERE[layout]}.`,
1387
+ "- After writing one, run `markode wait <path>` in the background: it returns when the user answers in markode.",
1388
+ "- The user ticks options and comments (`<!-- comment by \u2026 -->`). When it is answered, reply to the comments,",
1389
+ " update every doc that depends on the decision (tasks, phases, architecture\u2026), then archive the proposal:",
1390
+ " `status: applied`, moved to the `applied/` folder next to it. Never read or search archived proposals",
1391
+ " unless the user asks about a past one."
1392
+ ].join("\n");
1393
+ }
1394
+ function mainDocs(root2, mainDoc) {
1395
+ const doc = mainDoc ?? mainDocOf(root2);
1396
+ return doc ? [path12.join(root2, doc)] : [];
1397
+ }
1398
+ function activateProposals(root2, layout, mainDoc) {
1399
+ const text3 = fill(fs12.readFileSync(path12.join(TEMPLATES_DIR, "proposals", "SKILL.md"), "utf8"), { where: WHERE[layout] });
1400
+ for (const target of projectTargets(root2)) {
1401
+ const file = path12.join(root2, PROJECT_TARGETS[target], SKILL, "SKILL.md");
1402
+ assertInside(file);
1403
+ fs12.mkdirSync(path12.dirname(file), { recursive: true });
1404
+ if (!fs12.existsSync(file) || fs12.readFileSync(file, "utf8") !== text3) fs12.writeFileSync(file, text3);
1405
+ }
1406
+ fs12.mkdirSync(path12.join(root2, "docs", "proposals"), { recursive: true });
1407
+ for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, section(layout), false, "proposals");
1408
+ writeConfig(root2, { ...readConfig(root2) ?? { mainDoc, langs: [] }, proposals: layout });
1409
+ }
1410
+ function deactivateProposals(root2, mainDoc) {
1411
+ for (const dir of skillDirs(root2)) if (fs12.existsSync(dir)) assertInside(dir), fs12.rmSync(dir, { recursive: true, force: true });
1412
+ for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, null, false, "proposals");
1413
+ }
1414
+ function authorName(root2) {
1415
+ try {
1416
+ return runGit(root2, ["config", "user.name"]).trim() || null;
1417
+ } catch {
1418
+ return null;
1419
+ }
1420
+ }
1421
+
890
1422
  // src/core/rules.ts
891
- import fs10 from "fs";
892
- import path10 from "path";
893
- import YAML3 from "yaml";
894
- var rulesDir = () => path10.join(GLOBAL_DIR, "rules");
1423
+ import fs13 from "fs";
1424
+ import path13 from "path";
1425
+ import YAML4 from "yaml";
1426
+ var rulesDir = () => path13.join(GLOBAL_DIR, "rules");
895
1427
  var START = "<!-- markode:rules:start -->";
896
1428
  var END = "<!-- markode:rules:end -->";
897
1429
  var slugify = (s) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
898
1430
  function seedRules() {
899
- if (fs10.existsSync(rulesDir())) return;
900
- fs10.mkdirSync(rulesDir(), { recursive: true });
901
- const src = path10.join(TEMPLATES_DIR, "rules");
902
- for (const f of fs10.readdirSync(src)) fs10.copyFileSync(path10.join(src, f), path10.join(rulesDir(), f));
1431
+ if (fs13.existsSync(rulesDir())) return;
1432
+ fs13.mkdirSync(rulesDir(), { recursive: true });
1433
+ const src = path13.join(TEMPLATES_DIR, "rules");
1434
+ for (const f of fs13.readdirSync(src)) fs13.copyFileSync(path13.join(src, f), path13.join(rulesDir(), f));
903
1435
  }
904
1436
  function parseRule(id, text3) {
905
1437
  const m = text3.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
906
- const meta = m ? YAML3.parse(m[1], { logLevel: "error" }) ?? {} : {};
1438
+ const meta = m ? YAML4.parse(m[1], { logLevel: "error" }) ?? {} : {};
907
1439
  return { id, title: meta.title ?? id, body: (m ? m[2] : text3).trim() };
908
1440
  }
909
1441
  function listRules() {
910
1442
  seedRules();
911
- return fs10.readdirSync(rulesDir()).filter((f) => f.endsWith(".md")).map((f) => parseRule(f.slice(0, -3), fs10.readFileSync(path10.join(rulesDir(), f), "utf8"))).sort((a, b) => a.title.localeCompare(b.title));
1443
+ return fs13.readdirSync(rulesDir()).filter((f) => f.endsWith(".md")).map((f) => parseRule(f.slice(0, -3), fs13.readFileSync(path13.join(rulesDir(), f), "utf8"))).sort((a, b) => a.title.localeCompare(b.title));
912
1444
  }
913
1445
  function saveRule(rule) {
914
1446
  seedRules();
915
1447
  const id = rule.id || slugify(rule.title) || `rule-${Date.now()}`;
916
1448
  if (!/^[\w-]+$/.test(id)) throw new Error(`Invalid rule id: ${id}`);
917
1449
  const text3 = `---
918
- ${YAML3.stringify({ title: rule.title }).trim()}
1450
+ ${YAML4.stringify({ title: rule.title }).trim()}
919
1451
  ---
920
1452
  ${rule.body.trim()}
921
1453
  `;
922
- fs10.writeFileSync(path10.join(rulesDir(), `${id}.md`), text3);
1454
+ fs13.writeFileSync(path13.join(rulesDir(), `${id}.md`), text3);
923
1455
  return { id, title: rule.title, body: rule.body.trim() };
924
1456
  }
925
1457
  function deleteRule(id) {
926
- fs10.rmSync(path10.join(rulesDir(), `${path10.basename(id)}.md`), { force: true });
1458
+ fs13.rmSync(path13.join(rulesDir(), `${path13.basename(id)}.md`), { force: true });
927
1459
  }
928
1460
  function appliedRuleIds(docText) {
929
1461
  const section2 = extractSection(docText);
@@ -949,54 +1481,282 @@ function projectSourceLang(root2, mainDoc) {
949
1481
  const doc = mainDoc ?? mainDocOf(root2);
950
1482
  if (!doc) return DEFAULT_SOURCE;
951
1483
  try {
952
- return sourceLangOf(fs10.readFileSync(path10.join(root2, doc), "utf8"));
1484
+ return sourceLangOf(fs13.readFileSync(path13.join(root2, doc), "utf8"));
1485
+ } catch {
1486
+ return DEFAULT_SOURCE;
1487
+ }
1488
+ }
1489
+ function renderSection(rules, sourceLang) {
1490
+ const body = rules.map((r) => `<!-- rule:${r.id} -->
1491
+ ${r.body}`).join("\n\n");
1492
+ return [
1493
+ START,
1494
+ "## Working rules",
1495
+ "",
1496
+ "This project uses markode: follow its `markode` skill (`.claude/skills/markode/` or `.agents/skills/markode/`) when editing any `.md` file.",
1497
+ "",
1498
+ `<!-- markode:source-lang:${sourceLang} -->`,
1499
+ `- Source language of every \`.md\` file: **${LANG_NAMES[sourceLang]}** (\`${sourceLang}\`).`,
1500
+ ...body ? ["", body] : [],
1501
+ END
1502
+ ].join("\n");
1503
+ }
1504
+ function applyRules(docText, rules, sourceLang = sourceLangOf(docText)) {
1505
+ const section2 = renderSection(rules, sourceLang);
1506
+ const existing = extractSection(docText);
1507
+ if (existing) return docText.replace(existing, () => section2);
1508
+ return docText.replace(/\s*$/, "") + "\n\n" + section2 + "\n";
1509
+ }
1510
+ function applyRulesToFile(file, ids, sourceLang) {
1511
+ const text3 = fs13.existsSync(file) ? fs13.readFileSync(file, "utf8") : "";
1512
+ const all = listRules();
1513
+ const written = writtenRules(text3);
1514
+ const rules = (ids ?? appliedRuleIds(text3)).map((id) => all.find((r) => r.id === id) ?? written[id]).filter((r) => !!r);
1515
+ assertInside(file);
1516
+ fs13.writeFileSync(file, applyRules(text3, rules, sourceLang ?? sourceLangOf(text3)));
1517
+ }
1518
+
1519
+ // src/core/features.ts
1520
+ var FEATURES = ["agent", "rules", "proposals", "live", "translations", "dashboard", "skills", "vscode"];
1521
+ var REQUIRES = { rules: "agent", proposals: "agent", live: "agent", translations: "agent" };
1522
+ var FEATURE_INFO = {
1523
+ agent: { label: "AI agent", hint: "teaches your agent (Claude Code, Codex, Cursor\u2026) to work with markode docs; you can comment on any text for it" },
1524
+ rules: { label: "Working rules", hint: "rules the agent follows (ask before committing, protect secrets\u2026), chosen in the Rules tab and written in CLAUDE.md/AGENTS.md" },
1525
+ proposals: { label: "Proposals", hint: "the agent answers big questions with a doc you answer by ticking options and commenting" },
1526
+ live: { label: "Live changes", hint: "Claude Code hears about your edits and comments while it works (personal hooks in .claude/settings.local.json)" },
1527
+ translations: { label: "Translations", hint: "read and edit the docs in other languages; the agent translates them" },
1528
+ dashboard: { label: "Dashboard and tasks", hint: "a panel with the state of docs, phases and tasks; with the AI agent it also creates the task structure" },
1529
+ skills: { label: "Skills", hint: "a tab to browse, edit and add to the project the skills installed for your agents" },
1530
+ vscode: { label: "Hide in VS Code", hint: "hides markode's .markode/ folder (translations, state) from the VS Code explorer" }
1531
+ };
1532
+ var AGENT_SET = ["agent", "rules", "proposals", "live", "translations", "skills", "vscode"];
1533
+ var PROFILE_FEATURES = {
1534
+ viewer: [],
1535
+ agent: AGENT_SET,
1536
+ full: [...AGENT_SET, "dashboard"]
1537
+ };
1538
+ var PROFILE_INFO = {
1539
+ viewer: { label: "Viewer only", hint: "see and edit the .md files; nothing is written to the project" },
1540
+ agent: { label: "Viewer + AI agent", hint: "adds the agent skill, rules, proposals, comments, live changes and translations" },
1541
+ full: { label: "Complete", hint: "all of the above plus the dashboard and a docs structure for vision, phases and tasks" }
1542
+ };
1543
+ var isProfile = (v) => typeof v === "string" && v in PROFILE_FEATURES;
1544
+ var isFeature = (v) => FEATURES.includes(v);
1545
+ function withRequired(list) {
1546
+ const out = /* @__PURE__ */ new Set();
1547
+ for (const f of list) {
1548
+ const need = REQUIRES[f];
1549
+ if (need) out.add(need);
1550
+ out.add(f);
1551
+ }
1552
+ return FEATURES.filter((f) => out.has(f));
1553
+ }
1554
+ var UI_ONLY = ["rules", "translations", "dashboard", "skills"];
1555
+ var SKILL2 = "markode";
1556
+ var skillDirs2 = (root2) => Object.values(PROJECT_TARGETS).map((t) => path14.join(root2, t, SKILL2));
1557
+ var hasAgentSkill = (root2) => skillDirs2(root2).some((d) => fs14.existsSync(path14.join(d, "SKILL.md")));
1558
+ function featureState(root2) {
1559
+ const flags = readConfig(root2)?.features ?? {};
1560
+ const agent = hasAgentSkill(root2);
1561
+ return {
1562
+ agent,
1563
+ rules: agent && flags.rules !== false,
1564
+ proposals: agent && proposalsState(root2).active,
1565
+ live: agent && hasClaudeHooks(root2),
1566
+ translations: agent && flags.translations !== false,
1567
+ dashboard: flags.dashboard !== false,
1568
+ skills: flags.skills !== false,
1569
+ vscode: isHiddenInVsCode(root2)
1570
+ };
1571
+ }
1572
+ function setFlags(root2, patch) {
1573
+ const c = readConfig(root2) ?? { mainDoc: mainDocOf(root2), langs: [] };
1574
+ const { savedRules, ...flags } = patch;
1575
+ writeConfig(root2, { ...c, features: { ...c.features, ...flags }, ..."savedRules" in patch ? { savedRules } : {} });
1576
+ }
1577
+ function detectSourceLang(root2, fallback) {
1578
+ const votes = /* @__PURE__ */ new Map();
1579
+ for (const f of flattenFiles(scanTree(root2)).slice(0, 200)) {
1580
+ try {
1581
+ const lang = detectLang(fs14.readFileSync(path14.join(root2, f.path), "utf8"));
1582
+ if (lang) votes.set(lang, (votes.get(lang) ?? 0) + 1);
1583
+ } catch {
1584
+ }
1585
+ }
1586
+ return [...votes].sort((a, b) => b[1] - a[1])[0]?.[0] ?? fallback;
1587
+ }
1588
+ function ensureMainDoc(root2, preferred) {
1589
+ const existing = mainDocOf(root2);
1590
+ if (existing) return existing;
1591
+ const doc = preferred ?? (projectTargets(root2).includes("claude") ? "CLAUDE.md" : "AGENTS.md");
1592
+ createMainDoc(root2, doc);
1593
+ return doc;
1594
+ }
1595
+ function installAgentSkill(root2) {
1596
+ const src = path14.join(TEMPLATES_DIR, "skill", "SKILL.md");
1597
+ for (const target of projectTargets(root2)) {
1598
+ const dir = path14.join(root2, PROJECT_TARGETS[target], SKILL2);
1599
+ fs14.mkdirSync(dir, { recursive: true });
1600
+ const dest = path14.join(dir, "SKILL.md");
1601
+ assertInside(dest);
1602
+ const text3 = fs14.readFileSync(src, "utf8");
1603
+ if (!fs14.existsSync(dest) || fs14.readFileSync(dest, "utf8") !== text3) fs14.writeFileSync(dest, text3);
1604
+ }
1605
+ }
1606
+ function allMainDocs(root2) {
1607
+ return ["", ...findSubprojects(scanTree(root2))].map((d) => contentDoc(path14.join(root2, d))).filter((f) => !!f);
1608
+ }
1609
+ var METHOD_SKILLS = () => fs14.readdirSync(path14.join(TEMPLATES_DIR, "method", "skills"));
1610
+ function setFeature(root2, feature, on, opts = {}) {
1611
+ const state = featureState(root2);
1612
+ const changed = {};
1613
+ if (on) {
1614
+ const need = REQUIRES[feature];
1615
+ if (need && !state[need]) Object.assign(changed, setFeature(root2, need, true, opts));
1616
+ } else if (feature === "agent") {
1617
+ for (const f of FEATURES) if (REQUIRES[f] === "agent" && state[f]) Object.assign(changed, setFeature(root2, f, false, opts));
1618
+ }
1619
+ if (state[feature] === on && !UI_ONLY.includes(feature)) return changed;
1620
+ apply(root2, feature, on, opts);
1621
+ changed[feature] = on;
1622
+ return changed;
1623
+ }
1624
+ function apply(root2, feature, on, opts) {
1625
+ switch (feature) {
1626
+ case "agent": {
1627
+ if (on) {
1628
+ const lang = readConfig(root2)?.savedSourceLang ?? detectSourceLang(root2, opts.sourceLang ?? "en");
1629
+ const doc = path14.join(root2, ensureMainDoc(root2, opts.mainDoc));
1630
+ installAgentSkill(root2);
1631
+ const text3 = fs14.readFileSync(doc, "utf8");
1632
+ const declared = /<!-- markode:source-lang:\w+ -->/.test(text3);
1633
+ applyRulesToFile(doc, appliedRuleIds(text3), declared ? sourceLangOf(text3) : lang);
1634
+ } else {
1635
+ const doc = mainDocOf(root2);
1636
+ const c = readConfig(root2);
1637
+ if (doc && c) writeConfig(root2, { ...c, savedSourceLang: sourceLangOf(fs14.readFileSync(path14.join(root2, doc), "utf8")) });
1638
+ for (const d of skillDirs2(root2)) if (fs14.existsSync(d)) assertInside(d), fs14.rmSync(d, { recursive: true, force: true });
1639
+ for (const doc2 of allMainDocs(root2)) writeManagedBlock(doc2, null, false, "rules");
1640
+ }
1641
+ return;
1642
+ }
1643
+ case "rules": {
1644
+ const doc = mainDocOf(root2);
1645
+ const file = doc && path14.join(root2, doc);
1646
+ if (on) {
1647
+ const saved = readConfig(root2)?.savedRules ?? [];
1648
+ const current = file ? appliedRuleIds(fs14.readFileSync(file, "utf8")) : [];
1649
+ const ids = current.length ? current : saved.length ? saved : opts.rules ?? [];
1650
+ if (file && ids.length) applyRulesToFile(file, ids);
1651
+ setFlags(root2, { rules: true, savedRules: void 0 });
1652
+ } else {
1653
+ const ids = file ? appliedRuleIds(fs14.readFileSync(file, "utf8")) : [];
1654
+ if (file) applyRulesToFile(file, []);
1655
+ setFlags(root2, { rules: false, savedRules: ids.length ? ids : void 0 });
1656
+ }
1657
+ return;
1658
+ }
1659
+ case "proposals":
1660
+ if (on) activateProposals(root2, readConfig(root2)?.proposals ?? (findSubprojects(scanTree(root2)).length ? "structured" : "general"), mainDocOf(root2));
1661
+ else deactivateProposals(root2, mainDocOf(root2));
1662
+ return;
1663
+ case "live":
1664
+ if (on) installClaudeHooks(root2);
1665
+ else removeClaudeHooks(root2);
1666
+ return;
1667
+ case "translations":
1668
+ setFlags(root2, { translations: on });
1669
+ return;
1670
+ case "dashboard": {
1671
+ setFlags(root2, { dashboard: on });
1672
+ if (on && hasAgentSkill(root2) && !detectWorkflow(root2) && (readConfig(root2)?.workflow ?? "none") === "none") {
1673
+ const mainDoc = ensureMainDoc(root2, opts.mainDoc);
1674
+ setupMethod(root2, mainDoc, opts.monorepo ?? "per-project");
1675
+ const c = readConfig(root2);
1676
+ writeConfig(root2, { ...c, workflow: "markode" });
1677
+ }
1678
+ if (!on && readConfig(root2)?.workflow === "markode") {
1679
+ for (const name of METHOD_SKILLS())
1680
+ for (const t of Object.values(PROJECT_TARGETS)) {
1681
+ const dir = path14.join(root2, t, name);
1682
+ if (fs14.existsSync(dir)) assertInside(dir), fs14.rmSync(dir, { recursive: true, force: true });
1683
+ }
1684
+ for (const doc of allMainDocs(root2)) writeManagedBlock(doc, null, false, "workflow");
1685
+ const c = readConfig(root2);
1686
+ writeConfig(root2, { ...c, workflow: "none" });
1687
+ }
1688
+ return;
1689
+ }
1690
+ case "skills":
1691
+ setFlags(root2, { skills: on });
1692
+ return;
1693
+ case "vscode":
1694
+ if (on) hideInVsCode(root2);
1695
+ else showInVsCode(root2);
1696
+ return;
1697
+ }
1698
+ }
1699
+ function refreshInstalled(root2) {
1700
+ if (!hasAgentSkill(root2)) return;
1701
+ installAgentSkill(root2);
1702
+ const proposals = proposalsState(root2);
1703
+ if (proposals.active) activateProposals(root2, proposals.layout, mainDocOf(root2));
1704
+ if (readConfig(root2)?.workflow === "markode") installMethodSkills(root2);
1705
+ }
1706
+ function ignoreStateDir(root2) {
1707
+ const file = path14.join(stateDir(root2), ".gitignore");
1708
+ if (fs14.existsSync(file)) return;
1709
+ assertInside(file);
1710
+ fs14.mkdirSync(stateDir(root2), { recursive: true });
1711
+ fs14.writeFileSync(file, "# markode's personal state: never committed\n*\n");
1712
+ }
1713
+
1714
+ // src/core/defaults.ts
1715
+ var BASE_DEFAULTS = {
1716
+ mode: "ask",
1717
+ features: PROFILE_FEATURES.agent,
1718
+ mainDoc: "CLAUDE.md",
1719
+ monorepo: "per-project",
1720
+ sourceLang: "en",
1721
+ rules: [],
1722
+ rulesInSubprojects: false
1723
+ };
1724
+ var defaultsFile = () => path15.join(GLOBAL_DIR, "defaults.json");
1725
+ function readDefaults() {
1726
+ try {
1727
+ const raw = JSON.parse(fs15.readFileSync(defaultsFile(), "utf8"));
1728
+ const saved = { ...BASE_DEFAULTS, ...raw };
1729
+ if (!isLang(saved.sourceLang)) saved.sourceLang = BASE_DEFAULTS.sourceLang;
1730
+ if (!MAIN_DOCS.includes(saved.mainDoc)) saved.mainDoc = BASE_DEFAULTS.mainDoc;
1731
+ if (saved.monorepo !== "single") saved.monorepo = "per-project";
1732
+ saved.features = Array.isArray(raw.features) ? raw.features.filter(isFeature) : PROFILE_FEATURES.agent.filter((f) => (f !== "live" || raw.claudeHooks !== false) && (f !== "vscode" || raw.hideInVsCode !== false)).concat(raw.workflow === "markode" ? ["dashboard"] : []);
1733
+ return saved;
953
1734
  } catch {
954
- return DEFAULT_SOURCE;
1735
+ return null;
955
1736
  }
956
1737
  }
957
- function renderSection(rules, sourceLang) {
958
- const body = rules.map((r) => `<!-- rule:${r.id} -->
959
- ${r.body}`).join("\n\n");
960
- return [
961
- START,
962
- "## Working rules",
963
- "",
964
- "This project uses markode: follow `.claude/skills/markode/SKILL.md` when editing any `.md` file.",
965
- "",
966
- `<!-- markode:source-lang:${sourceLang} -->`,
967
- `- Source language of every \`.md\` file: **${LANG_NAMES[sourceLang]}** (\`${sourceLang}\`).`,
968
- ...body ? ["", body] : [],
969
- END
970
- ].join("\n");
971
- }
972
- function applyRules(docText, rules, sourceLang = sourceLangOf(docText)) {
973
- const section2 = renderSection(rules, sourceLang);
974
- const existing = extractSection(docText);
975
- if (existing) return docText.replace(existing, () => section2);
976
- return docText.replace(/\s*$/, "") + "\n\n" + section2 + "\n";
977
- }
978
- function applyRulesToFile(file, ids, sourceLang) {
979
- const text3 = fs10.existsSync(file) ? fs10.readFileSync(file, "utf8") : "";
980
- const all = listRules();
981
- const written = writtenRules(text3);
982
- const rules = (ids ?? appliedRuleIds(text3)).map((id) => all.find((r) => r.id === id) ?? written[id]).filter((r) => !!r);
983
- fs10.writeFileSync(file, applyRules(text3, rules, sourceLang ?? sourceLangOf(text3)));
1738
+ function writeDefaults(defaults) {
1739
+ fs15.mkdirSync(GLOBAL_DIR, { recursive: true });
1740
+ fs15.writeFileSync(defaultsFile(), JSON.stringify(defaults, null, 2) + "\n");
984
1741
  }
985
1742
 
986
1743
  // src/core/i18n.ts
987
- var stateFile = (root2) => path11.join(stateDir(root2), "state.json");
988
- var translationPath = (root2, lang, rel) => path11.join(stateDir(root2), "i18n", lang, rel);
989
- var hash = (text3) => crypto.createHash("sha1").update(text3).digest("hex").slice(0, 12);
1744
+ import crypto2 from "crypto";
1745
+ import fs16 from "fs";
1746
+ import path16 from "path";
1747
+ var stateFile = (root2) => path16.join(stateDir(root2), "state.json");
1748
+ var translationPath = (root2, lang, rel) => path16.join(stateDir(root2), "i18n", lang, rel);
1749
+ var hash = (text3) => crypto2.createHash("sha1").update(text3).digest("hex").slice(0, 12);
990
1750
  var readText2 = (abs) => {
991
1751
  try {
992
- return fs11.readFileSync(abs, "utf8");
1752
+ return fs16.readFileSync(abs, "utf8");
993
1753
  } catch {
994
1754
  return null;
995
1755
  }
996
1756
  };
997
1757
  function readState(root2) {
998
1758
  try {
999
- const raw = JSON.parse(fs11.readFileSync(stateFile(root2), "utf8"));
1759
+ const raw = JSON.parse(fs16.readFileSync(stateFile(root2), "utf8"));
1000
1760
  if (!raw.translations) return { translations: raw, keepLanguage: {} };
1001
1761
  return { translations: raw.translations ?? {}, keepLanguage: raw.keepLanguage ?? {} };
1002
1762
  } catch {
@@ -1004,11 +1764,12 @@ function readState(root2) {
1004
1764
  }
1005
1765
  }
1006
1766
  function writeState(root2, state) {
1007
- fs11.mkdirSync(stateDir(root2), { recursive: true });
1008
- fs11.writeFileSync(stateFile(root2), JSON.stringify(state, null, 2) + "\n");
1767
+ fs16.mkdirSync(stateDir(root2), { recursive: true });
1768
+ assertInside(stateFile(root2));
1769
+ fs16.writeFileSync(stateFile(root2), JSON.stringify(state, null, 2) + "\n");
1009
1770
  }
1010
1771
  function fileStatus(root2, lang, rel) {
1011
- const source = readText2(path11.join(root2, rel));
1772
+ const source = readText2(path16.join(root2, rel));
1012
1773
  const translation = readText2(translationPath(root2, lang, rel));
1013
1774
  if (translation === null || source === null) return "missing";
1014
1775
  const recorded = readState(root2).translations[lang]?.[rel];
@@ -1025,14 +1786,14 @@ function allStatuses(root2, lang) {
1025
1786
  path: f.path,
1026
1787
  lang,
1027
1788
  status: fileStatus(root2, lang, f.path),
1028
- translation: path11.relative(root2, translationPath(root2, lang, f.path))
1789
+ translation: path16.relative(root2, translationPath(root2, lang, f.path))
1029
1790
  }));
1030
1791
  }
1031
1792
  function foreignFiles(root2, sourceLang) {
1032
1793
  const { keepLanguage: keepLanguage2 } = readState(root2);
1033
1794
  const out = [];
1034
1795
  for (const f of flattenFiles(scanTree(root2))) {
1035
- const text3 = readText2(path11.join(root2, f.path));
1796
+ const text3 = readText2(path16.join(root2, f.path));
1036
1797
  const detected = text3 ? detectLang(text3) : null;
1037
1798
  if (detected && detected !== sourceLang && keepLanguage2[f.path] !== detected) out.push({ path: f.path, detected });
1038
1799
  }
@@ -1052,17 +1813,17 @@ function syncReport(root2) {
1052
1813
  };
1053
1814
  }
1054
1815
  function markSynced(root2, lang, rel) {
1055
- const source = readText2(path11.join(root2, rel));
1816
+ const source = readText2(path16.join(root2, rel));
1056
1817
  const translation = readText2(translationPath(root2, lang, rel));
1057
1818
  if (source === null) throw new Error(`Source not found: ${rel}`);
1058
- if (translation === null) throw new Error(`Translation not found: ${path11.relative(root2, translationPath(root2, lang, rel))}`);
1819
+ if (translation === null) throw new Error(`Translation not found: ${path16.relative(root2, translationPath(root2, lang, rel))}`);
1059
1820
  const state = readState(root2);
1060
1821
  state.translations[lang] ??= {};
1061
1822
  state.translations[lang][rel] = { source: hash(source), translation: hash(translation) };
1062
1823
  writeState(root2, state);
1063
1824
  }
1064
1825
  function keepLanguage(root2, rel) {
1065
- const text3 = readText2(path11.join(root2, rel));
1826
+ const text3 = readText2(path16.join(root2, rel));
1066
1827
  const detected = text3 ? detectLang(text3) : null;
1067
1828
  if (!detected) throw new Error(`Could not detect the language of ${rel}`);
1068
1829
  const state = readState(root2);
@@ -1072,14 +1833,14 @@ function keepLanguage(root2, rel) {
1072
1833
  }
1073
1834
 
1074
1835
  // src/core/wait.ts
1075
- import fs12 from "fs";
1076
- import path12 from "path";
1077
- import YAML4 from "yaml";
1078
- var waitsDir = (root2) => path12.join(stateDir(root2), "waits");
1079
- var pidFile = (root2, rel) => path12.join(waitsDir(root2), `${encodeURIComponent(rel)}.pid`);
1080
- var frontmatter = (text3) => {
1836
+ import fs17 from "fs";
1837
+ import path17 from "path";
1838
+ import YAML5 from "yaml";
1839
+ var waitsDir = (root2) => path17.join(stateDir(root2), "waits");
1840
+ var pidFile = (root2, rel) => path17.join(waitsDir(root2), `${encodeURIComponent(rel)}.pid`);
1841
+ var frontmatter2 = (text3) => {
1081
1842
  try {
1082
- return YAML4.parse(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text3)?.[1] ?? "", { logLevel: "error" }) ?? {};
1843
+ return YAML5.parse(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text3)?.[1] ?? "", { logLevel: "error" }) ?? {};
1083
1844
  } catch {
1084
1845
  return {};
1085
1846
  }
@@ -1095,30 +1856,31 @@ var isAlive = (pid) => {
1095
1856
  function waitingOn(root2) {
1096
1857
  let names2 = [];
1097
1858
  try {
1098
- names2 = fs12.readdirSync(waitsDir(root2)).filter((f) => f.endsWith(".pid"));
1859
+ names2 = fs17.readdirSync(waitsDir(root2)).filter((f) => f.endsWith(".pid"));
1099
1860
  } catch {
1100
1861
  }
1101
1862
  return names2.flatMap((f) => {
1102
- const pid = Number(fs12.readFileSync(path12.join(waitsDir(root2), f), "utf8"));
1863
+ const pid = Number(fs17.readFileSync(path17.join(waitsDir(root2), f), "utf8"));
1103
1864
  return isAlive(pid) ? [decodeURIComponent(f.slice(0, -4))] : [];
1104
1865
  });
1105
1866
  }
1106
1867
  function waitForAnswer(root2, rel) {
1107
- const abs = path12.join(root2, rel);
1108
- fs12.mkdirSync(waitsDir(root2), { recursive: true });
1109
- fs12.writeFileSync(pidFile(root2, rel), String(process.pid));
1110
- const cleanup = () => fs12.rmSync(pidFile(root2, rel), { force: true });
1868
+ const abs = path17.join(root2, rel);
1869
+ fs17.mkdirSync(waitsDir(root2), { recursive: true });
1870
+ assertInside(pidFile(root2, rel));
1871
+ fs17.writeFileSync(pidFile(root2, rel), String(process.pid));
1872
+ const cleanup = () => fs17.rmSync(pidFile(root2, rel), { force: true });
1111
1873
  process.once("exit", cleanup);
1112
1874
  for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(sig, () => process.exit(130));
1113
1875
  return new Promise((resolve) => {
1114
1876
  const poll = () => {
1115
1877
  let text3;
1116
1878
  try {
1117
- text3 = fs12.readFileSync(abs, "utf8");
1879
+ text3 = fs17.readFileSync(abs, "utf8");
1118
1880
  } catch {
1119
1881
  return resolve("gone");
1120
1882
  }
1121
- const fm = frontmatter(text3);
1883
+ const fm = frontmatter2(text3);
1122
1884
  if (fm.type !== "proposal") return resolve("gone");
1123
1885
  if (fm.status === "answered") return resolve("answered");
1124
1886
  setTimeout(poll, 1500);
@@ -1128,8 +1890,8 @@ function waitForAnswer(root2, rel) {
1128
1890
  }
1129
1891
  var COMMENT = /^\s*<!-- (comment|reply) by (.+?), (\d{4}-\d{2}-\d{2})(?:, on "((?:[^"\\]|\\.)*)")?:\s?([\s\S]*?)\s*-->\s*$/;
1130
1892
  function answerSummary(root2, rel) {
1131
- const text3 = fs12.readFileSync(path12.join(root2, rel), "utf8");
1132
- const fm = frontmatter(text3);
1893
+ const text3 = fs17.readFileSync(path17.join(root2, rel), "utf8");
1894
+ const fm = frontmatter2(text3);
1133
1895
  const body = text3.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
1134
1896
  const lines = body.split("\n");
1135
1897
  const chosen = [];
@@ -1202,236 +1964,13 @@ function answerSummary(root2, rel) {
1202
1964
  }
1203
1965
 
1204
1966
  // src/server/index.ts
1205
- import fs15 from "fs";
1967
+ import fs18 from "fs";
1206
1968
  import http from "http";
1207
- import path15 from "path";
1969
+ import path18 from "path";
1208
1970
  import { watch } from "chokidar";
1209
1971
  import open from "open";
1210
1972
  import pc from "picocolors";
1211
1973
  import { WebSocketServer } from "ws";
1212
-
1213
- // src/core/git.ts
1214
- import { execFileSync } from "child_process";
1215
- function git(root2, args2) {
1216
- try {
1217
- return execFileSync("git", ["-c", "core.quotepath=off", ...args2], { cwd: root2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 32 * 1024 * 1024 });
1218
- } catch (err) {
1219
- return typeof err?.stdout === "string" && err.stdout ? err.stdout : null;
1220
- }
1221
- }
1222
- var isGitRepo = (root2) => git(root2, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
1223
- function gitStatuses(root2) {
1224
- const prefix = git(root2, ["rev-parse", "--show-prefix"])?.trim() ?? "";
1225
- const out = git(root2, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."]);
1226
- const result = {};
1227
- if (!out) return result;
1228
- const parts = out.split("\0");
1229
- for (let i = 0; i < parts.length; i++) {
1230
- const entry = parts[i];
1231
- if (entry.length < 4) continue;
1232
- const code = entry.slice(0, 2);
1233
- let file = entry.slice(3);
1234
- if (code[0] === "R" || code[0] === "C") i++;
1235
- if (!file.toLowerCase().endsWith(".md")) continue;
1236
- if (prefix && file.startsWith(prefix)) file = file.slice(prefix.length);
1237
- if (file.startsWith(".markode/")) continue;
1238
- result[file] = code === "??" ? "U" : code.includes("D") ? "D" : code.includes("A") ? "A" : "M";
1239
- }
1240
- return result;
1241
- }
1242
- function gitDiff(root2, rel) {
1243
- const status = gitStatuses(root2)[rel];
1244
- if (status === "U") return git(root2, ["diff", "--no-index", "--", "/dev/null", rel]) ?? "";
1245
- const hasHead = git(root2, ["rev-parse", "--verify", "HEAD"]) !== null;
1246
- return git(root2, hasHead ? ["diff", "HEAD", "--", rel] : ["diff", "--cached", "--", rel]) ?? "";
1247
- }
1248
-
1249
- // src/core/proposals.ts
1250
- import { execFileSync as execFileSync2 } from "child_process";
1251
- import fs13 from "fs";
1252
- import path13 from "path";
1253
- var SKILL = path13.join(".claude", "skills", "markode-proposals");
1254
- function proposalsState(root2) {
1255
- return { active: fs13.existsSync(path13.join(root2, SKILL, "SKILL.md")), layout: readConfig(root2)?.proposals ?? "general" };
1256
- }
1257
- var WHERE = {
1258
- general: "`docs/proposals/NNN-short-name.md` (NNN: the next number in that folder)",
1259
- structured: "`docs/proposals/<topic>/NNN-short-name.md`, one folder per topic (e.g. `sidebar`, `auth`); in a monorepo, a proposal for a single project goes in `<project>/docs/proposals/<topic>/` (e.g. `apps/web/docs/proposals/auth/`). Reuse an existing topic folder; ask the user before creating a new one"
1260
- };
1261
- function section(layout) {
1262
- return [
1263
- "## Proposals",
1264
- "",
1265
- "When an answer would be long, has options or a design to validate, or the user asks for one, write a",
1266
- "temporary proposal the user answers in markode instead of a chat answer (skill: `markode-proposals`).",
1267
- "",
1268
- `- Where: ${WHERE[layout]}.`,
1269
- "- After writing one, run `markode wait <path>` in the background: it returns when the user answers in markode.",
1270
- "- The user ticks options and comments (`<!-- comment by \u2026 -->`). When it is answered, reply to the comments,",
1271
- " update every doc that depends on the decision (tasks, phases, architecture\u2026), then delete the proposal."
1272
- ].join("\n");
1273
- }
1274
- function mainDocs(root2, mainDoc) {
1275
- const doc = mainDoc ?? mainDocOf(root2);
1276
- return doc ? [path13.join(root2, doc)] : [];
1277
- }
1278
- function activateProposals(root2, layout, mainDoc) {
1279
- const dir = path13.join(root2, SKILL);
1280
- fs13.mkdirSync(dir, { recursive: true });
1281
- fs13.writeFileSync(path13.join(dir, "SKILL.md"), fill(fs13.readFileSync(path13.join(TEMPLATES_DIR, "proposals", "SKILL.md"), "utf8"), { where: WHERE[layout] }));
1282
- fs13.mkdirSync(path13.join(root2, "docs", "proposals"), { recursive: true });
1283
- for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, section(layout), false, "proposals");
1284
- writeConfig(root2, { ...readConfig(root2) ?? { mainDoc, langs: [] }, proposals: layout });
1285
- }
1286
- function deactivateProposals(root2, mainDoc) {
1287
- fs13.rmSync(path13.join(root2, SKILL), { recursive: true, force: true });
1288
- for (const file of mainDocs(root2, mainDoc)) writeManagedBlock(file, null, false, "proposals");
1289
- }
1290
- function authorName(root2) {
1291
- try {
1292
- return execFileSync2("git", ["config", "user.name"], { cwd: root2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
1293
- } catch {
1294
- return null;
1295
- }
1296
- }
1297
-
1298
- // src/core/skills.ts
1299
- import fs14 from "fs";
1300
- import os2 from "os";
1301
- import path14 from "path";
1302
- import YAML5 from "yaml";
1303
- var SOURCE_DIRS = {
1304
- claude: [".claude", "skills"],
1305
- // Shared folder read by Codex, Copilot and Gemini CLI, and where `npx skills` installs.
1306
- agents: [".agents", "skills"],
1307
- codex: [".codex", "skills"],
1308
- gemini: [".gemini", "skills"],
1309
- copilot: [".copilot", "skills"],
1310
- cursor: [".cursor", "skills"]
1311
- };
1312
- var PROJECT_TARGETS = { claude: ".claude/skills", agents: ".agents/skills" };
1313
- var sourceDir = (source) => path14.join(os2.homedir(), ...SOURCE_DIRS[source]);
1314
- var display = (p3) => p3.startsWith(os2.homedir() + path14.sep) ? "~" + p3.slice(os2.homedir().length) : p3;
1315
- function skillSources() {
1316
- return Object.keys(SOURCE_DIRS).map((id) => ({ id, path: display(sourceDir(id)), exists: fs14.existsSync(sourceDir(id)) }));
1317
- }
1318
- var tagsFile = () => path14.join(GLOBAL_DIR, "skills.json");
1319
- function readTags() {
1320
- try {
1321
- return JSON.parse(fs14.readFileSync(tagsFile(), "utf8")).tags ?? {};
1322
- } catch {
1323
- return {};
1324
- }
1325
- }
1326
- function setSkillTags(name, tags) {
1327
- const all = readTags();
1328
- const clean = [...new Set(tags.map((t) => t.trim().toLowerCase()).filter(Boolean))];
1329
- if (clean.length) all[name] = clean;
1330
- else delete all[name];
1331
- fs14.mkdirSync(GLOBAL_DIR, { recursive: true });
1332
- fs14.writeFileSync(tagsFile(), JSON.stringify({ tags: all }, null, 2) + "\n");
1333
- return clean;
1334
- }
1335
- function findSkills(dir, source, managed, depth = 0) {
1336
- let entries;
1337
- try {
1338
- entries = fs14.readdirSync(dir, { withFileTypes: true });
1339
- } catch {
1340
- return [];
1341
- }
1342
- return entries.flatMap((e) => {
1343
- const abs = path14.join(dir, e.name);
1344
- let real;
1345
- try {
1346
- if (!fs14.statSync(abs).isDirectory()) return [];
1347
- real = fs14.realpathSync(abs);
1348
- } catch {
1349
- return [];
1350
- }
1351
- if (fs14.existsSync(path14.join(abs, "SKILL.md"))) return [{ dir: abs, real, source, managed }];
1352
- return depth < 2 ? findSkills(abs, source, managed || e.name === "synced" || e.name.startsWith("."), depth + 1) : [];
1353
- });
1354
- }
1355
- function frontmatter2(text3) {
1356
- const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text3);
1357
- if (!m) return null;
1358
- try {
1359
- const data = YAML5.parse(m[1], { logLevel: "error" });
1360
- return data && typeof data === "object" ? data : null;
1361
- } catch {
1362
- return null;
1363
- }
1364
- }
1365
- function listFiles(dir) {
1366
- try {
1367
- return fs14.readdirSync(dir, { recursive: true, withFileTypes: true }).map((e) => ({ e, rel: path14.relative(dir, path14.join(e.parentPath ?? e.path ?? dir, e.name)).split(path14.sep).join("/") })).filter(({ e, rel }) => e.isFile() && rel !== "SKILL.md").map(({ rel }) => rel).sort().slice(0, 100);
1368
- } catch {
1369
- return [];
1370
- }
1371
- }
1372
- function listSkills(root2) {
1373
- const byReal = /* @__PURE__ */ new Map();
1374
- for (const source of Object.keys(SOURCE_DIRS)) {
1375
- for (const f of findSkills(sourceDir(source), source, false)) byReal.set(f.real, [...byReal.get(f.real) ?? [], f]);
1376
- }
1377
- const userTags = readTags();
1378
- const skills = [];
1379
- for (const [real, found] of byReal) {
1380
- const text3 = fs14.readFileSync(path14.join(real, "SKILL.md"), "utf8");
1381
- const fm = frontmatter2(text3);
1382
- const folder = path14.basename(found[0].dir);
1383
- const name = typeof fm?.name === "string" ? fm.name : folder;
1384
- const description = typeof fm?.description === "string" ? fm.description.trim() : "";
1385
- const issues = [];
1386
- if (!fm) issues.push("frontmatter");
1387
- else {
1388
- if (typeof fm.name !== "string" || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fm.name) || fm.name.length > 64) issues.push("name");
1389
- else if (fm.name !== folder) issues.push("name-dir");
1390
- if (!description) issues.push("description");
1391
- else if (description.length > 1024) issues.push("description-long");
1392
- }
1393
- const declared = fm?.metadata?.tags;
1394
- const metaTags = Array.isArray(declared) ? declared.map(String) : typeof declared === "string" ? declared.split(",") : [];
1395
- skills.push({
1396
- key: real,
1397
- name,
1398
- folder,
1399
- description,
1400
- locations: found.map((f) => ({ source: f.source, path: display(f.dir) })),
1401
- managed: found.every((f) => f.managed),
1402
- files: listFiles(real),
1403
- tags: [.../* @__PURE__ */ new Set([...userTags[name] ?? [], ...metaTags.map((t) => t.trim().toLowerCase()).filter(Boolean)])],
1404
- issues,
1405
- installed: Object.keys(PROJECT_TARGETS).filter((t) => fs14.existsSync(path14.join(root2, PROJECT_TARGETS[t], folder, "SKILL.md")))
1406
- });
1407
- }
1408
- return skills.sort((a, b) => a.name.localeCompare(b.name));
1409
- }
1410
- function getSkill(root2, key) {
1411
- const skill = typeof key === "string" ? listSkills(root2).find((s) => s.key === key) : void 0;
1412
- if (!skill) throw Object.assign(new Error("Skill not found"), { code: "ENOENT" });
1413
- return skill;
1414
- }
1415
- function projectTargets(root2) {
1416
- const has2 = (p3) => fs14.existsSync(path14.join(root2, p3));
1417
- const out = [];
1418
- if (has2(".claude") || has2("CLAUDE.md")) out.push("claude");
1419
- if (has2(".agents") || has2("AGENTS.md") || has2(".codex") || has2(".gemini") || has2(".github/skills")) out.push("agents");
1420
- return out.length ? out : ["claude"];
1421
- }
1422
- function installSkill2(root2, skill, targets, overwrite) {
1423
- const dests = targets.map((t) => path14.join(root2, PROJECT_TARGETS[t], skill.folder));
1424
- const existing = dests.filter((d) => fs14.existsSync(d));
1425
- if (existing.length && !overwrite) return { conflict: existing.map((d) => path14.relative(root2, d).split(path14.sep).join("/")) };
1426
- for (const dest of dests) {
1427
- fs14.rmSync(dest, { recursive: true, force: true });
1428
- fs14.mkdirSync(path14.dirname(dest), { recursive: true });
1429
- fs14.cpSync(skill.key, dest, { recursive: true, dereference: true });
1430
- }
1431
- return { ok: true };
1432
- }
1433
-
1434
- // src/server/index.ts
1435
1974
  var IMAGE_MIME = {
1436
1975
  ".png": "image/png",
1437
1976
  ".jpg": "image/jpeg",
@@ -1458,9 +1997,13 @@ var HttpError = class extends Error {
1458
1997
  }
1459
1998
  status;
1460
1999
  };
2000
+ var MAX_BODY = 20 * 1024 * 1024;
1461
2001
  async function readBody(req) {
1462
2002
  let data = "";
1463
- for await (const chunk of req) data += chunk;
2003
+ for await (const chunk of req) {
2004
+ data += chunk;
2005
+ if (data.length > MAX_BODY) throw new HttpError(413, "Request too large");
2006
+ }
1464
2007
  if (!data) return {};
1465
2008
  if (!req.headers["content-type"]?.startsWith("application/json")) throw new HttpError(415, "Expected application/json");
1466
2009
  return JSON.parse(data);
@@ -1470,6 +2013,8 @@ function isLocalRequest(req) {
1470
2013
  const host = req.headers.host ?? "";
1471
2014
  try {
1472
2015
  if (!LOCAL_HOSTS.has(new URL(`http://${host}`).hostname)) return false;
2016
+ const site = req.headers["sec-fetch-site"];
2017
+ if (site && site !== "same-origin" && site !== "none") return false;
1473
2018
  return !req.headers.origin || new URL(req.headers.origin).host === host;
1474
2019
  } catch {
1475
2020
  return false;
@@ -1477,8 +2022,9 @@ function isLocalRequest(req) {
1477
2022
  }
1478
2023
  async function startServer({ root: root2, port, open: openBrowser }) {
1479
2024
  const config = () => readConfig(root2) ?? { mainDoc: null, langs: [] };
2025
+ const langs = () => featureState(root2).translations ? config().langs : [];
1480
2026
  const sourceLang = () => projectSourceLang(root2, readConfig(root2)?.mainDoc);
1481
- const viewLang = (value) => isLangCode(value) && value !== sourceLang() && config().langs.includes(value) ? value : null;
2027
+ const viewLang = (value) => isLangCode(value) && value !== sourceLang() && langs().includes(value) ? value : null;
1482
2028
  const mdPath = (rel) => {
1483
2029
  if (typeof rel !== "string" || !rel.toLowerCase().endsWith(".md")) throw new HttpError(400, "Expected a .md path");
1484
2030
  safeResolve(root2, rel);
@@ -1488,24 +2034,36 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1488
2034
  if (typeof rel !== "string" || !rel.trim()) throw new HttpError(400, "Expected a path");
1489
2035
  const abs = safeResolve(root2, rel);
1490
2036
  if (abs === root2) throw new HttpError(400, "Refusing to delete the project root");
1491
- if (!fs15.existsSync(abs)) throw new HttpError(404, "Not found");
2037
+ if (!fs18.existsSync(abs)) throw new HttpError(404, "Not found");
1492
2038
  return abs;
1493
2039
  };
2040
+ const translationFile = (lang, rel) => {
2041
+ const file = translationPath(root2, lang, rel);
2042
+ assertInside(file, root2);
2043
+ return file;
2044
+ };
1494
2045
  const routes = {
1495
2046
  "GET /api/info": () => {
1496
2047
  const c = config();
1497
2048
  return {
1498
- name: path15.basename(root2),
2049
+ name: path18.basename(root2),
1499
2050
  mainDoc: c.mainDoc ?? null,
1500
2051
  sourceLang: sourceLang(),
1501
- langs: c.langs.filter((l) => l !== sourceLang()),
2052
+ langs: langs().filter((l) => l !== sourceLang()),
1502
2053
  langNames: Object.fromEntries(c.langs.map((l) => [l, langName(l, c)])),
1503
2054
  git: isGitRepo(root2),
1504
2055
  proposals: proposalsState(root2),
1505
2056
  author: authorName(root2),
1506
- unlinked: c.skipLink ? [] : missingMainDocs(root2)
2057
+ unlinked: c.skipLink ? [] : missingMainDocs(root2),
2058
+ features: featureState(root2)
1507
2059
  };
1508
2060
  },
2061
+ /** Turn a module on or off (with the ones it needs or that need it); returns every module's state. */
2062
+ "PUT /api/features": (_, body) => {
2063
+ if (!isFeature(body.feature)) throw new HttpError(400, `Expected one of ${FEATURES.join(", ")}`);
2064
+ setFeature(root2, body.feature, !!body.on, readDefaults() ?? BASE_DEFAULTS);
2065
+ return featureState(root2);
2066
+ },
1509
2067
  "PUT /api/config": (_, body) => {
1510
2068
  const c = config();
1511
2069
  if (Array.isArray(body.langs)) c.langs = body.langs.filter(isLangCode);
@@ -1515,7 +2073,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1515
2073
  }
1516
2074
  if (isLang(body.sourceLang) && body.sourceLang !== sourceLang()) {
1517
2075
  if (!c.mainDoc) throw new HttpError(400, "No main document configured; run `markode init`");
1518
- applyRulesToFile(path15.join(root2, c.mainDoc), void 0, body.sourceLang);
2076
+ applyRulesToFile(path18.join(root2, c.mainDoc), void 0, body.sourceLang);
1519
2077
  }
1520
2078
  writeConfig(root2, c);
1521
2079
  return { ...c, langs: c.langs.filter((l) => l !== sourceLang()) };
@@ -1539,17 +2097,17 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1539
2097
  "GET /api/tree": (url2) => {
1540
2098
  const l = viewLang(url2.searchParams.get("lang"));
1541
2099
  const sync2 = l ? Object.fromEntries(allStatuses(root2, l).map((e) => [e.path, e.status])) : {};
1542
- const foreign = Object.fromEntries(foreignFiles(root2, sourceLang()).map((f) => [f.path, f.detected]));
2100
+ const foreign = featureState(root2).translations ? Object.fromEntries(foreignFiles(root2, sourceLang()).map((f) => [f.path, f.detected])) : {};
1543
2101
  return { tree: scanTree(root2), git: gitStatuses(root2), sync: sync2, foreign };
1544
2102
  },
1545
2103
  "POST /api/keep-language": (_, body) => ({ lang: keepLanguage(root2, mdPath(body.path)) }),
1546
2104
  "GET /api/file": (url2) => {
1547
2105
  const rel = mdPath(url2.searchParams.get("path"));
1548
2106
  const l = viewLang(url2.searchParams.get("lang"));
1549
- const source = fs15.readFileSync(safeResolve(root2, rel), "utf8");
2107
+ const source = fs18.readFileSync(safeResolve(root2, rel), "utf8");
1550
2108
  if (l) {
1551
- const tPath = translationPath(root2, l, rel);
1552
- if (fs15.existsSync(tPath)) return { path: rel, content: fs15.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
2109
+ const tPath = translationFile(l, rel);
2110
+ if (fs18.existsSync(tPath)) return { path: rel, content: fs18.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
1553
2111
  return { path: rel, content: source, target: "source", status: "missing" };
1554
2112
  }
1555
2113
  return { path: rel, content: source, target: "source", status: null };
@@ -1557,18 +2115,20 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1557
2115
  "PUT /api/file": (_, body) => {
1558
2116
  const rel = mdPath(body.path);
1559
2117
  const l = viewLang(body.lang);
1560
- const file = body.target === "translation" && l ? translationPath(root2, l, rel) : safeResolve(root2, rel);
1561
- fs15.mkdirSync(path15.dirname(file), { recursive: true });
1562
- fs15.writeFileSync(file, String(body.content ?? ""));
2118
+ const file = body.target === "translation" && l ? translationFile(l, rel) : safeResolve(root2, rel);
2119
+ fs18.mkdirSync(path18.dirname(file), { recursive: true });
2120
+ const content = String(body.content ?? "");
2121
+ if (!l && featureState(root2).agent) logSave(root2, rel, fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "", content);
2122
+ fs18.writeFileSync(file, content);
1563
2123
  return { ok: true, status: l ? fileStatus(root2, l, rel) : null };
1564
2124
  },
1565
2125
  "POST /api/file": (_, body) => {
1566
2126
  const rel = mdPath(body.path);
1567
2127
  const file = safeResolve(root2, rel);
1568
- if (fs15.existsSync(file)) throw new HttpError(409, "File already exists");
1569
- fs15.mkdirSync(path15.dirname(file), { recursive: true });
1570
- const title = path15.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
1571
- fs15.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
2128
+ if (fs18.existsSync(file)) throw new HttpError(409, "File already exists");
2129
+ fs18.mkdirSync(path18.dirname(file), { recursive: true });
2130
+ const title = path18.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
2131
+ fs18.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
1572
2132
 
1573
2133
  `);
1574
2134
  return { ok: true, path: rel };
@@ -1578,23 +2138,24 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1578
2138
  if (typeof body.path !== "string" || !body.path.trim()) throw new HttpError(400, "Expected a path");
1579
2139
  const dir = safeResolve(root2, body.path);
1580
2140
  if (dir === root2) throw new HttpError(400, "Expected a path");
1581
- if (fs15.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
1582
- fs15.mkdirSync(dir, { recursive: true });
1583
- return { ok: true, path: toPosix(path15.relative(root2, dir)) };
2141
+ if (fs18.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
2142
+ fs18.mkdirSync(dir, { recursive: true });
2143
+ return { ok: true, path: toPosix(path18.relative(root2, dir)) };
1584
2144
  },
1585
2145
  "GET /api/delete-preview": (url2) => {
1586
2146
  const abs = deletable(url2.searchParams.get("path"));
1587
- if (!fs15.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
1588
- const files = fs15.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
2147
+ if (!fs18.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
2148
+ const files = fs18.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
1589
2149
  return { kind: "dir", files: files.length, md: files.filter((e) => e.name.toLowerCase().endsWith(".md")).length };
1590
2150
  },
1591
2151
  "DELETE /api/path": (url2) => {
1592
- const rel = toPosix(path15.relative(root2, deletable(url2.searchParams.get("path"))));
1593
- fs15.rmSync(safeResolve(root2, rel), { recursive: true });
1594
- for (const l of config().langs) fs15.rmSync(translationPath(root2, l, rel), { recursive: true, force: true });
2152
+ const rel = toPosix(path18.relative(root2, deletable(url2.searchParams.get("path"))));
2153
+ fs18.rmSync(safeResolve(root2, rel), { recursive: true });
2154
+ for (const l of config().langs) fs18.rmSync(translationFile(l, rel), { recursive: true, force: true });
1595
2155
  return { ok: true };
1596
2156
  },
1597
2157
  "POST /api/ignore": (_, body) => {
2158
+ if (typeof body.path !== "string" || /[\r\n]/.test(body.path)) throw new HttpError(400, "Expected a path");
1598
2159
  safeResolve(root2, body.path);
1599
2160
  setIgnored(root2, body.path, body.kind === "dir" ? "dir" : "file", !!body.ignored);
1600
2161
  return { ok: true };
@@ -1607,7 +2168,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1607
2168
  },
1608
2169
  "GET /api/rules": () => {
1609
2170
  const mainDoc = readConfig(root2)?.mainDoc;
1610
- const text3 = mainDoc && fs15.existsSync(path15.join(root2, mainDoc)) ? fs15.readFileSync(path15.join(root2, mainDoc), "utf8") : "";
2171
+ const text3 = mainDoc && fs18.existsSync(path18.join(root2, mainDoc)) ? fs18.readFileSync(path18.join(root2, mainDoc), "utf8") : "";
1611
2172
  return { rules: listRules(), applied: appliedRuleIds(text3), mainDoc: mainDoc ?? null };
1612
2173
  },
1613
2174
  "PUT /api/rules": (_, body) => saveRule(body),
@@ -1623,29 +2184,31 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1623
2184
  },
1624
2185
  "GET /api/skills/file": (url2) => {
1625
2186
  const skill = getSkill(root2, url2.searchParams.get("key"));
1626
- return { path: skill.key, content: fs15.readFileSync(path15.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
2187
+ return { path: skill.key, content: fs18.readFileSync(path18.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
1627
2188
  },
1628
2189
  "PUT /api/skills/file": (_, body) => {
1629
2190
  const skill = getSkill(root2, body.key);
1630
2191
  if (skill.managed) throw new HttpError(403, "This skill is managed by the app that installed it");
1631
- fs15.writeFileSync(path15.join(skill.key, "SKILL.md"), String(body.content ?? ""));
2192
+ fs18.writeFileSync(path18.join(skill.key, "SKILL.md"), String(body.content ?? ""));
1632
2193
  return { ok: true, status: null };
1633
2194
  },
1634
2195
  "POST /api/skills/install": (_, body) => {
1635
2196
  const targets = (Array.isArray(body.targets) ? body.targets : []).filter((t) => typeof t === "string" && t in PROJECT_TARGETS);
1636
2197
  if (!targets.length) throw new HttpError(400, "Expected at least one target");
1637
- const result = installSkill2(root2, getSkill(root2, body.key), targets, !!body.overwrite);
2198
+ const result = installSkill(root2, getSkill(root2, body.key), targets, !!body.overwrite);
1638
2199
  if (result.conflict) throw new HttpError(409, result.conflict.join(", "));
1639
2200
  return result;
1640
2201
  },
1641
2202
  "POST /api/rules/apply": (_, body) => {
1642
2203
  const mainDoc = readConfig(root2)?.mainDoc;
1643
2204
  if (!mainDoc) throw new HttpError(400, "No main document configured; run `markode init`");
1644
- applyRulesToFile(path15.join(root2, mainDoc), body.ids ?? []);
2205
+ applyRulesToFile(path18.join(root2, mainDoc), body.ids ?? []);
1645
2206
  return { ok: true };
1646
2207
  }
1647
2208
  };
1648
2209
  const server = http.createServer(async (req, res) => {
2210
+ res.setHeader("X-Frame-Options", "DENY");
2211
+ res.setHeader("X-Content-Type-Options", "nosniff");
1649
2212
  if (!isLocalRequest(req)) {
1650
2213
  res.writeHead(403).end("Forbidden");
1651
2214
  return;
@@ -1673,13 +2236,13 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1673
2236
  res.writeHead(404).end();
1674
2237
  return;
1675
2238
  }
1676
- const type = IMAGE_MIME[path15.extname(file).toLowerCase()];
1677
- if (!type || !fs15.existsSync(file) || !fs15.statSync(file).isFile()) {
2239
+ const type = IMAGE_MIME[path18.extname(file).toLowerCase()];
2240
+ if (!type || !fs18.existsSync(file) || !fs18.statSync(file).isFile()) {
1678
2241
  res.writeHead(404).end();
1679
2242
  return;
1680
2243
  }
1681
- res.writeHead(200, { "content-type": type, "cache-control": "no-cache", "content-security-policy": "sandbox", "x-content-type-options": "nosniff" });
1682
- fs15.createReadStream(file).pipe(res);
2244
+ res.writeHead(200, { "content-type": type, "cache-control": "no-cache", "content-security-policy": "sandbox; frame-ancestors 'none'" });
2245
+ fs18.createReadStream(file).pipe(res);
1683
2246
  }
1684
2247
  const actualPort = await listen(server, port);
1685
2248
  const wss = new WebSocketServer({ server, path: "/ws", verifyClient: ({ req }) => isLocalRequest(req) });
@@ -1693,16 +2256,16 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1693
2256
  // Unreadable folders (e.g. caches owned by another user) are skipped instead of failing.
1694
2257
  ignorePermissionErrors: true,
1695
2258
  ignored: (p3, stats) => {
1696
- const rel = toPosix(path15.relative(root2, p3));
2259
+ const rel = toPosix(path18.relative(root2, p3));
1697
2260
  if (rel === ".markode") return false;
1698
2261
  if (rel.startsWith(".markode/")) return !rel.startsWith(".markode/i18n") && rel !== ".markode/state.json" && rel !== ".markode/config.json";
1699
2262
  return skips(rel, stats ? stats.isDirectory() : void 0);
1700
2263
  }
1701
2264
  }).on("error", (err) => console.warn(pc.yellow(` file watcher: ${err.message}`))).on("all", (event, file) => {
1702
- const abs = path15.resolve(file);
2265
+ const abs = path18.resolve(file);
1703
2266
  if (!abs.endsWith(".md") && !abs.endsWith(".json") && !abs.endsWith(".markodeignore") && event !== "addDir" && event !== "unlinkDir") return;
1704
- let rel = toPosix(path15.relative(root2, abs));
1705
- const i18n = toPosix(path15.relative(root2, path15.join(stateDir(root2), "i18n")));
2267
+ let rel = toPosix(path18.relative(root2, abs));
2268
+ const i18n = toPosix(path18.relative(root2, path18.join(stateDir(root2), "i18n")));
1706
2269
  if (rel.startsWith(i18n + "/")) rel = rel.split("/").slice(3).join("/");
1707
2270
  broadcast({ type: "fs", event, path: rel });
1708
2271
  });
@@ -1710,7 +2273,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
1710
2273
  console.log(`
1711
2274
  ${pc.bold("markode")} ${pc.dim("\u2192")} ${pc.cyan(url)} ${pc.dim(root2)}
1712
2275
  `);
1713
- if (!fs15.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
2276
+ if (!fs18.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
1714
2277
  else if (openBrowser) open(url).catch(() => {
1715
2278
  });
1716
2279
  }
@@ -1724,19 +2287,19 @@ function listen(server, port) {
1724
2287
  });
1725
2288
  }
1726
2289
  function serveStatic(pathname, res) {
1727
- let file = path15.join(WEB_DIR, path15.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
1728
- if (!file.startsWith(WEB_DIR) || !fs15.existsSync(file) || fs15.statSync(file).isDirectory()) file = path15.join(WEB_DIR, "index.html");
1729
- if (!fs15.existsSync(file)) {
2290
+ let file = path18.join(WEB_DIR, path18.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
2291
+ if (!file.startsWith(WEB_DIR) || !fs18.existsSync(file) || fs18.statSync(file).isDirectory()) file = path18.join(WEB_DIR, "index.html");
2292
+ if (!fs18.existsSync(file)) {
1730
2293
  res.writeHead(404).end("Web UI not built. Run `pnpm build`.");
1731
2294
  return;
1732
2295
  }
1733
- res.writeHead(200, { "content-type": MIME[path15.extname(file)] ?? "application/octet-stream" });
1734
- fs15.createReadStream(file).pipe(res);
2296
+ res.writeHead(200, { "content-type": MIME[path18.extname(file)] ?? "application/octet-stream", "content-security-policy": "frame-ancestors 'none'" });
2297
+ fs18.createReadStream(file).pipe(res);
1735
2298
  }
1736
2299
 
1737
2300
  // src/cli/init.ts
1738
- import fs16 from "fs";
1739
- import path16 from "path";
2301
+ import fs19 from "fs";
2302
+ import path19 from "path";
1740
2303
  import * as p from "@clack/prompts";
1741
2304
  import pc2 from "picocolors";
1742
2305
  function check(value) {
@@ -1747,12 +2310,12 @@ function check(value) {
1747
2310
  return value;
1748
2311
  }
1749
2312
  async function offerClaudeLink(root2, d) {
1750
- if (fs16.existsSync(path16.join(root2, "CLAUDE.md"))) return;
2313
+ if (fs19.existsSync(path19.join(root2, "CLAUDE.md"))) return;
1751
2314
  const link2 = d ? d.mainDoc === "CLAUDE.md" : check(await p.confirm({ message: "Claude Code reads CLAUDE.md, not AGENTS.md. Add a CLAUDE.md that imports AGENTS.md?", initialValue: true }));
1752
2315
  if (link2 && linkAgentsDoc(root2)) p.log.success("Created CLAUDE.md (imports AGENTS.md)");
1753
2316
  }
1754
2317
  async function chooseMainDoc(root2, d) {
1755
- const existing = MAIN_DOCS.filter((d2) => fs16.existsSync(path16.join(root2, d2)) && !isPointer(fs16.readFileSync(path16.join(root2, d2), "utf8"), d2));
2318
+ const existing = MAIN_DOCS.filter((d2) => fs19.existsSync(path19.join(root2, d2)) && !isPointer(fs19.readFileSync(path19.join(root2, d2), "utf8"), d2));
1756
2319
  if (d) {
1757
2320
  if (existing.length === 1 && existing[0] === "AGENTS.md") await offerClaudeLink(root2, d);
1758
2321
  if (existing.length) return existing.includes(d.mainDoc) ? d.mainDoc : existing[0];
@@ -1798,7 +2361,7 @@ var WORKFLOW_OPTIONS = [
1798
2361
  { value: "openspec", label: "OpenSpec", hint: "specs + proposed changes, good for existing projects" },
1799
2362
  { value: "speckit", label: "GitHub Spec Kit", hint: "constitution \u2192 spec \u2192 plan \u2192 tasks, per feature" }
1800
2363
  ];
1801
- var PROPOSALS_QUESTION = "The AI can write proposals you answer by ticking options and commenting. How should they be organized?";
2364
+ var PROPOSALS_QUESTION = "The agent can write proposals you answer by ticking options and commenting. How should they be organized?";
1802
2365
  var PROPOSAL_OPTIONS = [
1803
2366
  { value: "structured", label: "By topic and area", hint: "docs/proposals/auth/\u2026, services/docs/proposals/\u2026" },
1804
2367
  { value: "general", label: "One folder", hint: "docs/proposals/" }
@@ -1811,7 +2374,6 @@ var MONOREPO_OPTIONS = [
1811
2374
  var SOURCE_LANG_QUESTION = "Language you work in (every .md file is written in it)";
1812
2375
  var SOURCE_LANG_MARK = /<!-- markode:source-lang:\w+ -->/;
1813
2376
  var LANG_OPTIONS = LANGS.map((l) => ({ value: l, label: LANG_NAMES[l], hint: l === "en" ? "default" : void 0 }));
1814
- var HIDE_QUESTION = "Hide the .markode/ folder in VS Code?";
1815
2377
  var ruleOptions = (rules) => rules.map((r) => ({ value: r.id, label: r.title, hint: r.body.split("\n")[0].replace(/^- /, "") }));
1816
2378
  async function chooseRules(message, rules, initial) {
1817
2379
  const pick = check(
@@ -1830,11 +2392,10 @@ async function chooseRules(message, rules, initial) {
1830
2392
  var agentOptions = () => Object.keys(SDD_AGENTS).map((a) => ({ value: a, label: SDD_AGENTS[a].label }));
1831
2393
  var WORKFLOW_LABEL = { none: "None", markode: "markode", openspec: "OpenSpec", speckit: "GitHub Spec Kit" };
1832
2394
  var WORKFLOW_GUIDE = { none: "", markode: ".claude/skills/markode-method/SKILL.md", openspec: "openspec/README.md", speckit: "specs/README.md" };
1833
- var canAsk = () => !!process.stdin.isTTY;
1834
- async function chooseAreas(root2, d) {
2395
+ async function chooseAreas(root2) {
1835
2396
  const { dirs: candidates, unsure } = detectAreas(root2);
1836
2397
  if (!candidates.length) return [];
1837
- if (unsure && canAsk()) {
2398
+ if (unsure) {
1838
2399
  const separate = check(
1839
2400
  await p.confirm({
1840
2401
  message: `${candidates.map((c) => `${c}/`).join(", ")} each have their own manifest. Are they separate projects (e.g. a front end and a back end)?`,
@@ -1843,17 +2404,13 @@ async function chooseAreas(root2, d) {
1843
2404
  );
1844
2405
  if (!separate) return [];
1845
2406
  }
1846
- if (d) {
1847
- const used2 = /* @__PURE__ */ new Set();
1848
- return d.monorepo === "single" ? [] : candidates.map((dir) => ({ dir, prefix: suggestPrefix(dir, used2) }));
1849
- }
1850
2407
  p.log.info(`Monorepo detected: ${candidates.map((c) => `${c}/`).join(", ")}`);
1851
2408
  const layout = check(await p.select({ message: MONOREPO_QUESTION, options: MONOREPO_OPTIONS }));
1852
2409
  if (layout === "single") return [];
1853
2410
  const dirs2 = check(
1854
2411
  await p.multiselect({
1855
2412
  message: "Which projects get their own docs?",
1856
- options: candidates.map((d2) => ({ value: d2, label: `${d2}/` })),
2413
+ options: candidates.map((d) => ({ value: d, label: `${d}/` })),
1857
2414
  initialValues: candidates,
1858
2415
  required: false
1859
2416
  })
@@ -1875,8 +2432,8 @@ async function chooseAreas(root2, d) {
1875
2432
  return areas;
1876
2433
  }
1877
2434
  async function chooseKind(root2, dir) {
1878
- const guess = guessKind(path16.join(root2, dir));
1879
- if (!guess.unclear || !canAsk()) return guess.kind;
2435
+ const guess = guessKind(path19.join(root2, dir));
2436
+ if (!guess.unclear) return guess.kind;
1880
2437
  const where = dir ? `${dir}/` : "this project";
1881
2438
  const seen = guess.signals.length ? `found: ${guess.signals.join(", ")}` : "no code yet";
1882
2439
  return check(
@@ -1887,9 +2444,9 @@ async function chooseKind(root2, dir) {
1887
2444
  })
1888
2445
  );
1889
2446
  }
1890
- async function askProfile(root2, dir, kind, d) {
1891
- const base = detectProfile(path16.join(root2, dir), kind);
1892
- if (d || kind === "app") return base;
2447
+ async function askProfile(root2, dir, kind) {
2448
+ const base = detectProfile(path19.join(root2, dir), kind);
2449
+ if (kind === "app") return base;
1893
2450
  const where = dir ? ` (${dir}/)` : "";
1894
2451
  if (kind === "library") return { kind, published: check(await p.confirm({ message: `Is the library${where} published (npm, PyPI\u2026)?`, initialValue: base.published })) };
1895
2452
  if (kind === "mobile") {
@@ -1917,10 +2474,9 @@ async function askProfile(root2, dir, kind, d) {
1917
2474
  const environments = check(await p.text({ message: `Environments${where}`, initialValue: base.environments, defaultValue: base.environments }));
1918
2475
  return { kind, environments: environments.trim() };
1919
2476
  }
1920
- async function chooseContracts(root2, areas, d) {
2477
+ async function chooseContracts(root2, areas) {
1921
2478
  if (!looksLikeMicroservices(root2, areas.length)) return null;
1922
2479
  const found = findContracts(root2);
1923
- if (d && found || !canAsk()) return found;
1924
2480
  const answer = check(
1925
2481
  await p.text({
1926
2482
  message: "Several services: where do their contracts (APIs, events) live? (e.g. proto/, an OpenAPI file; empty if none)",
@@ -1930,9 +2486,9 @@ async function chooseContracts(root2, areas, d) {
1930
2486
  );
1931
2487
  return answer.trim() || null;
1932
2488
  }
1933
- async function chooseExisting(root2, d) {
2489
+ async function chooseExisting(root2) {
1934
2490
  const found = existingDocs(root2);
1935
- if (!found.length || d) return found;
2491
+ if (!found.length) return found;
1936
2492
  return check(
1937
2493
  await p.multiselect({
1938
2494
  message: "These docs already exist. Which are the official ones? (the agent updates them instead of creating new ones)",
@@ -1942,38 +2498,43 @@ async function chooseExisting(root2, d) {
1942
2498
  })
1943
2499
  );
1944
2500
  }
1945
- async function chooseWorkflow(root2, mainDoc, previous, d) {
2501
+ async function chooseWorkflow(root2, mainDoc, previous, quiet) {
1946
2502
  const detected = detectWorkflow(root2);
1947
2503
  if (detected) {
1948
2504
  p.log.info(`${WORKFLOW_LABEL[detected]} found: markode shows it in the dashboard.`);
1949
- return { workflow: detected };
2505
+ return detected;
2506
+ }
2507
+ if (previous === "markode") return previous;
2508
+ if (quiet) {
2509
+ const { created, areas } = setupMethod(root2, mainDoc, quiet.monorepo);
2510
+ activateProposals(root2, areas.length ? "structured" : "general", mainDoc);
2511
+ if (created.length) p.log.success(`Tasks structure: ${created.length} files (${created.slice(0, 5).join(", ")}${created.length > 5 ? "\u2026" : ""})`);
2512
+ return "markode";
1950
2513
  }
1951
2514
  for (; ; ) {
1952
- const choice = d ? d.workflow : check(await p.select({ message: "Create a base structure for specs and planning?", options: WORKFLOW_OPTIONS, initialValue: previous ?? "none" }));
1953
- if (choice === "none") return { workflow: choice };
2515
+ const choice = check(await p.select({ message: "Create a base structure for specs and planning?", options: WORKFLOW_OPTIONS, initialValue: previous ?? "markode" }));
2516
+ if (choice === "none") return choice;
1954
2517
  if (choice === "markode") {
1955
- const existing = await chooseExisting(root2, d);
1956
- const areas = await chooseAreas(root2, d);
1957
- for (const a of areas) a.profile = await askProfile(root2, a.dir, await chooseKind(root2, a.dir), d);
1958
- const contracts = await chooseContracts(root2, areas, d);
1959
- const profile = areas.length ? void 0 : await askProfile(root2, "", await chooseKind(root2, ""), d);
2518
+ const existing = await chooseExisting(root2);
2519
+ const areas = await chooseAreas(root2);
2520
+ for (const a of areas) a.profile = await askProfile(root2, a.dir, await chooseKind(root2, a.dir));
2521
+ const contracts = await chooseContracts(root2, areas);
2522
+ const profile = areas.length ? void 0 : await askProfile(root2, "", await chooseKind(root2, ""));
1960
2523
  const created = scaffoldMethod(root2, mainDoc, areas);
1961
2524
  installMethodSkills(root2);
2525
+ applyWorkflowToFile(path19.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
1962
2526
  const big = !areas.length && flattenFiles(scanTree(root2)).length > 40;
1963
- const layout = areas.length ? "structured" : !big || d ? "general" : check(await p.select({ message: PROPOSALS_QUESTION, options: PROPOSAL_OPTIONS }));
2527
+ const layout = areas.length ? "structured" : !big ? "general" : check(await p.select({ message: PROPOSALS_QUESTION, options: PROPOSAL_OPTIONS }));
1964
2528
  activateProposals(root2, layout, mainDoc);
1965
2529
  if (created.length) p.log.success(`Created ${created.length} files: ${created.slice(0, 8).join(", ")}${created.length > 8 ? "\u2026" : ""}`);
1966
2530
  p.log.info(`Docs grow when there is something to write. Skills: /markode-status, /markode-new-task, /markode-close-session.`);
1967
- return { workflow: choice, section: methodSection(areas, root2, { profile, contracts, existing }) };
2531
+ return choice;
1968
2532
  }
1969
2533
  let official = true;
1970
- if (choice === "speckit" && !hasUv() && d) {
1971
- p.log.warn("Spec Kit needs uv (Python) to install its AI commands, and uv was not found: creating the structure only.");
1972
- official = false;
1973
- } else if (choice === "speckit" && !hasUv()) {
2534
+ if (choice === "speckit" && !hasUv()) {
1974
2535
  const next = check(
1975
2536
  await p.select({
1976
- message: "Spec Kit needs uv (Python) to install its AI commands, and uv was not found.",
2537
+ message: "Spec Kit needs uv (Python) to install its agent commands, and uv was not found.",
1977
2538
  options: [
1978
2539
  { value: "back", label: "Choose another option" },
1979
2540
  { value: "skeleton", label: "Create the structure only", hint: "the guide explains how to install Spec Kit later" }
@@ -1984,7 +2545,7 @@ async function chooseWorkflow(root2, mainDoc, previous, d) {
1984
2545
  official = false;
1985
2546
  }
1986
2547
  const initial = mainDoc === "AGENTS.md" ? "codex" : "claude";
1987
- const agents = !official ? [initial] : d ? choice === "speckit" ? [d.agents[0] ?? initial] : d.agents : choice === "openspec" ? check(await p.multiselect({ message: "Install the OpenSpec commands for which agents?", options: agentOptions(), initialValues: [initial], required: false })) : [check(await p.select({ message: "Install the Spec Kit commands for which agent?", options: agentOptions(), initialValue: initial }))];
2548
+ const agents = !official ? [initial] : choice === "openspec" ? check(await p.multiselect({ message: "Install the OpenSpec commands for which agents?", options: agentOptions(), initialValues: [initial], required: false })) : [check(await p.select({ message: "Install the Spec Kit commands for which agent?", options: agentOptions(), initialValue: initial }))];
1988
2549
  const spin = p.spinner();
1989
2550
  spin.start(official ? `Installing ${WORKFLOW_LABEL[choice]}\u2026` : "Creating the structure\u2026");
1990
2551
  const res = installWorkflow(root2, choice, agents, official);
@@ -1992,51 +2553,86 @@ async function chooseWorkflow(root2, mainDoc, previous, d) {
1992
2553
  if (official && !res.official) p.log.warn(`Couldn't run the ${WORKFLOW_LABEL[choice]} installer:
1993
2554
  ${pc2.dim(res.error ?? "")}
1994
2555
  Created the structure only.`);
1995
- if (!res.official) p.log.info(`To add the AI commands later, see ${pc2.cyan(WORKFLOW_GUIDE[choice])}.`);
2556
+ if (!res.official) p.log.info(`To add the agent commands later, see ${pc2.cyan(WORKFLOW_GUIDE[choice])}.`);
1996
2557
  if (res.ignored.length) p.log.info(`Agent commands hidden in markode (${res.ignored.length} folders); the eye button in the sidebar shows them.`);
1997
- return { workflow: choice };
2558
+ return choice;
1998
2559
  }
1999
2560
  }
2000
- async function runInit(root2, d = null) {
2561
+ var featureOptions = () => FEATURES.map((f) => ({ value: f, label: FEATURE_INFO[f].label, hint: FEATURE_INFO[f].hint + (REQUIRES[f] ? ` (needs ${FEATURE_INFO[REQUIRES[f]].label})` : "") }));
2562
+ async function askFeatures(message = "What should markode install in this project? (the viewer is always included)", initial = PROFILE_FEATURES.agent) {
2563
+ const same = (a, b) => a.length === b.length && a.every((f) => b.includes(f));
2564
+ const current = Object.keys(PROFILE_FEATURES).find((k) => same(PROFILE_FEATURES[k], initial)) ?? "custom";
2565
+ const choice = check(
2566
+ await p.select({
2567
+ message,
2568
+ options: [
2569
+ ...Object.keys(PROFILE_INFO).map((k) => ({ value: k, label: PROFILE_INFO[k].label + (k === "agent" ? " (recommended)" : ""), hint: PROFILE_INFO[k].hint })),
2570
+ { value: "custom", label: "Choose the modules", hint: "pick each one; any of them can be turned on or off later in Settings" }
2571
+ ],
2572
+ initialValue: current
2573
+ })
2574
+ );
2575
+ if (choice !== "custom") return PROFILE_FEATURES[choice];
2576
+ return withRequired(check(await p.multiselect({ message: "Modules (space to toggle)", options: featureOptions(), initialValues: initial, required: false })));
2577
+ }
2578
+ async function runInit(root2, plan2) {
2001
2579
  p.intro(pc2.bgCyan(pc2.black(" markode ")));
2002
- if (d) p.log.info(`Using your defaults (${pc2.cyan(defaultsFile())}). To choose differently for this project: ${pc2.cyan("markode init --custom")}`);
2580
+ const d = plan2.defaults;
2003
2581
  const previous = readConfig(root2);
2004
- const mainDoc = await chooseMainDoc(root2, d);
2005
- const mainFile = path16.join(root2, mainDoc);
2006
- const { workflow, section: section2 } = await chooseWorkflow(root2, mainDoc, previous?.workflow, d);
2007
- applyWorkflowToFile(mainFile, workflow, section2);
2008
- const mainText = fs16.readFileSync(mainFile, "utf8");
2009
- const sourceLang = d ? SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : d.sourceLang : check(await p.select({ message: SOURCE_LANG_QUESTION, options: LANG_OPTIONS, initialValue: sourceLangOf(mainText) }));
2010
- const langs = (previous?.langs ?? []).filter((l) => l !== sourceLang);
2011
- const rules = listRules();
2012
- const applied = appliedRuleIds(mainText);
2013
- const selected = d ? [.../* @__PURE__ */ new Set([...applied, ...d.rules.filter((id) => rules.some((r) => r.id === id))])] : await chooseRules("Working rules for this project", rules, applied);
2014
- applyRulesToFile(mainFile, selected, sourceLang);
2015
- const subprojects = findSubprojects(scanTree(root2));
2016
- if (subprojects.length && selected.length) {
2017
- const targets = d ? d.rulesInSubprojects ? subprojects : [] : check(
2018
- await p.multiselect({
2019
- message: "Monorepo detected. Apply the same rules to these projects too?",
2020
- options: subprojects.map((s) => ({ value: s, label: s })),
2021
- required: false
2022
- })
2023
- );
2024
- for (const dir of targets) {
2025
- applyRulesToFile(contentDoc(path16.join(root2, dir)), selected, sourceLang);
2582
+ const features2 = withRequired(plan2.features);
2583
+ const has2 = (f) => features2.includes(f);
2584
+ const detectedLang = detectSourceLang(root2, d?.sourceLang ?? "en");
2585
+ ignoreStateDir(root2);
2586
+ let mainDoc = previous?.mainDoc ?? mainDocOf(root2);
2587
+ let workflow = previous?.workflow ?? "none";
2588
+ if (has2("agent")) {
2589
+ mainDoc = await chooseMainDoc(root2, d);
2590
+ const mainFile = path19.join(root2, mainDoc);
2591
+ if (has2("dashboard")) workflow = await chooseWorkflow(root2, mainDoc, previous?.workflow, d);
2592
+ if (workflow === "openspec" || workflow === "speckit") applyWorkflowToFile(mainFile, workflow);
2593
+ const mainText = fs19.readFileSync(mainFile, "utf8");
2594
+ const sourceLang = d ? SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : detectedLang : check(await p.select({ message: SOURCE_LANG_QUESTION, options: LANG_OPTIONS, initialValue: SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : detectedLang }));
2595
+ const rules = listRules();
2596
+ const applied = appliedRuleIds(mainText);
2597
+ const selected = !has2("rules") ? applied : d ? [.../* @__PURE__ */ new Set([...applied, ...d.rules.filter((id) => rules.some((r) => r.id === id))])] : await chooseRules("Working rules for this project", rules, applied);
2598
+ applyRulesToFile(mainFile, selected, sourceLang);
2599
+ const subprojects = findSubprojects(scanTree(root2));
2600
+ if (subprojects.length && selected.length) {
2601
+ const targets = d ? d.rulesInSubprojects ? subprojects : [] : check(
2602
+ await p.multiselect({
2603
+ message: "Monorepo detected. Apply the same rules to these projects too?",
2604
+ options: subprojects.map((s) => ({ value: s, label: s })),
2605
+ required: false
2606
+ })
2607
+ );
2608
+ for (const dir of targets) applyRulesToFile(contentDoc(path19.join(root2, dir)), selected, sourceLang);
2026
2609
  }
2027
- }
2028
- ensureGitignore(root2);
2029
- if (!previous?.initializedAt) {
2030
- const hide = d ? d.hideInVsCode : check(await p.confirm({ message: HIDE_QUESTION, initialValue: true }));
2031
- if (hide && !hideInVsCode(root2)) p.log.warn('.vscode/settings.json has comments; add "**/.markode": true to files.exclude by hand.');
2032
- }
2033
- installSkill(root2);
2034
- const config = { mainDoc, langs, langNames: previous?.langNames, workflow, proposals: readConfig(root2)?.proposals, skipLink: previous?.skipLink, initializedAt: previous?.initializedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
2610
+ installAgentSkill(root2);
2611
+ const proposals = proposalsState(root2);
2612
+ if (has2("proposals") || proposals.active) activateProposals(root2, proposals.active ? proposals.layout : subprojects.length ? "structured" : "general", mainDoc);
2613
+ if (has2("live") && projectTargets(root2).includes("claude") && !hasClaudeHooks(root2) && !installClaudeHooks(root2))
2614
+ p.log.warn('.claude/settings.local.json could not be read; add the markode hooks by hand (README: "Claude Code hooks").');
2615
+ }
2616
+ if (has2("vscode") && !isHiddenInVsCode(root2) && !hideInVsCode(root2)) p.log.warn('.vscode/settings.json has comments; add "**/.markode": true to files.exclude by hand.');
2617
+ const flags = { ...previous?.features };
2618
+ for (const f of ["rules", "translations", "dashboard", "skills"]) flags[f] = has2(f) || (previous?.initializedAt ? flags[f] !== false : false);
2619
+ const config = {
2620
+ ...previous,
2621
+ mainDoc,
2622
+ langs: previous?.langs ?? [],
2623
+ workflow,
2624
+ proposals: readConfig(root2)?.proposals,
2625
+ features: flags,
2626
+ initializedAt: previous?.initializedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2627
+ };
2035
2628
  writeConfig(root2, config);
2036
- p.outro(`Ready. Skill installed in ${pc2.cyan(".claude/skills/markode")}. To review the docs in another language: ${pc2.cyan("markode translate")}`);
2629
+ const on = featureState(root2);
2630
+ const lines = FEATURES.filter((f) => on[f]).map((f) => `${pc2.green("\u25CF")} ${FEATURE_INFO[f].label} ${pc2.dim("\u2014 " + FEATURE_INFO[f].hint)}`);
2631
+ p.note([`${pc2.green("\u25CF")} Viewer ${pc2.dim("\u2014 see and edit every .md file of the project")}`, ...lines].join("\n"), "Installed");
2632
+ p.outro(`Ready. Turn modules on or off in ${pc2.cyan("Settings \u2192 Features")}, or with ${pc2.cyan("markode features")}.`);
2037
2633
  return config;
2038
2634
  }
2039
- async function runSetup(quick = false) {
2635
+ async function runSetup(quick = false, custom = false) {
2040
2636
  const previous = readDefaults() ?? BASE_DEFAULTS;
2041
2637
  if (quick) {
2042
2638
  const d2 = { ...previous, mode: "ask" };
@@ -2044,42 +2640,39 @@ async function runSetup(quick = false) {
2044
2640
  return d2;
2045
2641
  }
2046
2642
  p.intro(pc2.bgCyan(pc2.black(" markode setup ")));
2047
- const mode = check(
2643
+ const ask = check(
2048
2644
  await p.select({
2049
2645
  message: "How should markode set up your projects?",
2050
2646
  options: [
2051
- { value: "defaults", label: "With my answers", hint: "asked now, once; every project uses them" },
2052
- { value: "ask", label: "Ask in each project" }
2647
+ { value: "defaults", label: "Always the same way", hint: "choose the modules now; every new project gets them without questions" },
2648
+ { value: "ask", label: "Ask in each project", hint: "one question per project: which modules" }
2053
2649
  ],
2054
2650
  initialValue: previous.mode === "ask" && !readDefaults() ? "defaults" : previous.mode
2055
2651
  })
2056
2652
  );
2057
- if (mode === "ask") {
2058
- const d2 = { ...previous, mode };
2653
+ if (ask === "ask") {
2654
+ const d2 = { ...previous, mode: "ask" };
2059
2655
  writeDefaults(d2);
2060
- p.outro(`Saved. Each project will ask its own questions. Change it with ${pc2.cyan("markode setup")}`);
2656
+ p.outro(`Saved. Each project will ask which modules to install. Change it with ${pc2.cyan("markode setup")}`);
2061
2657
  return d2;
2062
2658
  }
2063
- const mainDoc = check(
2064
- await p.select({
2065
- message: "Main document to create when a project has none",
2066
- options: MAIN_DOCS.map((d2) => ({ value: d2, label: d2, hint: d2 === "CLAUDE.md" ? "Claude Code" : "other agents" })),
2067
- initialValue: previous.mainDoc
2068
- })
2069
- );
2070
- const workflow = check(await p.select({ message: "Base structure for specs and planning", options: WORKFLOW_OPTIONS, initialValue: previous.workflow }));
2071
- let agents = previous.agents;
2072
- let monorepo = previous.monorepo;
2073
- if (workflow === "openspec")
2074
- agents = check(await p.multiselect({ message: "Install the OpenSpec commands for which agents?", options: agentOptions(), initialValues: previous.agents, required: false }));
2075
- if (workflow === "speckit") agents = [check(await p.select({ message: "Install the Spec Kit commands for which agent?", options: agentOptions(), initialValue: previous.agents[0] }))];
2076
- if (workflow === "markode") monorepo = check(await p.select({ message: MONOREPO_QUESTION, options: MONOREPO_OPTIONS, initialValue: previous.monorepo }));
2077
- const sourceLang = check(await p.select({ message: `${SOURCE_LANG_QUESTION}; projects that already declare one keep it`, options: LANG_OPTIONS, initialValue: previous.sourceLang }));
2078
- const rules = listRules();
2079
- const selected = await chooseRules("Working rules for your projects", rules, previous.rules.filter((id) => rules.some((r) => r.id === id)));
2080
- const rulesInSubprojects = selected.length ? check(await p.confirm({ message: "In monorepos, apply the rules to the subprojects too?", initialValue: previous.rulesInSubprojects })) : false;
2081
- const hideInVsCode2 = check(await p.confirm({ message: HIDE_QUESTION, initialValue: previous.hideInVsCode }));
2082
- const d = { mode, mainDoc, workflow, agents, monorepo, sourceLang, rules: selected, rulesInSubprojects, hideInVsCode: hideInVsCode2 };
2659
+ const features2 = await askFeatures("What should every new project get? (the viewer is always included)", previous.features);
2660
+ let d = { ...previous, mode: "defaults", features: features2 };
2661
+ if (custom) {
2662
+ const mainDoc = check(
2663
+ await p.select({
2664
+ message: "Main document to create when a project has none",
2665
+ options: MAIN_DOCS.map((m) => ({ value: m, label: m, hint: m === "CLAUDE.md" ? "Claude Code" : "other agents" })),
2666
+ initialValue: previous.mainDoc
2667
+ })
2668
+ );
2669
+ const sourceLang = check(await p.select({ message: `${SOURCE_LANG_QUESTION}, when a project's docs don't show it`, options: LANG_OPTIONS, initialValue: previous.sourceLang }));
2670
+ const rules = listRules();
2671
+ const selected = features2.includes("rules") ? await chooseRules("Working rules for your projects", rules, previous.rules.filter((id) => rules.some((r) => r.id === id))) : previous.rules;
2672
+ const rulesInSubprojects = selected.length ? check(await p.confirm({ message: "In monorepos, apply the rules to the subprojects too?", initialValue: previous.rulesInSubprojects })) : false;
2673
+ const monorepo = features2.includes("dashboard") ? check(await p.select({ message: MONOREPO_QUESTION, options: MONOREPO_OPTIONS, initialValue: previous.monorepo })) : previous.monorepo;
2674
+ d = { ...d, mainDoc, sourceLang, rules: selected, rulesInSubprojects, monorepo };
2675
+ }
2083
2676
  writeDefaults(d);
2084
2677
  p.outro(`Saved in ${pc2.cyan(defaultsFile())}. Change them with ${pc2.cyan("markode setup")}; for a single project: ${pc2.cyan("markode init --custom")}`);
2085
2678
  return d;
@@ -2153,15 +2746,20 @@ async function runTranslate(root2) {
2153
2746
  var HELP = `markode \u2014 visual editor for your project's Markdown
2154
2747
 
2155
2748
  Usage
2156
- markode init if needed, then open the editor
2157
- markode init (re)run the project setup: main doc, specs structure, rules, languages
2158
- markode setup choose how projects are set up: with your saved answers, or asking each time
2749
+ markode set the project up if needed, then open the editor
2750
+ markode init (re)run the project setup: choose the modules to install (or use your defaults)
2751
+ markode setup choose how projects are set up: always the same modules, or asking each time
2752
+ markode features list the modules and whether they are on in this project
2753
+ markode features <module> on|off turn a module on or off (same as Settings \u2192 Features)
2159
2754
  markode serve open the editor without running init
2160
2755
  markode translate choose languages to review the docs in (or: markode --translate)
2161
2756
  markode check [--json] what /markode sync fixes: answered proposals, phases vs tasks, projects table, broken links
2162
2757
  markode wait <proposal> wait until the user answers a proposal in markode, then print the answer
2163
2758
  markode link [--dry-run] where there is only CLAUDE.md or only AGENTS.md, add the other one pointing to it
2164
2759
  markode link --skip don't offer it again in this project
2760
+ markode changes [--json] what the user edited or commented in markode since the agent was last told
2761
+ markode changes --start a new agent session: what happened before is old news
2762
+ markode changes --hook <event> the same, as Claude Code hook output (silent when nothing changed)
2165
2763
 
2166
2764
  For the agent's /markode translate:
2167
2765
  markode translate --status [--json] translations + files not in the source language
@@ -2170,8 +2768,11 @@ For the agent's /markode translate:
2170
2768
 
2171
2769
  Options
2172
2770
  -v, --version print the version
2173
- -D first run: skip saving defaults; every project asks its own questions
2174
- --custom init: ask every question for this project, ignoring your defaults
2771
+ -D first run: don't save your choice as the default for new projects
2772
+ --profile <viewer|agent|full> init: install this set of modules, without questions
2773
+ --yes init: no questions; your defaults, or the recommended modules (viewer + AI agent)
2774
+ --custom init: choose each module and answer every question for this project
2775
+ setup: also choose the main doc, language, rules and monorepo layout
2175
2776
  --port <n> server port (default 4319)
2176
2777
  --no-open do not open the browser
2177
2778
  `;
@@ -2184,26 +2785,32 @@ var option = (name) => {
2184
2785
  var command = args[0] && !args[0].startsWith("-") ? args[0] : "start";
2185
2786
  var root = process.env.MARKODE_ROOT ?? process.cwd();
2186
2787
  async function main() {
2788
+ if (fs20.existsSync(root)) setProjectRoot(root);
2187
2789
  if (flag("--help") || flag("-h")) return console.log(HELP);
2188
- if (flag("--version") || flag("-v")) return console.log(JSON.parse(fs17.readFileSync(path17.join(PKG_ROOT, "package.json"), "utf8")).version);
2790
+ if (flag("--version") || flag("-v")) return console.log(JSON.parse(fs20.readFileSync(path20.join(PKG_ROOT, "package.json"), "utf8")).version);
2189
2791
  const translation = flag("--status") || flag("--mark") || flag("--keep");
2190
2792
  if ((command === "sync" || command === "translate") && translation) return sync();
2191
2793
  if (command === "translate" || flag("--translate")) return runTranslate(root);
2192
2794
  if (command === "check") return check3();
2193
2795
  if (command === "wait") return wait();
2194
2796
  if (command === "link") return link();
2797
+ if (command === "changes") return changes();
2798
+ if (command === "features") return features();
2195
2799
  if (command === "setup") {
2196
- await runSetup(flag("-D"));
2800
+ await runSetup(false, flag("--custom"));
2197
2801
  return;
2198
2802
  }
2199
2803
  if (command === "init") {
2200
- await runInit(root, flag("--custom") ? null : await defaults());
2804
+ const p3 = await plan();
2805
+ if (p3) await runInit(root, p3);
2806
+ else console.log("No terminal to ask in: run `markode init --yes` or `--profile <viewer|agent|full>`.");
2201
2807
  return;
2202
2808
  }
2203
2809
  if (command === "start" && !readConfig(root)?.initializedAt) {
2204
- if (process.stdin.isTTY) await runInit(root, flag("--custom") ? null : await defaults());
2205
- else if (readDefaults()?.mode === "defaults") await runInit(root, readDefaults());
2810
+ const p3 = await plan();
2811
+ if (p3) await runInit(root, p3);
2206
2812
  }
2813
+ if (command === "start" || command === "serve") refreshInstalled(root);
2207
2814
  const unlinked = command === "start" && process.stdout.isTTY && !readConfig(root)?.skipLink ? missingMainDocs(root) : [];
2208
2815
  if (unlinked.length)
2209
2816
  console.log(`Tip: Claude Code reads CLAUDE.md and other agents read AGENTS.md; ${unlinked.map((m) => m.path).join(", ")} missing. Run \`markode link\` (or \`markode link --skip\`).`);
@@ -2214,9 +2821,40 @@ async function main() {
2214
2821
  console.log(HELP);
2215
2822
  process.exitCode = 1;
2216
2823
  }
2217
- async function defaults() {
2218
- const d = readDefaults() ?? (process.stdin.isTTY ? await runSetup(flag("-D")) : null);
2219
- return d?.mode === "defaults" ? d : null;
2824
+ async function plan() {
2825
+ const saved = readDefaults();
2826
+ const d = saved ?? BASE_DEFAULTS;
2827
+ const profile = option("--profile");
2828
+ if (flag("--custom")) return { features: await askFeatures(), defaults: null };
2829
+ if (profile) {
2830
+ if (!isProfile(profile)) throw new Error(`Unknown profile "${profile}": use viewer, agent or full`);
2831
+ return { features: PROFILE_FEATURES[profile], defaults: d };
2832
+ }
2833
+ if (saved?.mode === "defaults" || flag("--yes")) return { features: d.features, defaults: d };
2834
+ if (!process.stdin.isTTY) return null;
2835
+ const features2 = await askFeatures();
2836
+ if (!saved && !flag("-D")) {
2837
+ writeDefaults({ ...d, mode: "defaults", features: features2 });
2838
+ console.log(` Saved as your default for new projects (they won't ask again). Change it with \`markode setup\`.`);
2839
+ }
2840
+ return { features: features2, defaults: d };
2841
+ }
2842
+ function features() {
2843
+ const [id, value] = [args[1], args[2]];
2844
+ if (id) {
2845
+ if (!isFeature(id) || value !== "on" && value !== "off") {
2846
+ console.error(`Usage: markode features <${FEATURES.join("|")}> on|off`);
2847
+ process.exitCode = 1;
2848
+ return;
2849
+ }
2850
+ const changed = setFeature(root, id, value === "on", readDefaults() ?? BASE_DEFAULTS);
2851
+ for (const [f, on] of Object.entries(changed)) console.log(`${on ? "on " : "off"} ${FEATURE_INFO[f].label}`);
2852
+ if (!Object.keys(changed).length) console.log(`${FEATURE_INFO[id].label} was already ${value}.`);
2853
+ return;
2854
+ }
2855
+ const state = featureState(root);
2856
+ console.log("on Viewer \u2014 see and edit every .md file of the project (always on)");
2857
+ for (const f of FEATURES) console.log(`${state[f] ? "on " : "off"} ${FEATURE_INFO[f].label} \u2014 ${FEATURE_INFO[f].hint}${REQUIRES[f] ? ` (needs ${FEATURE_INFO[REQUIRES[f]].label})` : ""}`);
2220
2858
  }
2221
2859
  function check3() {
2222
2860
  const report = checkDocs(root);
@@ -2242,9 +2880,21 @@ function link() {
2242
2880
  if (flag("--dry-run")) return console.log(missing.map((m) => `would create ${m.path} \u2192 ${m.pointsTo}`).join("\n"));
2243
2881
  for (const p3 of linkMainDocs(root)) console.log(`created ${p3}`);
2244
2882
  }
2883
+ function changes() {
2884
+ if (flag("--start")) return startSession(root);
2885
+ const list = takeChanges(root);
2886
+ const event = option("--hook");
2887
+ if (event) {
2888
+ const text3 = formatChanges(list);
2889
+ if (text3) console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text3 } }));
2890
+ return;
2891
+ }
2892
+ if (flag("--json")) return console.log(JSON.stringify(list, null, 2));
2893
+ console.log(formatChanges(list) || "No edits or comments from the user since you were last told.");
2894
+ }
2245
2895
  async function wait() {
2246
2896
  const rel = args[1];
2247
- if (!rel || !fs17.existsSync(path17.join(root, rel))) {
2897
+ if (!rel || !fs20.existsSync(path20.join(root, rel))) {
2248
2898
  console.error(`Usage: markode wait <proposal.md> (relative to ${root})`);
2249
2899
  process.exitCode = 1;
2250
2900
  return;