@letrquan/book 0.3.0 → 0.4.0-beta.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 (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
@@ -0,0 +1,931 @@
1
+ import {
2
+ parseFrontmatter
3
+ } from "./chunk-KXAYUIKP.js";
4
+ import {
5
+ isUserHome,
6
+ tryRealPath
7
+ } from "./chunk-SBTU5BEC.js";
8
+ import {
9
+ createDebugLogger
10
+ } from "./chunk-OJBMDM5H.js";
11
+ import {
12
+ createUncGuard
13
+ } from "./chunk-EEX6QNXV.js";
14
+ import {
15
+ writeFileAtomic
16
+ } from "./chunk-REASC2QG.js";
17
+ import {
18
+ resolveBookHome
19
+ } from "./chunk-ZPSZZUWF.js";
20
+
21
+ // src/memory-store.ts
22
+ import {
23
+ closeSync,
24
+ constants,
25
+ copyFileSync,
26
+ existsSync,
27
+ fstatSync,
28
+ linkSync,
29
+ lstatSync,
30
+ mkdirSync,
31
+ openSync,
32
+ readdirSync,
33
+ readFileSync,
34
+ readSync,
35
+ realpathSync,
36
+ renameSync,
37
+ rmdirSync,
38
+ rmSync,
39
+ statSync,
40
+ unlinkSync
41
+ } from "fs";
42
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
43
+
44
+ // src/memory-limits.ts
45
+ var DEFAULT_MAX_INDEX_LINES = 200;
46
+ var MAX_INDEX_BYTES = 25e3;
47
+
48
+ // src/memory-store.ts
49
+ var MEMORY_TYPES = ["user", "feedback", "project", "reference"];
50
+ function sanitizeMemorySlug(rawSlug) {
51
+ const cleaned = rawSlug.replace(/[\x00-\x1f\x7f]/g, "").replace(/[/\\]/g, "").replace(/[[\]()]/g, "").trim().replace(/^\.+/, "").trim();
52
+ return cleaned.length > 0 ? cleaned : null;
53
+ }
54
+ function lastSlugSegment(raw) {
55
+ return raw.split(/[/\\]/).pop() ?? "";
56
+ }
57
+ function memoryFileForSlug(dir, raw) {
58
+ const cleanSlug = sanitizeMemorySlug(lastSlugSegment(raw.trim()));
59
+ if (!cleanSlug) return { error: "Invalid memory slug." };
60
+ const filename = cleanSlug.toLowerCase().endsWith(".md") ? cleanSlug : `${cleanSlug}.md`;
61
+ if (filename.toLowerCase() === INDEX_FILE.toLowerCase()) {
62
+ return { error: "Target filename cannot be the index file." };
63
+ }
64
+ const target = join(dir, filename);
65
+ const rel = relative(dir, target);
66
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
67
+ return { error: "Target path must stay inside the project memory directory." };
68
+ }
69
+ return { filename, target };
70
+ }
71
+ function memoryWriteScope(config) {
72
+ const memory = config.settings?.memory;
73
+ if (memory && (memory.enabled === false || memory.autoSave === false)) return "none";
74
+ return config.sessionDriver === "agent" ? "learnings" : "full";
75
+ }
76
+ var INDEX_FILE = "MEMORY.md";
77
+ var INBOX_DIR = ".inbox";
78
+ var DISCARDED_DIR = "discarded";
79
+ function slugifyWorkspace(workspace) {
80
+ return workspace.replace(/[/\\:]+/g, "-").replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "workspace";
81
+ }
82
+ function bookRoot(opts) {
83
+ return opts?.bookRoot ?? resolveBookHome();
84
+ }
85
+ var log = createDebugLogger("memory-store");
86
+ function errorCode(error) {
87
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
88
+ }
89
+ function canonicalPath(path) {
90
+ const absolute = resolve(path);
91
+ try {
92
+ return realpathSync(absolute);
93
+ } catch {
94
+ return absolute;
95
+ }
96
+ }
97
+ function lstatIfPresent(path) {
98
+ try {
99
+ return lstatSync(path);
100
+ } catch (e) {
101
+ if (errorCode(e) === "ENOENT" || errorCode(e) === "ENOTDIR") return null;
102
+ throw e;
103
+ }
104
+ }
105
+ function statIfPresent(path) {
106
+ try {
107
+ return statSync(path);
108
+ } catch (e) {
109
+ if (errorCode(e) === "ENOENT" || errorCode(e) === "ENOTDIR") return null;
110
+ throw e;
111
+ }
112
+ }
113
+ var MAX_POINTER_FILE_BYTES = 64 * 1024;
114
+ function readPointerFile(path, reachesUnc) {
115
+ if (reachesUnc(path)) return void 0;
116
+ let fd;
117
+ try {
118
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
119
+ } catch (e) {
120
+ const code = errorCode(e);
121
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "EISDIR") return void 0;
122
+ throw e;
123
+ }
124
+ try {
125
+ if (!fstatSync(fd).isFile()) return void 0;
126
+ const buffer = Buffer.alloc(MAX_POINTER_FILE_BYTES + 1);
127
+ let length = 0;
128
+ while (length < buffer.length) {
129
+ const n = readSync(fd, buffer, length, buffer.length - length, null);
130
+ if (n === 0) break;
131
+ length += n;
132
+ }
133
+ if (length > MAX_POINTER_FILE_BYTES) throw new Error(`not a git pointer file: ${path}`);
134
+ return buffer.toString("utf-8", 0, length).split(/\r?\n/, 1)[0].trim();
135
+ } finally {
136
+ closeSync(fd);
137
+ }
138
+ }
139
+ function gitRootMarkedBy(dir, reachesUnc) {
140
+ const dotGit = join(dir, ".git");
141
+ if (reachesUnc(dotGit)) throw new Error(`.git on a UNC path: ${dotGit}`);
142
+ const st = statIfPresent(dotGit);
143
+ if (!st) return void 0;
144
+ if (st.isDirectory()) return dir;
145
+ if (!st.isFile()) return void 0;
146
+ const pointer = /^gitdir:[ \t]*(\S.*)$/.exec(readPointerFile(dotGit, reachesUnc) ?? "");
147
+ if (!pointer) throw new Error(`unrecognized .git file: ${dotGit}`);
148
+ const gitDir = resolve(dir, pointer[1]);
149
+ if (reachesUnc(gitDir)) return dir;
150
+ const commonDir = readPointerFile(join(gitDir, "commondir"), reachesUnc);
151
+ if (commonDir === void 0) return dir;
152
+ const commonPath = resolve(gitDir, commonDir);
153
+ if (reachesUnc(commonPath)) return dir;
154
+ const common = canonicalPath(commonPath);
155
+ return basename(common) === ".git" && isListedWorktree(dir, gitDir, common, reachesUnc) ? dirname(common) : dir;
156
+ }
157
+ function comparablePath(path) {
158
+ return tryRealPath(path) ?? resolve(path);
159
+ }
160
+ function samePath(a, b) {
161
+ return relative(comparablePath(a), comparablePath(b)) === "";
162
+ }
163
+ function isListedWorktree(dir, gitDir, common, reachesUnc) {
164
+ const entry = comparablePath(gitDir);
165
+ if (!samePath(dirname(entry), join(common, "worktrees"))) return false;
166
+ const listing = join(entry, "gitdir");
167
+ try {
168
+ const backPointer = readPointerFile(listing, reachesUnc);
169
+ if (!backPointer) return false;
170
+ const target = resolve(entry, backPointer);
171
+ if (basename(target) !== ".git" || !samePath(dirname(target), dir)) return false;
172
+ return lstatSync(join(dir, ".git")).uid === statSync(listing).uid;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ function findGitRoot(workspace) {
178
+ const reachesUnc = createUncGuard();
179
+ if (reachesUnc(workspace)) return resolve(workspace);
180
+ const resolved = canonicalPath(workspace);
181
+ try {
182
+ for (let dir = resolved; ; ) {
183
+ const root = gitRootMarkedBy(dir, reachesUnc);
184
+ if (root !== void 0) {
185
+ return isUserHome(tryRealPath(dir) ?? dir, void 0) ? resolved : root;
186
+ }
187
+ const parent = dirname(dir);
188
+ if (parent === dir) return resolved;
189
+ dir = parent;
190
+ }
191
+ } catch {
192
+ return resolved;
193
+ }
194
+ }
195
+ function memoryDirIn(root, workspaceKey) {
196
+ return join(root, "projects", slugifyWorkspace(workspaceKey), "memory");
197
+ }
198
+ function getProjectMemoryDir(workspace, opts) {
199
+ return memoryDirIn(bookRoot(opts), findGitRoot(workspace));
200
+ }
201
+ function moveFileNoClobber(src, dst) {
202
+ try {
203
+ linkSync(src, dst);
204
+ } catch (e) {
205
+ if (errorCode(e) === "EEXIST") return "exists";
206
+ if (errorCode(e) === "ENOENT" && !lstatIfPresent(src)) return "gone";
207
+ try {
208
+ copyFileSync(src, dst, constants.COPYFILE_EXCL);
209
+ } catch (copyError) {
210
+ if (errorCode(copyError) === "EEXIST") return "exists";
211
+ if (errorCode(copyError) === "ENOENT" && !lstatIfPresent(src)) return "gone";
212
+ throw copyError;
213
+ }
214
+ }
215
+ try {
216
+ unlinkSync(src);
217
+ } catch (e) {
218
+ if (errorCode(e) !== "ENOENT") throw e;
219
+ }
220
+ return "moved";
221
+ }
222
+ function moveKeepingBoth(sourceDir, targetDir, name) {
223
+ const stem = name.slice(0, -".md".length);
224
+ for (let n = 1; n <= 1e3; n++) {
225
+ const candidate = n === 1 ? name : `${stem}-${n}.md`;
226
+ const outcome = moveFileNoClobber(join(sourceDir, name), join(targetDir, candidate));
227
+ if (outcome === "moved") return candidate;
228
+ if (outcome === "gone") return null;
229
+ }
230
+ throw new Error(`too many name collisions moving ${name}`);
231
+ }
232
+ function ensureRealDir(path) {
233
+ const st = lstatIfPresent(path);
234
+ if (!st) {
235
+ mkdirSync(path, { recursive: true });
236
+ return true;
237
+ }
238
+ return st.isDirectory();
239
+ }
240
+ function isMemoryTopicFile(name) {
241
+ const lower = name.toLowerCase();
242
+ return lower.endsWith(".md") && lower !== INDEX_FILE.toLowerCase();
243
+ }
244
+ function regularMarkdownFiles(dir, accept) {
245
+ const st = lstatIfPresent(dir);
246
+ if (!st?.isDirectory()) return [];
247
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && accept(entry.name)).map((entry) => entry.name).sort();
248
+ }
249
+ function mergeLegacyIndex(oldDir, newDir, renamed) {
250
+ const oldIndex = join(oldDir, INDEX_FILE);
251
+ if (!lstatIfPresent(oldIndex)?.isFile()) return;
252
+ const newIndexStat = lstatIfPresent(join(newDir, INDEX_FILE));
253
+ if (newIndexStat && !newIndexStat.isFile()) return;
254
+ let oldText;
255
+ try {
256
+ oldText = readFileSync(oldIndex, "utf-8");
257
+ } catch (e) {
258
+ if (errorCode(e) === "ENOENT") return;
259
+ throw e;
260
+ }
261
+ const lines = readIndexLines(newDir);
262
+ const listed = /* @__PURE__ */ new Set();
263
+ for (const line of lines) {
264
+ const file = indexEntryFile(line);
265
+ if (file) listed.add(file.toLowerCase());
266
+ }
267
+ const additions = [];
268
+ for (const line of oldText.split("\n")) {
269
+ const file = indexEntryFile(line);
270
+ if (!file) continue;
271
+ const target = renamed.get(file) ?? file;
272
+ if (listed.has(target.toLowerCase())) continue;
273
+ listed.add(target.toLowerCase());
274
+ additions.push(target === file ? line : line.replace(`](${file})`, `](${target})`));
275
+ }
276
+ if (additions.length > 0) {
277
+ const merged = lines.length === 0 ? ["# Book memory index", ...additions] : lines[0].startsWith("#") ? [lines[0], ...additions, ...lines.slice(1)] : [...additions, ...lines];
278
+ writeIndexLines(newDir, merged);
279
+ }
280
+ try {
281
+ unlinkSync(oldIndex);
282
+ } catch (e) {
283
+ if (errorCode(e) !== "ENOENT") throw e;
284
+ }
285
+ }
286
+ function mergeLegacyMemoryDir(oldDir, newDir) {
287
+ const renamed = /* @__PURE__ */ new Map();
288
+ for (const name of regularMarkdownFiles(oldDir, isMemoryTopicFile)) {
289
+ const final = moveKeepingBoth(oldDir, newDir, name);
290
+ if (final && final !== name) renamed.set(name, final);
291
+ }
292
+ for (const sub of [INBOX_DIR, join(INBOX_DIR, DISCARDED_DIR)]) {
293
+ const files = regularMarkdownFiles(
294
+ join(oldDir, sub),
295
+ (name) => name.toLowerCase().endsWith(".md")
296
+ );
297
+ if (files.length === 0) continue;
298
+ const targetDir = join(newDir, sub);
299
+ if (!ensureRealDir(join(newDir, INBOX_DIR)) || !ensureRealDir(targetDir)) continue;
300
+ for (const name of files) moveKeepingBoth(join(oldDir, sub), targetDir, name);
301
+ }
302
+ mergeLegacyIndex(oldDir, newDir, renamed);
303
+ for (const leftover of [
304
+ join(oldDir, INBOX_DIR, DISCARDED_DIR),
305
+ join(oldDir, INBOX_DIR),
306
+ oldDir
307
+ ]) {
308
+ try {
309
+ if (lstatIfPresent(leftover)?.isDirectory()) rmdirSync(leftover);
310
+ } catch {
311
+ }
312
+ }
313
+ }
314
+ function removeEmptyLegacyProjectDir(projectDir) {
315
+ try {
316
+ if (lstatIfPresent(projectDir)?.isDirectory()) rmdirSync(projectDir);
317
+ } catch {
318
+ }
319
+ }
320
+ function migrateLegacyMemoryDir(workspace, opts) {
321
+ try {
322
+ const legacySlug = slugifyWorkspace(workspace);
323
+ if (legacySlug === "." || legacySlug === "..") return;
324
+ const oldDir = memoryDirIn(bookRoot(opts), workspace);
325
+ const newDir = getProjectMemoryDir(workspace, opts);
326
+ if (oldDir === newDir) return;
327
+ if (!lstatIfPresent(oldDir)?.isDirectory()) return;
328
+ if (!lstatIfPresent(newDir)) {
329
+ mkdirSync(dirname(newDir), { recursive: true });
330
+ try {
331
+ renameSync(oldDir, newDir);
332
+ removeEmptyLegacyProjectDir(dirname(oldDir));
333
+ return;
334
+ } catch (e) {
335
+ const code = errorCode(e);
336
+ if (code === "ENOENT" && !lstatIfPresent(oldDir)) return;
337
+ if (code !== "EEXIST" && code !== "ENOTEMPTY" && code !== "ENOENT") throw e;
338
+ }
339
+ }
340
+ if (!lstatIfPresent(newDir)?.isDirectory()) return;
341
+ mergeLegacyMemoryDir(oldDir, newDir);
342
+ removeEmptyLegacyProjectDir(dirname(oldDir));
343
+ } catch (e) {
344
+ log.warn("memory move from the old directory failed", e);
345
+ }
346
+ }
347
+ var FORMAT_FILE = ".format";
348
+ var FORMAT_VERSION = "2";
349
+ var BACKUP_PREFIX = "memory.legacy-";
350
+ var BACKUP_NAME = /^memory\.legacy-(\d{8})(?:-(\d+))?$/;
351
+ var SLUG_MAX = 60;
352
+ var DESCRIPTION_MAX = 150;
353
+ var INDEX_LINE_MAX = 200;
354
+ var TITLE_MAX = 120;
355
+ var DROPPED_STATUSES = /* @__PURE__ */ new Set(["superseded", "pending", "discarded"]);
356
+ var isLegacyLeftover = (name) => name === INBOX_DIR || /^\.extraction[-.]/.test(name);
357
+ function parseLegacyFile(text) {
358
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
359
+ if (lines[0].trim() !== "---") return null;
360
+ const close = lines.indexOf("---", 1);
361
+ if (close < 0) return null;
362
+ const keys = /* @__PURE__ */ new Map();
363
+ const extra = /* @__PURE__ */ new Map();
364
+ let current = null;
365
+ for (const line of lines.slice(1, close)) {
366
+ const key = /^([A-Za-z][\w-]*)[ \t]*:[ \t]*(.*)$/.exec(line);
367
+ if (key) {
368
+ current = key[1];
369
+ keys.set(current, key[2].trim());
370
+ extra.set(current, []);
371
+ } else if (current && line.trim() !== "" && !line.startsWith("#")) {
372
+ extra.get(current)?.push(line);
373
+ }
374
+ }
375
+ return { keys, extra, body: lines.slice(close + 1).join("\n") };
376
+ }
377
+ var unquote = (value) => value.replace(/^(["'])(.*)\1$/, "$2").trim();
378
+ var stripControls = (text) => text.replace(/[\x00-\x1f\x7f]/g, "").trim();
379
+ function shortenAtWord(text, max) {
380
+ if (text.length <= max) return text;
381
+ if (max < 1) return "";
382
+ let end = max - 1;
383
+ for (; ; ) {
384
+ const atBoundary = end === 0 || /\s/.test(text[end]) || /\s/.test(text[end - 1]);
385
+ if (!atBoundary) {
386
+ end = Math.max(0, text.slice(0, end).search(/\s\S*$/));
387
+ continue;
388
+ }
389
+ const ticks = text.slice(0, end).split("`").length - 1;
390
+ if (ticks % 2 === 1) {
391
+ end = text.lastIndexOf("`", end - 1);
392
+ continue;
393
+ }
394
+ break;
395
+ }
396
+ const kept = text.slice(0, end).trimEnd();
397
+ return `${kept}\u2026`;
398
+ }
399
+ function yamlValue(value) {
400
+ const risky = value === "" || /^[\s\-?:,[\]{}#&*!|>'"%@`]/.test(value) || /:(\s|$)|\s#|\s$/.test(value) || /^(?:true|false|null|yes|no|on|off|~|[-+]?[\d.]+)$/i.test(value);
401
+ return risky ? JSON.stringify(value) : value;
402
+ }
403
+ function capSlug(slug, max) {
404
+ if (slug.length <= max) return slug;
405
+ const cut = slug.slice(0, max);
406
+ const whole = slug[max] === "-" ? cut : cut.slice(0, Math.max(cut.lastIndexOf("-"), 0) || max);
407
+ return whole.replace(/-+$/, "");
408
+ }
409
+ function kebabSlug(text) {
410
+ return capSlug(
411
+ text.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""),
412
+ SLUG_MAX
413
+ );
414
+ }
415
+ function titleFromLegacyName(name) {
416
+ return name.replace(/\.md$/i, "").replace(/^\d{4}-\d{2}-\d{2}-/, "").replace(/^(?:user|feedback|project|reference)-/, "").replace(/-(?=[a-f]*\d)[0-9a-f]{6,12}$/, "").replace(/-+/g, " ").trim();
417
+ }
418
+ var OLD_ENTRY_TAIL = /^\s*—\s*[^—]*?\s*—\s*\d{4}-\d{2}-\d{2}(?:\s*—\s*(.*))?$/;
419
+ function parseIndexEntry(line) {
420
+ const entry = /^- \[[^\]]*\]\(([^()]+)\)(.*)$/.exec(line);
421
+ if (!entry) return null;
422
+ const target = entry[1].replace(/^\.\//, "").toLowerCase();
423
+ const old = OLD_ENTRY_TAIL.exec(entry[2]);
424
+ if (old) return { target, hook: old[1] ?? "", old: true };
425
+ return { target, hook: entry[2].replace(/^\s*—\s*/, ""), old: false };
426
+ }
427
+ function indexLine(title, file, description) {
428
+ const plain = title.replace(/[[\]()]/g, "").trim() || file;
429
+ const linkCost = 6 + file.length;
430
+ let shown = shortenAtWord(plain, Math.min(TITLE_MAX, INDEX_LINE_MAX - linkCost));
431
+ if (shown === "\u2026") shown = file;
432
+ const head = `- [${shown}](${file})`;
433
+ const room = INDEX_LINE_MAX - head.length - 3;
434
+ const hook = room > 1 ? shortenAtWord(description, room) : "";
435
+ return hook && hook !== "\u2026" ? `${head} \u2014 ${hook}` : head;
436
+ }
437
+ function legacyModifiedMs(file, name, mtimeMs) {
438
+ const stamped = Date.parse(unquote(file.keys.get("updated") || file.keys.get("created") || ""));
439
+ if (!Number.isNaN(stamped)) return stamped;
440
+ const named = Date.parse(/^\d{4}-\d{2}-\d{2}/.exec(name)?.[0] ?? "");
441
+ return Number.isNaN(named) ? mtimeMs : named;
442
+ }
443
+ function convertLegacyFile(name, file, hook, slug, mtimeMs) {
444
+ const lines = file.body.split("\n");
445
+ let start = 0;
446
+ while (start < lines.length && lines[start].trim() === "") start++;
447
+ let heading;
448
+ if (start < lines.length && /^#\s+\S/.test(lines[start])) {
449
+ heading = lines[start].replace(/^#\s+/, "").replace(/^Candidate:\s*/i, "").trim();
450
+ start++;
451
+ }
452
+ const body = lines.slice(start).join("\n").replace(/^(?:[ \t]*\n)+/, "").trimEnd();
453
+ const title = heading || titleFromLegacyName(name) || "Memory";
454
+ const firstLine = body.split("\n").find((line) => line.trim() !== "") ?? "";
455
+ const description = shortenAtWord(
456
+ stripControls(hook ?? "") || stripControls(firstLine) || stripControls(title),
457
+ DESCRIPTION_MAX
458
+ );
459
+ const finalSlug = slug(title);
460
+ const value = (key) => unquote(file.keys.get(key) ?? "");
461
+ const meta = [];
462
+ if (value("type")) meta.push(` type: ${yamlValue(value("type"))}`);
463
+ if (value("sessionId")) meta.push(` originSessionId: ${yamlValue(value("sessionId"))}`);
464
+ const modified = value("updated") || value("created");
465
+ if (modified) meta.push(` modified: ${yamlValue(modified)}`);
466
+ if (value("driver") === "agent") meta.push(" driver: agent");
467
+ const evidence = file.keys.get("evidence") ?? "";
468
+ const evidenceLines = file.extra.get("evidence") ?? [];
469
+ if (evidence !== "") {
470
+ meta.push(` evidence: ${evidence}`);
471
+ } else if (evidenceLines.length > 0) {
472
+ meta.push(" evidence:");
473
+ for (const line of evidenceLines) {
474
+ const item = /^\s*-\s+(.*)$/.exec(line)?.[1]?.trim();
475
+ meta.push(
476
+ item === void 0 ? ` ${line}` : ` - ${/^(["']).*\1$/.test(item) ? item : yamlValue(item)}`
477
+ );
478
+ }
479
+ }
480
+ const content = [
481
+ "---",
482
+ `name: ${finalSlug}`,
483
+ `description: ${yamlValue(description)}`,
484
+ ...meta.length > 0 ? ["metadata:", ...meta] : [],
485
+ "---",
486
+ ...body ? [body] : []
487
+ ].join("\n") + "\n";
488
+ return {
489
+ oldName: name,
490
+ slug: finalSlug,
491
+ title,
492
+ description,
493
+ content,
494
+ modifiedMs: legacyModifiedMs(file, name, mtimeMs)
495
+ };
496
+ }
497
+ function localDateStamp(now) {
498
+ const pad = (n) => String(n).padStart(2, "0");
499
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
500
+ }
501
+ function copyTreeWithoutLinks(source, target) {
502
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
503
+ const from = join(source, entry.name);
504
+ const to = join(target, entry.name);
505
+ if (entry.isDirectory()) {
506
+ mkdirSync(to);
507
+ copyTreeWithoutLinks(from, to);
508
+ } else if (entry.isFile()) {
509
+ copyFileSync(from, to, constants.COPYFILE_EXCL);
510
+ }
511
+ }
512
+ }
513
+ function backUpMemoryDir(dir) {
514
+ const stamp = localDateStamp(/* @__PURE__ */ new Date());
515
+ for (let n = 1; n <= 1e3; n++) {
516
+ const target = join(dirname(dir), `${BACKUP_PREFIX}${stamp}${n === 1 ? "" : `-${n}`}`);
517
+ try {
518
+ mkdirSync(target);
519
+ } catch (e) {
520
+ if (errorCode(e) === "EEXIST") continue;
521
+ throw e;
522
+ }
523
+ try {
524
+ copyTreeWithoutLinks(dir, target);
525
+ return target;
526
+ } catch (e) {
527
+ rmSync(target, { recursive: true, force: true });
528
+ throw e;
529
+ }
530
+ }
531
+ throw new Error("too many memory backups for one day");
532
+ }
533
+ function findLegacyMemoryBackup(dir) {
534
+ try {
535
+ const found = readdirSync(dirname(dir), { withFileTypes: true }).filter((entry) => entry.isDirectory() && BACKUP_NAME.test(entry.name)).map((entry) => {
536
+ const [, date, n] = BACKUP_NAME.exec(entry.name) ?? [];
537
+ return { name: entry.name, date, n: Number(n ?? 1) };
538
+ }).sort((a, b) => a.date.localeCompare(b.date) || a.n - b.n);
539
+ const newest = found.at(-1);
540
+ return newest ? join(dirname(dir), newest.name) : null;
541
+ } catch {
542
+ return null;
543
+ }
544
+ }
545
+ function legacyBackupNotice(backup) {
546
+ return `Migrated to the new memory format; the old files are in ${backup}.`;
547
+ }
548
+ function migratedIndexLines(oldText, converted, droppedNames) {
549
+ const byTarget = new Map(converted.map((c) => [`${c.slug}.md`, c]));
550
+ for (const c of converted) byTarget.set(c.oldName.toLowerCase(), c);
551
+ const kept = [];
552
+ const ordered = [];
553
+ for (const raw of oldText?.split("\n") ?? []) {
554
+ const line = raw.replace(/\r$/, "");
555
+ if (/^#\s*Book memory index\s*$/i.test(line)) continue;
556
+ const entry = parseIndexEntry(line);
557
+ if (!entry) {
558
+ kept.push(line);
559
+ continue;
560
+ }
561
+ const match = byTarget.get(entry.target);
562
+ if (match) {
563
+ if (!ordered.includes(match)) ordered.push(match);
564
+ } else if (!droppedNames.has(entry.target) && !entry.old) {
565
+ kept.push(line);
566
+ }
567
+ }
568
+ const missing = converted.filter((c) => !ordered.includes(c)).sort((a, b) => b.modifiedMs - a.modifiedMs || b.oldName.localeCompare(a.oldName));
569
+ const tidy = [];
570
+ for (const line of kept) {
571
+ if (line.trim() === "" && (tidy.length === 0 || tidy[tidy.length - 1].trim() === "")) continue;
572
+ tidy.push(line);
573
+ }
574
+ while (tidy.length > 0 && tidy[tidy.length - 1].trim() === "") tidy.pop();
575
+ return [
576
+ ...tidy,
577
+ ...[...ordered, ...missing].map((c) => indexLine(c.title, `${c.slug}.md`, c.description))
578
+ ];
579
+ }
580
+ function migrateFormat(dir) {
581
+ if (!lstatIfPresent(dir)?.isDirectory()) return;
582
+ const marker = join(dir, FORMAT_FILE);
583
+ if (lstatIfPresent(marker)?.isFile() && readFileSync(marker, "utf-8").trim() === FORMAT_VERSION) {
584
+ return;
585
+ }
586
+ const everything = readdirSync(dir, { withFileTypes: true });
587
+ const entries = everything.filter((e) => !e.isSymbolicLink());
588
+ const leftovers = entries.filter((e) => isLegacyLeftover(e.name)).map((e) => e.name);
589
+ const legacy = [];
590
+ for (const entry of entries) {
591
+ if (!entry.isFile() || entry.name.startsWith(".") || !isMemoryTopicFile(entry.name)) continue;
592
+ const path = join(dir, entry.name);
593
+ const file = parseLegacyFile(readFileSync(path, "utf-8"));
594
+ if (file && (file.keys.has("status") || file.keys.has("type"))) {
595
+ legacy.push({ name: entry.name, file, mtimeMs: statSync(path).mtimeMs });
596
+ }
597
+ }
598
+ if (legacy.length === 0 && leftovers.length === 0) {
599
+ writeFileAtomic(marker, `${FORMAT_VERSION}
600
+ `);
601
+ return;
602
+ }
603
+ const indexPath = join(dir, INDEX_FILE);
604
+ const indexStat = lstatIfPresent(indexPath);
605
+ const oldIndex = indexStat?.isFile() ? readFileSync(indexPath, "utf-8") : null;
606
+ const hooks = /* @__PURE__ */ new Map();
607
+ for (const line of oldIndex?.split("\n") ?? []) {
608
+ const entry = parseIndexEntry(line.replace(/\r$/, ""));
609
+ if (entry && !hooks.has(entry.target)) hooks.set(entry.target, entry.hook);
610
+ }
611
+ const backup = backUpMemoryDir(dir);
612
+ log.info(`memory backed up to ${backup}`);
613
+ const taken = new Set(everything.map((e) => e.name.toLowerCase()));
614
+ taken.add(INDEX_FILE.toLowerCase());
615
+ const converted = [];
616
+ const dropped = /* @__PURE__ */ new Set();
617
+ for (const { name, file, mtimeMs } of legacy.sort((a, b) => a.name.localeCompare(b.name))) {
618
+ const status = unquote(file.keys.get("status") ?? "").toLowerCase();
619
+ if (DROPPED_STATUSES.has(status)) {
620
+ dropped.add(name.toLowerCase());
621
+ continue;
622
+ }
623
+ const own = name.toLowerCase();
624
+ const claim = (title) => {
625
+ const base = kebabSlug(title) || kebabSlug(titleFromLegacyName(name)) || kebabSlug(unquote(file.keys.get("type") ?? "")) || "memory";
626
+ for (let n = 1; ; n++) {
627
+ const suffix = n === 1 ? "" : `-${n}`;
628
+ const slug = `${capSlug(base, SLUG_MAX - suffix.length)}${suffix}`;
629
+ const lower = `${slug}.md`;
630
+ if (!taken.has(lower) || lower === own) {
631
+ taken.add(lower);
632
+ return slug;
633
+ }
634
+ }
635
+ };
636
+ converted.push(convertLegacyFile(name, file, hooks.get(own) || void 0, claim, mtimeMs));
637
+ }
638
+ for (const c of converted) {
639
+ writeFileAtomic(join(dir, `${c.slug}.md`), c.content);
640
+ if (`${c.slug}.md` !== c.oldName) unlinkSync(join(dir, c.oldName));
641
+ }
642
+ for (const name of dropped) {
643
+ const original = legacy.find((l) => l.name.toLowerCase() === name)?.name;
644
+ if (original) unlinkSync(join(dir, original));
645
+ }
646
+ for (const name of leftovers) rmSync(join(dir, name), { recursive: true, force: true });
647
+ if (indexStat === null || indexStat.isFile()) {
648
+ const lines = migratedIndexLines(oldIndex, converted, dropped);
649
+ if (oldIndex !== null || lines.length > 0) {
650
+ writeFileAtomic(indexPath, lines.length > 0 ? `${lines.join("\n")}
651
+ ` : "");
652
+ }
653
+ } else {
654
+ log.warn("MEMORY.md is not a regular file; it was left as it is");
655
+ }
656
+ writeFileAtomic(marker, `${FORMAT_VERSION}
657
+ `);
658
+ }
659
+ function migrateLegacyMemoryFormat(dir) {
660
+ try {
661
+ migrateFormat(dir);
662
+ } catch (e) {
663
+ log.warn("memory format migration failed", e);
664
+ }
665
+ }
666
+ function isMemoryType(value) {
667
+ return typeof value === "string" && MEMORY_TYPES.includes(value);
668
+ }
669
+ function titleFromBody(body) {
670
+ const firstHeading = body.split("\n").find((line) => line.startsWith("# "));
671
+ return firstHeading?.replace(/^#\s+/, "").trim();
672
+ }
673
+ function summarizeMemoryFile(path, name = basename(path)) {
674
+ try {
675
+ const st = statSync(path);
676
+ if (!st.isFile()) return null;
677
+ const { body, frontmatter } = parseFrontmatter(readFileSync(path, "utf-8"));
678
+ const title = typeof frontmatter.title === "string" && frontmatter.title || titleFromBody(body) || typeof frontmatter.name === "string" && frontmatter.name || void 0;
679
+ return {
680
+ name,
681
+ path,
682
+ type: isMemoryType(frontmatter.type) ? frontmatter.type : void 0,
683
+ title,
684
+ ...typeof frontmatter.description === "string" && frontmatter.description.trim() ? { description: frontmatter.description.trim() } : {},
685
+ size: st.size,
686
+ ...frontmatter.driver === "agent" ? { driver: "agent" } : {},
687
+ ...frontmatter.status === "superseded" ? { legacySuperseded: true } : {}
688
+ };
689
+ } catch {
690
+ return null;
691
+ }
692
+ }
693
+ function listMemoryFiles(workspace, opts) {
694
+ const dir = getProjectMemoryDir(workspace, opts);
695
+ if (!existsSync(dir)) return [];
696
+ const files = [];
697
+ try {
698
+ for (const entry of readdirSync(dir).sort()) {
699
+ if (entry.startsWith(".") || !isMemoryTopicFile(entry)) continue;
700
+ const summary = summarizeMemoryFile(join(dir, entry), entry);
701
+ if (summary) files.push(summary);
702
+ }
703
+ } catch {
704
+ return files;
705
+ }
706
+ return files;
707
+ }
708
+ var formatCount = (n) => n.toLocaleString("en-US");
709
+ function countNonEmptyLines(text) {
710
+ return text.split("\n").filter((line) => line.trim().length > 0).length;
711
+ }
712
+ function cutIndex(raw, maxLines) {
713
+ const trimmed = raw.trim();
714
+ const byteCount = Buffer.byteLength(trimmed);
715
+ const lines = trimmed.length > 0 ? trimmed.split("\n") : [];
716
+ const wasLineTruncated = lines.length > maxLines;
717
+ let kept = wasLineTruncated ? lines.slice(0, maxLines).join("\n") : trimmed;
718
+ let keptLineCount = Math.min(lines.length, maxLines);
719
+ let wasByteTruncated = false;
720
+ let bytesKeptOfLine1;
721
+ if (Buffer.byteLength(kept) > MAX_INDEX_BYTES) {
722
+ wasByteTruncated = true;
723
+ const bytes = Buffer.from(kept, "utf-8");
724
+ const newline = bytes.lastIndexOf(10, MAX_INDEX_BYTES);
725
+ if (newline > 0) {
726
+ kept = bytes.subarray(0, newline).toString("utf-8");
727
+ keptLineCount = kept.split("\n").length;
728
+ } else {
729
+ let end = MAX_INDEX_BYTES;
730
+ while (end > 0 && (bytes[end] & 192) === 128) end--;
731
+ kept = bytes.subarray(0, end).toString("utf-8");
732
+ keptLineCount = 1;
733
+ bytesKeptOfLine1 = end;
734
+ }
735
+ }
736
+ let text = kept;
737
+ if (wasLineTruncated || wasByteTruncated) {
738
+ const detail = bytesKeptOfLine1 !== void 0 ? `everything after the first ${formatCount(bytesKeptOfLine1)} bytes of line 1 was cut off` : `${formatCount(lines.length - keptLineCount)} of ${formatCount(lines.length)} lines were cut off, starting at line ${formatCount(keptLineCount + 1)} ("${Array.from(lines[keptLineCount]).slice(0, 80).join("")}")`;
739
+ text = `${kept}
740
+
741
+ > WARNING: MEMORY.md is ${formatCount(lines.length)} lines and ${formatCount(byteCount)} bytes (limits: ${formatCount(maxLines)} lines, ${formatCount(MAX_INDEX_BYTES)} bytes). Only part of it was loaded: ${detail}. Keep each entry to one line and move detail into the memory file.`;
742
+ }
743
+ return {
744
+ text,
745
+ totalLines: countNonEmptyLines(trimmed),
746
+ loadedLines: countNonEmptyLines(kept),
747
+ byteCount,
748
+ wasLineTruncated,
749
+ wasByteTruncated
750
+ };
751
+ }
752
+ function loadMemoryContext(workspace, opts) {
753
+ migrateLegacyMemoryDir(workspace, opts);
754
+ const dir = getProjectMemoryDir(workspace, opts);
755
+ migrateLegacyMemoryFormat(dir);
756
+ const maxIndexLines = opts?.maxIndexLines ?? DEFAULT_MAX_INDEX_LINES;
757
+ const result = {
758
+ dir,
759
+ indexFile: null,
760
+ indexLoaded: false,
761
+ indexLineCount: 0,
762
+ loadedLineCount: 0,
763
+ indexByteCount: 0,
764
+ wasLineTruncated: false,
765
+ wasByteTruncated: false,
766
+ indexText: "",
767
+ files: []
768
+ };
769
+ if (!existsSync(dir)) return result;
770
+ const indexFile = join(dir, INDEX_FILE);
771
+ if (existsSync(indexFile)) {
772
+ try {
773
+ const st = statSync(indexFile);
774
+ if (st.isFile()) {
775
+ const index = cutIndex(readFileSync(indexFile, "utf-8"), maxIndexLines);
776
+ result.indexFile = indexFile;
777
+ result.indexLoaded = index.text.length > 0;
778
+ result.indexLineCount = index.totalLines;
779
+ result.loadedLineCount = index.loadedLines;
780
+ result.indexByteCount = index.byteCount;
781
+ result.wasLineTruncated = index.wasLineTruncated;
782
+ result.wasByteTruncated = index.wasByteTruncated;
783
+ result.indexText = index.text;
784
+ }
785
+ } catch {
786
+ result.indexFile = indexFile;
787
+ }
788
+ }
789
+ result.files = listMemoryFiles(workspace, opts);
790
+ return result;
791
+ }
792
+ function getNewestMemoryWriteTime(memoryDir) {
793
+ if (!existsSync(memoryDir)) return null;
794
+ let newestMs = 0;
795
+ let entries;
796
+ try {
797
+ entries = readdirSync(memoryDir);
798
+ } catch {
799
+ return null;
800
+ }
801
+ for (const entry of entries) {
802
+ if (entry.startsWith(".")) continue;
803
+ try {
804
+ const st = lstatSync(join(memoryDir, entry));
805
+ if (st.isSymbolicLink() || !st.isFile()) continue;
806
+ if (st.mtimeMs > newestMs) newestMs = st.mtimeMs;
807
+ } catch {
808
+ }
809
+ }
810
+ return newestMs > 0 ? new Date(newestMs) : null;
811
+ }
812
+ function getMemoryHealth(workspaceOrContext, opts) {
813
+ const dir = typeof workspaceOrContext === "string" ? getProjectMemoryDir(workspaceOrContext, opts) : workspaceOrContext.dir;
814
+ let fileCount = 0;
815
+ let legacySupersededCount = 0;
816
+ if (existsSync(dir)) {
817
+ try {
818
+ for (const entry of readdirSync(dir)) {
819
+ if (entry.startsWith(".") || !isMemoryTopicFile(entry)) continue;
820
+ const full = join(dir, entry);
821
+ try {
822
+ const st = lstatSync(full);
823
+ if (st.isSymbolicLink() || !st.isFile()) continue;
824
+ const summary = summarizeMemoryFile(full, entry);
825
+ if (!summary) continue;
826
+ fileCount++;
827
+ if (summary.legacySuperseded) legacySupersededCount++;
828
+ } catch {
829
+ }
830
+ }
831
+ } catch {
832
+ }
833
+ }
834
+ let indexLineCount = 0;
835
+ let indexByteCount = 0;
836
+ let wasLineTruncated = false;
837
+ let wasByteTruncated = false;
838
+ try {
839
+ indexLineCount = readIndexLines(dir).length;
840
+ const indexPath = join(dir, INDEX_FILE);
841
+ if (existsSync(indexPath)) {
842
+ const cut = cutIndex(
843
+ readFileSync(indexPath, "utf-8"),
844
+ opts?.maxIndexLines ?? DEFAULT_MAX_INDEX_LINES
845
+ );
846
+ indexByteCount = cut.byteCount;
847
+ wasLineTruncated = cut.wasLineTruncated;
848
+ wasByteTruncated = cut.wasByteTruncated;
849
+ }
850
+ } catch {
851
+ }
852
+ return {
853
+ fileCount,
854
+ legacySupersededCount,
855
+ indexLineCount,
856
+ indexByteCount,
857
+ wasLineTruncated,
858
+ wasByteTruncated,
859
+ lastWrite: getNewestMemoryWriteTime(dir)
860
+ };
861
+ }
862
+ function indexEntryFile(line) {
863
+ return /^- \[[^\]]*\]\(([^()]+)\)/.exec(line)?.[1];
864
+ }
865
+ function readIndexLines(dir) {
866
+ const indexPath = join(dir, INDEX_FILE);
867
+ if (!existsSync(indexPath)) return [];
868
+ return readFileSync(indexPath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
869
+ }
870
+ function writeIndexLines(dir, lines) {
871
+ writeFileAtomic(
872
+ join(dir, INDEX_FILE),
873
+ lines.length > 0 ? lines.join("\n") + "\n" : "# Book memory index\n"
874
+ );
875
+ }
876
+ function removeIndexEntries(dir, filename) {
877
+ const indexPath = join(dir, INDEX_FILE);
878
+ if (!lstatIfPresent(indexPath)?.isFile()) return;
879
+ const raw = readFileSync(indexPath, "utf-8");
880
+ const target = filename.toLowerCase();
881
+ const lines = raw.split("\n");
882
+ const kept = lines.filter((line) => {
883
+ const file = indexEntryFile(line);
884
+ return file === void 0 || file.replace(/^\.\//, "").toLowerCase() !== target;
885
+ });
886
+ if (kept.length !== lines.length) writeFileAtomic(indexPath, kept.join("\n"));
887
+ }
888
+ function deleteMemoryEntry(workspace, slug, opts) {
889
+ try {
890
+ const dir = getProjectMemoryDir(workspace, opts);
891
+ const named = memoryFileForSlug(dir, slug);
892
+ if ("error" in named) return { ok: false, error: named.error };
893
+ const { filename, target } = named;
894
+ if (!lstatIfPresent(target)) {
895
+ return { ok: false, error: `Memory file not found: ${filename}` };
896
+ }
897
+ if (lstatSync(target).isSymbolicLink()) {
898
+ return { ok: false, error: "Refusing to delete symlinked file." };
899
+ }
900
+ unlinkSync(target);
901
+ try {
902
+ removeIndexEntries(dir, filename);
903
+ } catch {
904
+ }
905
+ return { ok: true, path: target };
906
+ } catch (e) {
907
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
908
+ }
909
+ }
910
+
911
+ export {
912
+ DEFAULT_MAX_INDEX_LINES,
913
+ MAX_INDEX_BYTES,
914
+ sanitizeMemorySlug,
915
+ memoryFileForSlug,
916
+ memoryWriteScope,
917
+ slugifyWorkspace,
918
+ findGitRoot,
919
+ getProjectMemoryDir,
920
+ migrateLegacyMemoryDir,
921
+ shortenAtWord,
922
+ findLegacyMemoryBackup,
923
+ legacyBackupNotice,
924
+ migrateLegacyMemoryFormat,
925
+ listMemoryFiles,
926
+ loadMemoryContext,
927
+ getNewestMemoryWriteTime,
928
+ getMemoryHealth,
929
+ deleteMemoryEntry
930
+ };
931
+ //# sourceMappingURL=chunk-EFXVSQ5J.js.map