grounder 0.4.0 → 0.6.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 (221) hide show
  1. package/README.md +150 -271
  2. package/dist/agents/claude.d.ts +4 -4
  3. package/dist/agents/claude.d.ts.map +1 -1
  4. package/dist/agents/claude.js +158 -178
  5. package/dist/agents/claude.js.map +1 -1
  6. package/dist/agents/cursor.d.ts +3 -3
  7. package/dist/agents/cursor.d.ts.map +1 -1
  8. package/dist/agents/cursor.js +97 -143
  9. package/dist/agents/cursor.js.map +1 -1
  10. package/dist/agents/home-skills.d.ts +46 -0
  11. package/dist/agents/home-skills.d.ts.map +1 -0
  12. package/dist/agents/home-skills.js +83 -0
  13. package/dist/agents/home-skills.js.map +1 -0
  14. package/dist/agents/hook-fragment.d.ts +52 -0
  15. package/dist/agents/hook-fragment.d.ts.map +1 -0
  16. package/dist/agents/hook-fragment.js +76 -0
  17. package/dist/agents/hook-fragment.js.map +1 -0
  18. package/dist/agents/hook-install.d.ts +62 -0
  19. package/dist/agents/hook-install.d.ts.map +1 -0
  20. package/dist/agents/hook-install.js +74 -0
  21. package/dist/agents/hook-install.js.map +1 -0
  22. package/dist/agents/hook-runtime.d.ts +80 -13
  23. package/dist/agents/hook-runtime.d.ts.map +1 -1
  24. package/dist/agents/hook-runtime.js +278 -74
  25. package/dist/agents/hook-runtime.js.map +1 -1
  26. package/dist/agents/index.d.ts +8 -14
  27. package/dist/agents/index.d.ts.map +1 -1
  28. package/dist/agents/index.js +13 -19
  29. package/dist/agents/index.js.map +1 -1
  30. package/dist/agents/types.d.ts +56 -16
  31. package/dist/agents/types.d.ts.map +1 -1
  32. package/dist/cli.js +15 -10
  33. package/dist/cli.js.map +1 -1
  34. package/dist/commands/apply.d.ts +69 -0
  35. package/dist/commands/apply.d.ts.map +1 -0
  36. package/dist/commands/apply.js +238 -0
  37. package/dist/commands/apply.js.map +1 -0
  38. package/dist/commands/doctor.d.ts.map +1 -1
  39. package/dist/commands/doctor.js +197 -101
  40. package/dist/commands/doctor.js.map +1 -1
  41. package/dist/commands/handoff/list.d.ts +23 -13
  42. package/dist/commands/handoff/list.d.ts.map +1 -1
  43. package/dist/commands/handoff/list.js +28 -16
  44. package/dist/commands/handoff/list.js.map +1 -1
  45. package/dist/commands/handoff/peek.d.ts +1 -1
  46. package/dist/commands/handoff/peek.d.ts.map +1 -1
  47. package/dist/commands/handoff/peek.js +28 -12
  48. package/dist/commands/handoff/peek.js.map +1 -1
  49. package/dist/commands/handoff.d.ts +2 -0
  50. package/dist/commands/handoff.d.ts.map +1 -1
  51. package/dist/commands/handoff.js +10 -1
  52. package/dist/commands/handoff.js.map +1 -1
  53. package/dist/commands/install-drift.d.ts +34 -0
  54. package/dist/commands/install-drift.d.ts.map +1 -0
  55. package/dist/commands/install-drift.js +72 -0
  56. package/dist/commands/install-drift.js.map +1 -0
  57. package/dist/commands/link.d.ts +12 -0
  58. package/dist/commands/link.d.ts.map +1 -0
  59. package/dist/commands/{repo/init.js → link.js} +35 -27
  60. package/dist/commands/link.js.map +1 -0
  61. package/dist/commands/migrate.d.ts +11 -3
  62. package/dist/commands/migrate.d.ts.map +1 -1
  63. package/dist/commands/migrate.js +66 -34
  64. package/dist/commands/migrate.js.map +1 -1
  65. package/dist/commands/note/list.d.ts +17 -9
  66. package/dist/commands/note/list.d.ts.map +1 -1
  67. package/dist/commands/note/list.js +19 -14
  68. package/dist/commands/note/list.js.map +1 -1
  69. package/dist/commands/note.d.ts +2 -0
  70. package/dist/commands/note.d.ts.map +1 -1
  71. package/dist/commands/note.js +10 -1
  72. package/dist/commands/note.js.map +1 -1
  73. package/dist/commands/output.d.ts +22 -2
  74. package/dist/commands/output.d.ts.map +1 -1
  75. package/dist/commands/output.js +18 -6
  76. package/dist/commands/output.js.map +1 -1
  77. package/dist/commands/overview.d.ts +33 -0
  78. package/dist/commands/overview.d.ts.map +1 -0
  79. package/dist/commands/overview.js +150 -0
  80. package/dist/commands/overview.js.map +1 -0
  81. package/dist/commands/plan/list.d.ts +20 -12
  82. package/dist/commands/plan/list.d.ts.map +1 -1
  83. package/dist/commands/plan/list.js +22 -17
  84. package/dist/commands/plan/list.js.map +1 -1
  85. package/dist/commands/plan.d.ts +2 -0
  86. package/dist/commands/plan.d.ts.map +1 -1
  87. package/dist/commands/plan.js +12 -2
  88. package/dist/commands/plan.js.map +1 -1
  89. package/dist/commands/render-artifact-table.d.ts +85 -0
  90. package/dist/commands/render-artifact-table.d.ts.map +1 -0
  91. package/dist/commands/render-artifact-table.js +232 -0
  92. package/dist/commands/render-artifact-table.js.map +1 -0
  93. package/dist/commands/require-linked.js +2 -2
  94. package/dist/commands/require-linked.js.map +1 -1
  95. package/dist/commands/search.d.ts +23 -0
  96. package/dist/commands/search.d.ts.map +1 -0
  97. package/dist/commands/search.js +288 -0
  98. package/dist/commands/search.js.map +1 -0
  99. package/dist/commands/{vault/init.d.ts → setup.d.ts} +5 -4
  100. package/dist/commands/setup.d.ts.map +1 -0
  101. package/dist/commands/setup.js +210 -0
  102. package/dist/commands/setup.js.map +1 -0
  103. package/dist/commands/status.d.ts +7 -0
  104. package/dist/commands/status.d.ts.map +1 -1
  105. package/dist/commands/status.js +273 -83
  106. package/dist/commands/status.js.map +1 -1
  107. package/dist/commands/upgrade-banner.d.ts +2 -2
  108. package/dist/commands/upgrade-banner.js +2 -2
  109. package/dist/connector/home.d.ts +12 -0
  110. package/dist/connector/home.d.ts.map +1 -1
  111. package/dist/connector/home.js +26 -2
  112. package/dist/connector/home.js.map +1 -1
  113. package/dist/connector/ledger-migrations.d.ts +55 -0
  114. package/dist/connector/ledger-migrations.d.ts.map +1 -0
  115. package/dist/connector/ledger-migrations.js +94 -0
  116. package/dist/connector/ledger-migrations.js.map +1 -0
  117. package/dist/connector/state.d.ts +97 -60
  118. package/dist/connector/state.d.ts.map +1 -1
  119. package/dist/connector/state.js +165 -107
  120. package/dist/connector/state.js.map +1 -1
  121. package/dist/connector/vault.d.ts +2 -0
  122. package/dist/connector/vault.d.ts.map +1 -1
  123. package/dist/connector/vault.js +6 -1
  124. package/dist/connector/vault.js.map +1 -1
  125. package/dist/help.d.ts +3 -3
  126. package/dist/help.d.ts.map +1 -1
  127. package/dist/help.js +97 -56
  128. package/dist/help.js.map +1 -1
  129. package/dist/reconcile/apply.d.ts +31 -0
  130. package/dist/reconcile/apply.d.ts.map +1 -0
  131. package/dist/reconcile/apply.js +97 -0
  132. package/dist/reconcile/apply.js.map +1 -0
  133. package/dist/reconcile/core.d.ts +82 -0
  134. package/dist/reconcile/core.d.ts.map +1 -0
  135. package/dist/reconcile/core.js +163 -0
  136. package/dist/reconcile/core.js.map +1 -0
  137. package/dist/reconcile/disk.d.ts +13 -0
  138. package/dist/reconcile/disk.d.ts.map +1 -0
  139. package/dist/reconcile/disk.js +28 -0
  140. package/dist/reconcile/disk.js.map +1 -0
  141. package/dist/util/frontmatter.d.ts +9 -7
  142. package/dist/util/frontmatter.d.ts.map +1 -1
  143. package/dist/util/frontmatter.js +29 -4
  144. package/dist/util/frontmatter.js.map +1 -1
  145. package/dist/util/fs.d.ts +14 -0
  146. package/dist/util/fs.d.ts.map +1 -1
  147. package/dist/util/fs.js +27 -1
  148. package/dist/util/fs.js.map +1 -1
  149. package/dist/util/merge-json.d.ts +13 -2
  150. package/dist/util/merge-json.d.ts.map +1 -1
  151. package/dist/util/merge-json.js +24 -10
  152. package/dist/util/merge-json.js.map +1 -1
  153. package/dist/util/path.d.ts +23 -0
  154. package/dist/util/path.d.ts.map +1 -1
  155. package/dist/util/path.js +39 -0
  156. package/dist/util/path.js.map +1 -1
  157. package/dist/vault/list-handoffs.d.ts +14 -2
  158. package/dist/vault/list-handoffs.d.ts.map +1 -1
  159. package/dist/vault/list-handoffs.js +40 -23
  160. package/dist/vault/list-handoffs.js.map +1 -1
  161. package/dist/vault/list-markdown.d.ts +8 -0
  162. package/dist/vault/list-markdown.d.ts.map +1 -0
  163. package/dist/vault/list-markdown.js +35 -0
  164. package/dist/vault/list-markdown.js.map +1 -0
  165. package/dist/vault/list-notes.d.ts +15 -6
  166. package/dist/vault/list-notes.d.ts.map +1 -1
  167. package/dist/vault/list-notes.js +23 -27
  168. package/dist/vault/list-notes.js.map +1 -1
  169. package/dist/vault/list-plans.d.ts +12 -3
  170. package/dist/vault/list-plans.d.ts.map +1 -1
  171. package/dist/vault/list-plans.js +20 -24
  172. package/dist/vault/list-plans.js.map +1 -1
  173. package/dist/vault/search.d.ts +55 -0
  174. package/dist/vault/search.d.ts.map +1 -0
  175. package/dist/vault/search.js +432 -0
  176. package/dist/vault/search.js.map +1 -0
  177. package/dist/vault/write-handoff.d.ts +2 -0
  178. package/dist/vault/write-handoff.d.ts.map +1 -1
  179. package/dist/vault/write-handoff.js +5 -0
  180. package/dist/vault/write-handoff.js.map +1 -1
  181. package/dist/vault/write-note.d.ts +2 -0
  182. package/dist/vault/write-note.d.ts.map +1 -1
  183. package/dist/vault/write-note.js +9 -1
  184. package/dist/vault/write-note.js.map +1 -1
  185. package/dist/vault/write-plan.d.ts +4 -0
  186. package/dist/vault/write-plan.d.ts.map +1 -1
  187. package/dist/vault/write-plan.js +14 -1
  188. package/dist/vault/write-plan.js.map +1 -1
  189. package/package.json +11 -4
  190. package/templates/agents/claude/skills/grounder-handoff/SKILL.md +73 -0
  191. package/templates/agents/claude/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +12 -2
  192. package/templates/agents/claude/skills/grounder-overview/SKILL.md +21 -0
  193. package/templates/agents/claude/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +13 -5
  194. package/templates/agents/claude/skills/grounder-recall/SKILL.md +34 -0
  195. package/templates/agents/claude/skills/grounder-search/SKILL.md +152 -0
  196. package/templates/agents/cursor/skills/grounder-handoff/SKILL.md +73 -0
  197. package/templates/agents/cursor/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +12 -2
  198. package/templates/agents/cursor/skills/grounder-overview/SKILL.md +21 -0
  199. package/templates/agents/cursor/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +13 -5
  200. package/templates/agents/cursor/skills/grounder-recall/SKILL.md +34 -0
  201. package/templates/agents/cursor/skills/grounder-search/SKILL.md +152 -0
  202. package/templates/vault/plan.md +1 -1
  203. package/templates/vault/session-handoff.md +2 -1
  204. package/dist/agents/install-command.d.ts +0 -35
  205. package/dist/agents/install-command.d.ts.map +0 -1
  206. package/dist/agents/install-command.js +0 -65
  207. package/dist/agents/install-command.js.map +0 -1
  208. package/dist/commands/apply-agent-installs.d.ts +0 -34
  209. package/dist/commands/apply-agent-installs.d.ts.map +0 -1
  210. package/dist/commands/apply-agent-installs.js +0 -137
  211. package/dist/commands/apply-agent-installs.js.map +0 -1
  212. package/dist/commands/repo/init.d.ts +0 -11
  213. package/dist/commands/repo/init.d.ts.map +0 -1
  214. package/dist/commands/repo/init.js.map +0 -1
  215. package/dist/commands/vault/init.d.ts.map +0 -1
  216. package/dist/commands/vault/init.js +0 -90
  217. package/dist/commands/vault/init.js.map +0 -1
  218. package/templates/agents/claude/commands/grounder-task-handoff.md +0 -54
  219. package/templates/agents/claude/commands/grounder-task.md +0 -22
  220. package/templates/agents/cursor/commands/grounder-task-handoff.md +0 -54
  221. package/templates/agents/cursor/commands/grounder-task.md +0 -22
@@ -0,0 +1,163 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Pure reconciliation core — no I/O anywhere in this file.
4
+ *
5
+ * Two shapes, per the design: {@link desiredDrift} answers "would migrate
6
+ * change something" cheaply (ledger vs. rendered template hashes only, no
7
+ * disk read) for `peek`/`status`; {@link reconcile} is the full three-way
8
+ * (desired vs. ledger vs. on-disk) plan `setup`/`migrate`/`doctor` execute or
9
+ * preview. Modeled on chezmoi's source/destination/target state and dpkg's
10
+ * old-pristine/new-pristine/on-disk compare.
11
+ */
12
+ /**
13
+ * True when `filePath` sits under one of `prefixes` (a directory, matched
14
+ * exactly or via a `prefix + separator` boundary — never a bare substring
15
+ * match, and never a `..`-segment escape). Callers use this to keep a
16
+ * ledger's extra/stray entries — a hand-edited or corrupted `state.json`,
17
+ * say — from ever being treated as delete candidates for paths outside the
18
+ * directories an adapter actually manages. Comparison is lexical
19
+ * (`path.resolve` + `path.relative`, no symlink following): both sides are
20
+ * resolved to absolute form first, so a `../` segment in `filePath` cannot
21
+ * walk back out of a prefix it superficially starts with.
22
+ * `reconcile()` itself stays agnostic to this (see its own docs); callers
23
+ * filter `ledger` before passing it in.
24
+ */
25
+ export function isUnderOwnedPrefix(filePath, prefixes) {
26
+ const target = path.resolve(filePath);
27
+ return prefixes.some((prefix) => {
28
+ const root = path.resolve(prefix);
29
+ if (root === target) {
30
+ return true;
31
+ }
32
+ const relative = path.relative(root, target);
33
+ return (relative !== "" &&
34
+ relative !== ".." &&
35
+ !relative.startsWith(`..${path.sep}`) &&
36
+ !path.isAbsolute(relative));
37
+ });
38
+ }
39
+ /**
40
+ * Compare desired (rendered template) hashes against one agent's last-applied
41
+ * ledger manifest. No disk I/O — the caller renders + hashes templates, and
42
+ * passes in the ledger's `files` map already read from `state.json`.
43
+ *
44
+ * `ledger === undefined` means this agent has no ledger entry at all (never
45
+ * installed) — returns no drift. Scoping to ledger-recorded agents lives
46
+ * here, not in every caller: otherwise a detected-but-never-installed agent
47
+ * would tease `migrate` forever with no command able to silence it.
48
+ *
49
+ * Within a ledger-recorded agent, a desired path missing from its `files` map
50
+ * IS drift (`"create"`) — that's how a newly added skill file surfaces here.
51
+ */
52
+ export function desiredDrift(desired, ledger) {
53
+ if (!ledger) {
54
+ return [];
55
+ }
56
+ const out = [];
57
+ for (const [path, hash] of Object.entries(desired)) {
58
+ const recorded = ledger[path]?.hash;
59
+ if (recorded === undefined) {
60
+ out.push({ path, kind: "create" });
61
+ }
62
+ else if (recorded !== hash) {
63
+ out.push({ path, kind: "update" });
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ /**
69
+ * Full three-way reconcile for one agent's whole-file artifacts: desired
70
+ * (current templates) vs. ledger (last-applied) vs. disk (actual). Pure —
71
+ * `disk`/`ledger` are already-read snapshots, not live filesystem access.
72
+ *
73
+ * `tombstones` are extra "previously desired, no longer desired" paths
74
+ * (pre-skill command files, etc.) unioned into the retirement side of the
75
+ * diff even when the ledger never recorded them — the case a pure
76
+ * ledger-manifest diff is blind to (pre-hash-tracking installs).
77
+ *
78
+ * Safety: only ever call this for a known agent id whose `desired` this
79
+ * binary can actually compute — never for an unrecognized ledger entry,
80
+ * which would treat its entire recorded file set as delete candidates.
81
+ */
82
+ export function reconcile(desired, tombstones, ledger, disk, force) {
83
+ const ledgerFiles = ledger ?? {};
84
+ const allPaths = new Set([
85
+ ...Object.keys(desired),
86
+ ...Object.keys(ledgerFiles),
87
+ ...tombstones,
88
+ ]);
89
+ const entries = [];
90
+ for (const path of allPaths) {
91
+ const desiredHash = desired[path];
92
+ const ledgerHash = ledgerFiles[path]?.hash;
93
+ const onDisk = disk[path];
94
+ if (desiredHash !== undefined) {
95
+ // Whole-file artifact currently wanted.
96
+ if (onDisk === undefined) {
97
+ entries.push({ path, action: "create" });
98
+ }
99
+ else if (onDisk === desiredHash) {
100
+ entries.push({ path, action: "noop" });
101
+ }
102
+ else if (force || (ledgerHash !== undefined && ledgerHash === onDisk)) {
103
+ entries.push({ path, action: "update" });
104
+ }
105
+ else {
106
+ entries.push({ path, action: "conflict", blockedAction: "overwrite" });
107
+ }
108
+ continue;
109
+ }
110
+ // Not in the current desired set — retirement candidate (tombstone, or a
111
+ // path dropped from the manifest by a schema change).
112
+ if (onDisk === undefined) {
113
+ // Already gone — noop, not a recurring delete (doctor would otherwise
114
+ // warn about an already-cleaned-up legacy path forever). If the ledger
115
+ // still holds a stale hash for it (removed outside `migrate`), that's
116
+ // a ledger-only cleanup ("forget"), not a file action.
117
+ entries.push({ path, action: ledgerHash !== undefined ? "forget" : "noop" });
118
+ }
119
+ else if (force || (ledgerHash !== undefined && ledgerHash === onDisk)) {
120
+ entries.push({ path, action: "delete" });
121
+ }
122
+ else {
123
+ entries.push({ path, action: "conflict", blockedAction: "delete" });
124
+ }
125
+ }
126
+ return entries;
127
+ }
128
+ /**
129
+ * Would applying `plan` actually change the ledger? Pure — mirrors exactly
130
+ * what `applyPlan()`'s per-artifact writes do (each has its own no-op guard):
131
+ * `create`/`update`/`noop` only write when the recorded hash would actually
132
+ * change; `delete` only forgets an entry that exists; `forget` always writes
133
+ * (by construction it only exists when there's something to forget);
134
+ * `conflict` never touches the ledger. Used so `--dry-run` and a real run
135
+ * predict/report the exact same thing for the exact same reason.
136
+ */
137
+ export function planChangesLedger(plan, ledger, desired) {
138
+ const ledgerFiles = ledger ?? {};
139
+ for (const entry of plan) {
140
+ switch (entry.action) {
141
+ case "create":
142
+ case "update":
143
+ case "noop": {
144
+ const desiredHash = desired[entry.path];
145
+ if (desiredHash !== undefined && ledgerFiles[entry.path]?.hash !== desiredHash) {
146
+ return true;
147
+ }
148
+ break;
149
+ }
150
+ case "delete":
151
+ if (ledgerFiles[entry.path]?.hash !== undefined) {
152
+ return true;
153
+ }
154
+ break;
155
+ case "forget":
156
+ return true;
157
+ case "conflict":
158
+ break;
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/reconcile/core.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;GASG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,QAA2B;IAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAClC,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,OAAO,CACL,QAAQ,KAAK,EAAE;YACf,QAAQ,KAAK,IAAI;YACjB,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;YACrC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAC3B,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAYD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAC1B,OAA+B,EAC/B,MAAkC;IAElC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QACpC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrC,CAAC;aAAM,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAcD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,SAAS,CACvB,OAA+B,EAC/B,UAA6B,EAC7B,MAAkC,EAClC,IAAgB,EAChB,KAAc;IAEd,MAAM,WAAW,GAAG,MAAM,IAAI,EAAE,CAAC;IACjC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAS;QAC/B,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;QACvB,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;QAC3B,GAAG,UAAU;KACd,CAAC,CAAC;IAEH,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QAE1B,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,wCAAwC;YACxC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC3C,CAAC;iBAAM,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;gBAClC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YACzC,CAAC;iBAAM,IAAI,KAAK,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,MAAM,CAAC,EAAE,CAAC;gBACxE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC,CAAC;YACzE,CAAC;YACD,SAAS;QACX,CAAC;QAED,yEAAyE;QACzE,sDAAsD;QACtD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,sEAAsE;YACtE,uEAAuE;YACvE,sEAAsE;YACtE,uDAAuD;YACvD,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/E,CAAC;aAAM,IAAI,KAAK,IAAI,CAAC,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,MAAM,CAAC,EAAE,CAAC;YACxE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC3C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAA0B,EAC1B,MAAkC,EAClC,OAA+B;IAE/B,MAAM,WAAW,GAAG,MAAM,IAAI,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;QACzB,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;YACrB,KAAK,QAAQ,CAAC;YACd,KAAK,QAAQ,CAAC;YACd,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACxC,IAAI,WAAW,KAAK,SAAS,IAAI,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC;oBAC/E,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM;YACR,CAAC;YACD,KAAK,QAAQ;gBACX,IAAI,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,KAAK,SAAS,EAAE,CAAC;oBAChD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM;YACR,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC;YACd,KAAK,UAAU;gBACb,MAAM;QACV,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { DiskHashes } from "./core.js";
2
+ /**
3
+ * Read + hash each path; a missing file maps to `undefined`. Any other read
4
+ * failure (EACCES, EISDIR, …) propagates instead of being folded into
5
+ * "missing" — an unreadable-but-present file must never be silently planned
6
+ * as a `create`, writing blind over content this binary never actually saw.
7
+ * Both callers already have somewhere for that to land gracefully: doctor's
8
+ * `computeAgentPlansSafe` catches per-agent and turns it into a "could not
9
+ * verify skill drift" warning; `setup`/`migrate` catch it around
10
+ * `applyAgentInstalls` and report it on stderr.
11
+ */
12
+ export declare function readDiskHashes(paths: Iterable<string>): Promise<DiskHashes>;
13
+ //# sourceMappingURL=disk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk.d.ts","sourceRoot":"","sources":["../../src/reconcile/disk.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAajF"}
@@ -0,0 +1,28 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { hashContent } from "../util/hash.js";
3
+ /**
4
+ * Read + hash each path; a missing file maps to `undefined`. Any other read
5
+ * failure (EACCES, EISDIR, …) propagates instead of being folded into
6
+ * "missing" — an unreadable-but-present file must never be silently planned
7
+ * as a `create`, writing blind over content this binary never actually saw.
8
+ * Both callers already have somewhere for that to land gracefully: doctor's
9
+ * `computeAgentPlansSafe` catches per-agent and turns it into a "could not
10
+ * verify skill drift" warning; `setup`/`migrate` catch it around
11
+ * `applyAgentInstalls` and report it on stderr.
12
+ */
13
+ export async function readDiskHashes(paths) {
14
+ const out = {};
15
+ for (const p of paths) {
16
+ try {
17
+ out[p] = hashContent(await readFile(p, "utf8"));
18
+ }
19
+ catch (error) {
20
+ if (error.code !== "ENOENT") {
21
+ throw error;
22
+ }
23
+ out[p] = undefined;
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ //# sourceMappingURL=disk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk.js","sourceRoot":"","sources":["../../src/reconcile/disk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAuB;IAC1D,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,GAAG,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,MAAM,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,MAAM,KAAK,CAAC;YACd,CAAC;YACD,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -1,11 +1,13 @@
1
+ export interface ParsedFrontmatter {
2
+ title?: string;
3
+ created?: string;
4
+ topics?: string[];
5
+ }
1
6
  /**
2
- * Parse `title` / `created` from Grounder handoff frontmatter.
3
- * Accepts the quoted `key: "value"` shape {@link writeHandoff} writes today, and
4
- * unquoted `key: value` from earlier handoffs. Not a general YAML parser.
7
+ * Parse `title` / `created` / `topics` from Grounder frontmatter.
8
+ * Accepts the quoted `key: "value"` shape write commands produce, and
9
+ * unquoted `key: value` from earlier files. Not a general YAML parser.
5
10
  * Returns `{}` on anything unexpected; never throws.
6
11
  */
7
- export declare function parseHandoffFrontmatter(content: string): {
8
- title?: string;
9
- created?: string;
10
- };
12
+ export declare function parseHandoffFrontmatter(content: string): ParsedFrontmatter;
11
13
  //# sourceMappingURL=frontmatter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AA6BA;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAqC7F"}
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAoBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CA2C1E"}
@@ -27,9 +27,28 @@ function unescapeYamlDoubleQuoted(value) {
27
27
  return result;
28
28
  }
29
29
  /**
30
- * Parse `title` / `created` from Grounder handoff frontmatter.
31
- * Accepts the quoted `key: "value"` shape {@link writeHandoff} writes today, and
32
- * unquoted `key: value` from earlier handoffs. Not a general YAML parser.
30
+ * Parse a YAML flow sequence like `["a", "b"]` or `[a, b]` into a string array.
31
+ * Returns `undefined` on anything unexpected.
32
+ */
33
+ function parseYamlFlowSequence(raw) {
34
+ const trimmed = raw.trim();
35
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
36
+ return undefined;
37
+ const inner = trimmed.slice(1, -1);
38
+ if (!inner.trim())
39
+ return [];
40
+ return inner.split(",").map((item) => {
41
+ const s = item.trim();
42
+ if (s.startsWith('"') && s.endsWith('"')) {
43
+ return unescapeYamlDoubleQuoted(s.slice(1, -1));
44
+ }
45
+ return s;
46
+ });
47
+ }
48
+ /**
49
+ * Parse `title` / `created` / `topics` from Grounder frontmatter.
50
+ * Accepts the quoted `key: "value"` shape write commands produce, and
51
+ * unquoted `key: value` from earlier files. Not a general YAML parser.
33
52
  * Returns `{}` on anything unexpected; never throws.
34
53
  */
35
54
  export function parseHandoffFrontmatter(content) {
@@ -44,13 +63,19 @@ export function parseHandoffFrontmatter(content) {
44
63
  if (line === "---") {
45
64
  break;
46
65
  }
66
+ const topicsMatch = /^topics:\s*(\[.*\])\s*$/.exec(line);
67
+ if (topicsMatch) {
68
+ const parsed = parseYamlFlowSequence(topicsMatch[1] ?? "");
69
+ if (parsed)
70
+ result.topics = parsed;
71
+ continue;
72
+ }
47
73
  const quoted = /^(title|created):\s*"(.*)"\s*$/.exec(line);
48
74
  if (quoted) {
49
75
  const key = quoted[1];
50
76
  result[key] = unescapeYamlDoubleQuoted(quoted[2] ?? "");
51
77
  continue;
52
78
  }
53
- // Opening quote without a closing pair — ignore rather than treat as unquoted.
54
79
  if (/^(title|created):\s*"/.test(line)) {
55
80
  continue;
56
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,SAAS,wBAAwB,CAAC,KAAa;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBAClC,MAAM,IAAI,IAAI,CAAC;gBACf,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,IAAI,IAAI,CAAC;gBACf,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,MAAM,IAAI,IAAI,CAAC;gBACf,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;QACH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAe;IACrD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;YACvB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,MAAM,GAAyC,EAAE,CAAC;QACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;gBACnB,MAAM;YACR,CAAC;YAED,MAAM,MAAM,GAAG,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3D,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAwB,CAAC;gBAC7C,MAAM,CAAC,GAAG,CAAC,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBACxD,SAAS;YACX,CAAC;YAED,+EAA+E;YAC/E,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvC,SAAS;YACX,CAAC;YAED,MAAM,QAAQ,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5D,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAwB,CAAC;gBAC/C,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAClC,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"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"}
package/dist/util/fs.d.ts CHANGED
@@ -5,6 +5,20 @@ export declare function fileExists(filePath: string): Promise<boolean>;
5
5
  * not oversell POSIX execute-bit semantics there.
6
6
  */
7
7
  export declare function isExecutable(filePath: string): Promise<boolean>;
8
+ /**
9
+ * Write `content` to `filePath` via tmp file + rename, so a crash mid-write
10
+ * never leaves a truncated file at the real path. Creates the destination
11
+ * directory if needed; best-effort removes the tmp file if `rename` itself
12
+ * throws, so a failed write doesn't leave an orphaned `.tmp-*` sibling behind.
13
+ * `mode` (e.g. the pre-existing file's permission bits), when given, is
14
+ * applied to the tmp file before the rename — otherwise the replaced file
15
+ * would pick up the tmp file's default umask permissions instead of keeping
16
+ * whatever the file already had, which matters for host-owned config files
17
+ * edited by other tools.
18
+ */
19
+ export declare function writeFileAtomic(filePath: string, content: string, options?: {
20
+ mode?: number;
21
+ }): Promise<void>;
8
22
  /**
9
23
  * Creates a new markdown file under `dir`; never overwrites.
10
24
  * Tries `basename.md`, then `basename_02.md`, `_03`, … with `wx` (O_EXCL)
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/util/fs.ts"],"names":[],"mappings":"AAIA,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrE;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAgBjB"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/util/fs.ts"],"names":[],"mappings":"AAIA,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9B,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAgBjB"}
package/dist/util/fs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { access, constants, writeFile } from "node:fs/promises";
1
+ import { access, chmod, constants, mkdir, rename, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { collisionSuffix } from "./timestamp-slug.js";
4
4
  export async function fileExists(filePath) {
@@ -24,6 +24,32 @@ export async function isExecutable(filePath) {
24
24
  return false;
25
25
  }
26
26
  }
27
+ /**
28
+ * Write `content` to `filePath` via tmp file + rename, so a crash mid-write
29
+ * never leaves a truncated file at the real path. Creates the destination
30
+ * directory if needed; best-effort removes the tmp file if `rename` itself
31
+ * throws, so a failed write doesn't leave an orphaned `.tmp-*` sibling behind.
32
+ * `mode` (e.g. the pre-existing file's permission bits), when given, is
33
+ * applied to the tmp file before the rename — otherwise the replaced file
34
+ * would pick up the tmp file's default umask permissions instead of keeping
35
+ * whatever the file already had, which matters for host-owned config files
36
+ * edited by other tools.
37
+ */
38
+ export async function writeFileAtomic(filePath, content, options = {}) {
39
+ await mkdir(path.dirname(filePath), { recursive: true });
40
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
41
+ await writeFile(tmpPath, content, "utf8");
42
+ if (options.mode !== undefined) {
43
+ await chmod(tmpPath, options.mode);
44
+ }
45
+ try {
46
+ await rename(tmpPath, filePath);
47
+ }
48
+ catch (error) {
49
+ await unlink(tmpPath).catch(() => { });
50
+ throw error;
51
+ }
52
+ }
27
53
  /**
28
54
  * Creates a new markdown file under `dir`; never overwrites.
29
55
  * Tries `basename.md`, then `basename_02.md`, `_03`, … with `wx` (O_EXCL)
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/util/fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgB;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAW,EACX,QAAgB,EAChB,OAAe;IAEf,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,SAAS,CAAC;QACR,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACrE,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;gBACxB,SAAS;YACX,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/util/fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9F,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgB;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgB,EAChB,OAAe,EACf,UAA6B,EAAE;IAE/B,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,GAAG,QAAQ,QAAQ,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC/D,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACtC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAW,EACX,QAAgB,EAChB,OAAe;IAEf,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,SAAS,CAAC;QACR,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACrE,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;gBACxB,SAAS;YACX,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -1,15 +1,26 @@
1
1
  export type MergeJsonResult = {
2
2
  ok: true;
3
3
  created: boolean;
4
+ changed: boolean;
4
5
  } | {
5
6
  ok: false;
6
7
  error: "unparseable";
7
8
  message: string;
8
9
  };
10
+ export interface MergeJsonFileOptions {
11
+ /** Compute the result without writing to disk. */
12
+ dryRun?: boolean;
13
+ }
9
14
  /**
10
- * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed.
15
+ * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed
16
+ * only when the merged content actually differs from what's on disk, so callers can trust
17
+ * `changed` to mean a real content change rather than "we ran the merge function."
18
+ * A `merge` that returns its `current` argument by reference is treated as a no-op and
19
+ * never writes, even if the file's on-disk formatting (indentation, line endings, key
20
+ * order) differs from `JSON.stringify(current, null, 2)` — callers rely on this to leave
21
+ * a file with nothing to change byte-for-byte untouched instead of reformatting it.
11
22
  * On parse failure or non-object root: leaves the file untouched and returns an error
12
23
  * so callers can warn without clobbering shared config.
13
24
  */
14
- export declare function mergeJsonFile(filePath: string, merge: (current: Record<string, unknown>) => Record<string, unknown>): Promise<MergeJsonResult>;
25
+ export declare function mergeJsonFile(filePath: string, merge: (current: Record<string, unknown>) => Record<string, unknown>, options?: MergeJsonFileOptions): Promise<MergeJsonResult>;
15
26
  //# sourceMappingURL=merge-json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-json.d.ts","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnE,OAAO,CAAC,eAAe,CAAC,CAiC1B"}
1
+ {"version":3,"file":"merge-json.d.ts","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAChD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,eAAe,CAAC,CA2C1B"}
@@ -1,19 +1,25 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileExists } from "./fs.js";
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { fileExists, writeFileAtomic } from "./fs.js";
4
3
  /**
5
- * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed.
4
+ * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed
5
+ * only when the merged content actually differs from what's on disk, so callers can trust
6
+ * `changed` to mean a real content change rather than "we ran the merge function."
7
+ * A `merge` that returns its `current` argument by reference is treated as a no-op and
8
+ * never writes, even if the file's on-disk formatting (indentation, line endings, key
9
+ * order) differs from `JSON.stringify(current, null, 2)` — callers rely on this to leave
10
+ * a file with nothing to change byte-for-byte untouched instead of reformatting it.
6
11
  * On parse failure or non-object root: leaves the file untouched and returns an error
7
12
  * so callers can warn without clobbering shared config.
8
13
  */
9
- export async function mergeJsonFile(filePath, merge) {
14
+ export async function mergeJsonFile(filePath, merge, options = {}) {
10
15
  const existed = await fileExists(filePath);
11
16
  let current = {};
17
+ let originalRaw;
12
18
  if (existed) {
13
- const raw = await readFile(filePath, "utf8");
19
+ originalRaw = await readFile(filePath, "utf8");
14
20
  let parsed;
15
21
  try {
16
- parsed = JSON.parse(raw);
22
+ parsed = JSON.parse(originalRaw);
17
23
  }
18
24
  catch (err) {
19
25
  const detail = err instanceof Error ? err.message : String(err);
@@ -33,8 +39,16 @@ export async function mergeJsonFile(filePath, merge) {
33
39
  current = parsed;
34
40
  }
35
41
  const next = merge(current);
36
- await mkdir(path.dirname(filePath), { recursive: true });
37
- await writeFile(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
38
- return { ok: true, created: !existed };
42
+ const nextRaw = `${JSON.stringify(next, null, 2)}\n`;
43
+ const changed = next !== current && nextRaw !== originalRaw;
44
+ if (changed && !options.dryRun) {
45
+ // Preserve the existing file's permission bits across the tmp+rename
46
+ // swap — these are host-owned config files (Cursor/Claude Code), not
47
+ // Grounder's own state, so a rename shouldn't reset them to the tmp
48
+ // file's default umask.
49
+ const mode = existed ? (await stat(filePath)).mode & 0o777 : undefined;
50
+ await writeFileAtomic(filePath, nextRaw, mode !== undefined ? { mode } : {});
51
+ }
52
+ return { ok: true, created: !existed, changed };
39
53
  }
40
54
  //# sourceMappingURL=merge-json.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-json.js","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAMrC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,KAAoE;IAEpE,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,OAAO,GAA4B,EAAE,CAAC;IAE1C,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC7C,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,mBAAmB,MAAM,GAAG;aACpE,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,oCAAoC;aAC5E,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,MAAiC,CAAC;IAC9C,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC;AACzC,CAAC"}
1
+ {"version":3,"file":"merge-json.js","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAWtD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,KAAoE,EACpE,UAAgC,EAAE;IAElC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,OAAO,GAA4B,EAAE,CAAC;IAC1C,IAAI,WAA+B,CAAC;IAEpC,IAAI,OAAO,EAAE,CAAC;QACZ,WAAW,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,mBAAmB,MAAM,GAAG;aACpE,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,oCAAoC;aAC5E,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,MAAiC,CAAC;IAC9C,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,KAAK,OAAO,IAAI,OAAO,KAAK,WAAW,CAAC;IAE5D,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAC/B,qEAAqE;QACrE,qEAAqE;QACrE,oEAAoE;QACpE,wBAAwB;QACxB,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,MAAM,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;AAClD,CAAC"}
@@ -17,4 +17,27 @@ export declare function isPathInside(parent: string, child: string): boolean;
17
17
  * Returns `null` if either path cannot be resolved (e.g. missing).
18
18
  */
19
19
  export declare function isRealPathInside(parent: string, child: string): Promise<boolean | null>;
20
+ /** `file://` href for an absolute filesystem path (spaces percent-encoded). */
21
+ export declare function toFileUri(filePath: string): string;
22
+ /**
23
+ * Escape text for a Markdown link label (`[label](...)`) so `\`, `[`, and `]`
24
+ * do not break the link syntax.
25
+ */
26
+ export declare function escapeMarkdownLinkLabel(text: string): string;
27
+ /**
28
+ * `[label](fileUri)` for an absolute path. Escapes special chars in the label;
29
+ * percent-encodes `(` / `)` in the URI so they do not close the destination.
30
+ */
31
+ export declare function formatMarkdownFileLink(label: string, filePath: string): string;
32
+ /**
33
+ * Path relative to a vault/project root, always with `/` separators
34
+ * (stable for markdown links and JSON across platforms).
35
+ */
36
+ export declare function vaultRelativePath(rootDir: string, filePath: string): string;
37
+ /**
38
+ * Plain list title: path relative to `rootDir` with a trailing `.md` stripped
39
+ * (e.g. `migration/phase-1`). Falls back to the filename stem when `rootDir`
40
+ * is omitted.
41
+ */
42
+ export declare function vaultItemPlainTitle(filePath: string, rootDir?: string): string;
20
43
  //# sourceMappingURL=path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/util/path.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAElF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAanE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAQ7F"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/util/path.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAElF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAanE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAQ7F;AAED,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG9E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAM9E"}
package/dist/util/path.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { realpath } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
4
5
  /**
5
6
  * Expand a leading `~` / `~/…` to the user home directory.
6
7
  * Leaves other paths unchanged (including a mid-path `~`, which is not home).
@@ -50,4 +51,42 @@ export async function isRealPathInside(parent, child) {
50
51
  return null;
51
52
  }
52
53
  }
54
+ /** `file://` href for an absolute filesystem path (spaces percent-encoded). */
55
+ export function toFileUri(filePath) {
56
+ return pathToFileURL(filePath).href;
57
+ }
58
+ /**
59
+ * Escape text for a Markdown link label (`[label](...)`) so `\`, `[`, and `]`
60
+ * do not break the link syntax.
61
+ */
62
+ export function escapeMarkdownLinkLabel(text) {
63
+ return text.replace(/\\/g, "\\\\").replace(/\[/g, "\\[").replace(/\]/g, "\\]");
64
+ }
65
+ /**
66
+ * `[label](fileUri)` for an absolute path. Escapes special chars in the label;
67
+ * percent-encodes `(` / `)` in the URI so they do not close the destination.
68
+ */
69
+ export function formatMarkdownFileLink(label, filePath) {
70
+ const uri = toFileUri(filePath).replace(/\(/g, "%28").replace(/\)/g, "%29");
71
+ return `[${escapeMarkdownLinkLabel(label)}](${uri})`;
72
+ }
73
+ /**
74
+ * Path relative to a vault/project root, always with `/` separators
75
+ * (stable for markdown links and JSON across platforms).
76
+ */
77
+ export function vaultRelativePath(rootDir, filePath) {
78
+ return path.relative(rootDir, filePath).split(path.sep).join("/");
79
+ }
80
+ /**
81
+ * Plain list title: path relative to `rootDir` with a trailing `.md` stripped
82
+ * (e.g. `migration/phase-1`). Falls back to the filename stem when `rootDir`
83
+ * is omitted.
84
+ */
85
+ export function vaultItemPlainTitle(filePath, rootDir) {
86
+ if (rootDir === undefined) {
87
+ return path.basename(filePath, ".md");
88
+ }
89
+ const rel = vaultRelativePath(rootDir, filePath);
90
+ return rel.endsWith(".md") ? rel.slice(0, -3) : rel;
91
+ }
53
92
  //# sourceMappingURL=path.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"path.js","sourceRoot":"","sources":["../../src/util/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;QAClB,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QAC/D,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IACxE,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,KAAa;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7C,OAAO,CACL,QAAQ,KAAK,EAAE;QACf,QAAQ,KAAK,IAAI;QACjB,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;QACrC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAc,EAAE,KAAa;IAClE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrC,OAAO,IAAI,KAAK,MAAM,IAAI,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../../src/util/path.ts"],"names":[],"mappings":"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"}
@@ -2,9 +2,21 @@ export interface ListHandoffsOptions {
2
2
  /** Max paths to return (newest first). Omit to return all. */
3
3
  limit?: number;
4
4
  }
5
+ /** One handoff with its mtime (see {@link listHandoffsDetailed}). */
6
+ export interface HandoffEntry {
7
+ path: string;
8
+ mtimeMs: number;
9
+ }
5
10
  /**
6
- * Lists handoff markdown files under `logsDir`, newest filename first.
7
- * Returns absolute paths. Missing or empty dirs yield `[]`.
11
+ * Lists handoff markdown files under `logsDir` recursively, newest basename
12
+ * first (timestamp-prefixed names sort correctly), with each entry's mtime
13
+ * attached. Same basename in different subfolders ties break by
14
+ * vault-relative path descending. Missing or empty dirs yield `[]`.
15
+ *
16
+ * Ranking never reads mtime (filenames already sort correctly), so this
17
+ * stats only the paths a `limit` actually keeps, not the whole bucket.
8
18
  */
19
+ export declare function listHandoffsDetailed(logsDir: string, options?: ListHandoffsOptions): Promise<HandoffEntry[]>;
20
+ /** Same ranking as {@link listHandoffsDetailed}, paths only (no stat calls). */
9
21
  export declare function listHandoffs(logsDir: string, options?: ListHandoffsOptions): Promise<string[]>;
10
22
  //# sourceMappingURL=list-handoffs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-handoffs.d.ts","sourceRoot":"","sources":["../../src/vault/list-handoffs.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,MAAM,EAAE,CAAC,CAuBnB"}
1
+ {"version":3,"file":"list-handoffs.d.ts","sourceRoot":"","sources":["../../src/vault/list-handoffs.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AA6BD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,YAAY,EAAE,CAAC,CAMzB;AAED,gFAAgF;AAChF,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB"}