finch-markdown-editor 0.1.9 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,69 +1,13 @@
1
1
  // src/index.ts
2
- import { createHash } from "node:crypto";
3
- import { mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
2
+ import { createHash as createHash3 } from "node:crypto";
3
+ import { mkdir as mkdir2, readFile as readFile2, realpath, stat, writeFile as writeFile2 } from "node:fs/promises";
4
4
  import { watch } from "node:fs";
5
- import { spawn } from "node:child_process";
6
- import path from "node:path";
7
- import { fileURLToPath } from "node:url";
5
+ import { spawn as spawn2 } from "node:child_process";
6
+ import path3 from "node:path";
8
7
  import os from "node:os";
9
- var PASTE_IMAGE_EXT = {
10
- "image/png": "png",
11
- "image/jpeg": "jpg",
12
- "image/jpg": "jpg",
13
- "image/gif": "gif",
14
- "image/webp": "webp",
15
- "image/svg+xml": "svg",
16
- "image/bmp": "bmp"
17
- };
18
- var IMAGE_MIME_BY_EXT = {
19
- ".png": "image/png",
20
- ".jpg": "image/jpeg",
21
- ".jpeg": "image/jpeg",
22
- ".gif": "image/gif",
23
- ".webp": "image/webp",
24
- ".svg": "image/svg+xml",
25
- ".bmp": "image/bmp"
26
- };
27
- var MAX_CLIPBOARD_INLINE_IMAGE_BYTES = 20 * 1024 * 1024;
28
- async function readClipboardImageDataUrls(ctx, urls) {
29
- const result2 = {};
30
- const assetsRoot = await realpath(path.join(ctx.storagePath, "assets"));
31
- for (const originalUrl of urls.slice(0, 30)) {
32
- try {
33
- const parsed = new URL(originalUrl);
34
- const requested = parsed.protocol === "finch-file:" && parsed.hostname === "local" ? parsed.searchParams.get("path") : null;
35
- if (!requested) continue;
36
- const target = await realpath(requested);
37
- const relative = path.relative(assetsRoot, target);
38
- const mimeType = IMAGE_MIME_BY_EXT[path.extname(target).toLowerCase()];
39
- if ((!relative || !relative.startsWith(".." + path.sep) && relative !== "..") && mimeType) {
40
- const info = await stat(target);
41
- if (info.size > MAX_CLIPBOARD_INLINE_IMAGE_BYTES) continue;
42
- result2[originalUrl] = `data:${mimeType};base64,${(await readFile(target)).toString("base64")}`;
43
- }
44
- } catch {
45
- }
46
- }
47
- return result2;
48
- }
49
- async function openLocalImagePreview(ctx, filePath) {
50
- if (!path.isAbsolute(filePath) || !IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()]) {
51
- throw new Error("Unsupported local image file.");
52
- }
53
- await ctx.ui.openFilePreview(filePath);
54
- }
55
- async function openMarkdownImagePreview(ctx, rawUrl) {
56
- const url = new URL(rawUrl);
57
- if (url.protocol === "http:" || url.protocol === "https:") {
58
- await ctx.browser.open(url.href);
59
- return;
60
- }
61
- throw new Error(`Unsupported image URL: ${url.protocol}`);
62
- }
63
- var STYLE_SLOT_COUNT = 3;
64
- function result(message, isError = false) {
65
- return { content: [{ type: "text", text: message }], isError };
66
- }
8
+
9
+ // src/document.ts
10
+ import path from "node:path";
67
11
  function unwrapTextEnvelope(content) {
68
12
  const bom = content.startsWith("\uFEFF") ? "\uFEFF" : "";
69
13
  const text = bom ? content.slice(1) : content;
@@ -119,43 +63,14 @@ function documentTitle(markdown, filePath) {
119
63
  const heading = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim();
120
64
  return heading || (filePath ? path.basename(filePath, path.extname(filePath)) : "Untitled article");
121
65
  }
122
- function stateFile(ctx) {
123
- return path.join(ctx.storagePath, "state.json");
124
- }
125
- function styleSlotsFile(ctx) {
126
- return path.join(ctx.storagePath, "style-slots.json");
127
- }
128
- function normalizeStyleSlots(raw) {
129
- const arr = Array.isArray(raw) ? raw : [];
130
- const slots = [];
131
- for (let i = 0; i < STYLE_SLOT_COUNT; i++) {
132
- const item = arr[i];
133
- if (item && typeof item === "object" && typeof item.css === "string") {
134
- slots.push({ css: item.css, label: String(item.label ?? "\u81EA\u5B9A\u4E49\u98CE\u683C") });
135
- } else {
136
- slots.push(null);
137
- }
138
- }
139
- return slots;
140
- }
141
- async function readStyleSlots(ctx) {
142
- try {
143
- const raw = await readFile(styleSlotsFile(ctx), "utf8");
144
- return normalizeStyleSlots(JSON.parse(raw));
145
- } catch {
146
- return normalizeStyleSlots([]);
147
- }
148
- }
149
- async function writeStyleSlot(ctx, slot, value) {
150
- const slots = await readStyleSlots(ctx);
151
- slots[slot] = value;
152
- await mkdir(ctx.storagePath, { recursive: true });
153
- await writeFile(styleSlotsFile(ctx), JSON.stringify(slots), "utf8");
154
- return slots;
155
- }
66
+
67
+ // src/drafts.ts
68
+ import { createHash } from "node:crypto";
69
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
70
+ import path2 from "node:path";
156
71
  function draftPathFor(ctx, sourcePath) {
157
72
  const digest = createHash("sha256").update(sourcePath).digest("hex").slice(0, 32);
158
- return path.join(ctx.storagePath, "drafts", `${digest}.json`);
73
+ return path2.join(ctx.storagePath, "drafts", `${digest}.json`);
159
74
  }
160
75
  function hashText(text) {
161
76
  return createHash("sha256").update(text).digest("hex");
@@ -171,7 +86,7 @@ async function readDraft(ctx, sourcePath) {
171
86
  async function writeDraft(ctx, sourcePath, markdown, base) {
172
87
  try {
173
88
  const file = draftPathFor(ctx, sourcePath);
174
- await mkdir(path.dirname(file), { recursive: true });
89
+ await mkdir(path2.dirname(file), { recursive: true });
175
90
  const entry = { path: sourcePath, markdown, savedAt: Date.now() };
176
91
  if (typeof base === "string") entry.baseHash = hashText(base);
177
92
  await writeFile(file, JSON.stringify(entry), "utf8");
@@ -216,23 +131,165 @@ async function readFileWithDraft(ctx, sourcePath) {
216
131
  if (diskMatchesBaseline) return { markdown: draft.markdown, diskMarkdown, draftRestored: true, draftConflict: false };
217
132
  return { markdown: diskMarkdown, diskMarkdown, draftRestored: false, draftConflict: true };
218
133
  }
134
+
135
+ // src/renderer.ts
136
+ import { createHash as createHash2 } from "node:crypto";
137
+ import { spawn } from "node:child_process";
138
+ import { fileURLToPath } from "node:url";
139
+ var BMMD_BIN_PATH = fileURLToPath(new URL("./bmmd/bin/bmmd.mjs", import.meta.url));
140
+ async function runBmmd(args, input) {
141
+ const binPath = BMMD_BIN_PATH;
142
+ return new Promise((resolve, reject) => {
143
+ const child = spawn(process.execPath, [binPath, ...args], { stdio: ["pipe", "pipe", "pipe"] });
144
+ let output = "";
145
+ let errors = "";
146
+ child.stdout.setEncoding("utf8");
147
+ child.stderr.setEncoding("utf8");
148
+ child.stdout.on("data", (chunk) => {
149
+ output += chunk;
150
+ });
151
+ child.stderr.on("data", (chunk) => {
152
+ errors += chunk;
153
+ });
154
+ child.on("error", reject);
155
+ child.on("close", (code) => {
156
+ if (code === 0) resolve(output);
157
+ else reject(new Error(errors.trim() || `bmmd exited with code ${code}`));
158
+ });
159
+ child.stdin.end(input, "utf8");
160
+ });
161
+ }
162
+ var FINCH_FILE_IMAGE_RE = /finch-file:\/\/local\?path=[^\s)"']+/g;
163
+ var FINCH_IMAGE_PLACEHOLDER_ORIGIN = "https://finch-local.invalid/markdown-image/";
164
+ function substituteFinchFileImagesForBm(markdown) {
165
+ const urls = /* @__PURE__ */ new Map();
166
+ let sequence = 0;
167
+ const substituted = markdown.replace(FINCH_FILE_IMAGE_RE, (originalUrl) => {
168
+ const placeholder = `${FINCH_IMAGE_PLACEHOLDER_ORIGIN}${createHash2("sha256").update(`${originalUrl}:${sequence++}`).digest("hex")}`;
169
+ urls.set(placeholder, originalUrl);
170
+ return placeholder;
171
+ });
172
+ return { markdown: substituted, urls };
173
+ }
174
+ async function renderWithBm(markdown, markdownStyle, customCss) {
175
+ const args = ["render", "--platform", "wechat", "--markdown-style", markdownStyle || "kami"];
176
+ if (customCss && customCss.trim()) args.push("--custom-css", customCss);
177
+ const prepared = substituteFinchFileImagesForBm(markdown);
178
+ let html = await runBmmd(args, prepared.markdown);
179
+ for (const [placeholder, originalUrl] of prepared.urls) html = html.split(placeholder).join(originalUrl);
180
+ return html;
181
+ }
182
+
183
+ // src/index.ts
184
+ var PASTE_IMAGE_EXT = {
185
+ "image/png": "png",
186
+ "image/jpeg": "jpg",
187
+ "image/jpg": "jpg",
188
+ "image/gif": "gif",
189
+ "image/webp": "webp",
190
+ "image/svg+xml": "svg",
191
+ "image/bmp": "bmp"
192
+ };
193
+ var IMAGE_MIME_BY_EXT = {
194
+ ".png": "image/png",
195
+ ".jpg": "image/jpeg",
196
+ ".jpeg": "image/jpeg",
197
+ ".gif": "image/gif",
198
+ ".webp": "image/webp",
199
+ ".svg": "image/svg+xml",
200
+ ".bmp": "image/bmp"
201
+ };
202
+ var MAX_CLIPBOARD_INLINE_IMAGE_BYTES = 20 * 1024 * 1024;
203
+ async function readClipboardImageDataUrls(ctx, urls) {
204
+ const result2 = {};
205
+ const assetsRoot = await realpath(path3.join(ctx.storagePath, "assets"));
206
+ for (const originalUrl of urls.slice(0, 30)) {
207
+ try {
208
+ const parsed = new URL(originalUrl);
209
+ const requested = parsed.protocol === "finch-file:" && parsed.hostname === "local" ? parsed.searchParams.get("path") : null;
210
+ if (!requested) continue;
211
+ const target = await realpath(requested);
212
+ const relative = path3.relative(assetsRoot, target);
213
+ const mimeType = IMAGE_MIME_BY_EXT[path3.extname(target).toLowerCase()];
214
+ if ((!relative || !relative.startsWith(".." + path3.sep) && relative !== "..") && mimeType) {
215
+ const info = await stat(target);
216
+ if (info.size > MAX_CLIPBOARD_INLINE_IMAGE_BYTES) continue;
217
+ result2[originalUrl] = `data:${mimeType};base64,${(await readFile2(target)).toString("base64")}`;
218
+ }
219
+ } catch {
220
+ }
221
+ }
222
+ return result2;
223
+ }
224
+ async function openLocalImagePreview(ctx, filePath) {
225
+ if (!path3.isAbsolute(filePath) || !IMAGE_MIME_BY_EXT[path3.extname(filePath).toLowerCase()]) {
226
+ throw new Error("Unsupported local image file.");
227
+ }
228
+ await ctx.ui.openFilePreview(filePath);
229
+ }
230
+ async function openMarkdownImagePreview(ctx, rawUrl) {
231
+ const url = new URL(rawUrl);
232
+ if (url.protocol === "http:" || url.protocol === "https:") {
233
+ await ctx.browser.open(url.href);
234
+ return;
235
+ }
236
+ throw new Error(`Unsupported image URL: ${url.protocol}`);
237
+ }
238
+ var STYLE_SLOT_COUNT = 3;
239
+ function result(message, isError = false) {
240
+ return { content: [{ type: "text", text: message }], isError };
241
+ }
242
+ function stateFile(ctx) {
243
+ return path3.join(ctx.storagePath, "state.json");
244
+ }
245
+ function styleSlotsFile(ctx) {
246
+ return path3.join(ctx.storagePath, "style-slots.json");
247
+ }
248
+ function normalizeStyleSlots(raw) {
249
+ const arr = Array.isArray(raw) ? raw : [];
250
+ const slots = [];
251
+ for (let i = 0; i < STYLE_SLOT_COUNT; i++) {
252
+ const item = arr[i];
253
+ if (item && typeof item === "object" && typeof item.css === "string") {
254
+ slots.push({ css: item.css, label: String(item.label ?? "\u81EA\u5B9A\u4E49\u98CE\u683C") });
255
+ } else {
256
+ slots.push(null);
257
+ }
258
+ }
259
+ return slots;
260
+ }
261
+ async function readStyleSlots(ctx) {
262
+ try {
263
+ const raw = await readFile2(styleSlotsFile(ctx), "utf8");
264
+ return normalizeStyleSlots(JSON.parse(raw));
265
+ } catch {
266
+ return normalizeStyleSlots([]);
267
+ }
268
+ }
269
+ async function writeStyleSlot(ctx, slot, value) {
270
+ const slots = await readStyleSlots(ctx);
271
+ slots[slot] = value;
272
+ await mkdir2(ctx.storagePath, { recursive: true });
273
+ await writeFile2(styleSlotsFile(ctx), JSON.stringify(slots), "utf8");
274
+ return slots;
275
+ }
219
276
  function sessionBucketKey(panel) {
220
277
  return panel.sessionId || "__global__";
221
278
  }
222
279
  async function readLastPathState(ctx) {
223
280
  try {
224
- const raw = await readFile(stateFile(ctx), "utf8");
281
+ const raw = await readFile2(stateFile(ctx), "utf8");
225
282
  return JSON.parse(raw);
226
283
  } catch {
227
284
  return {};
228
285
  }
229
286
  }
230
287
  function resolveRecentScope(state, cwd, sessionId, spaceId) {
231
- if (path.isAbsolute(cwd)) {
288
+ if (path3.isAbsolute(cwd)) {
232
289
  if (sessionId && !spaceId) state.homePath = cwd;
233
290
  return { scope: cwd };
234
291
  }
235
- if (!cwd && !sessionId && !spaceId && path.isAbsolute(state.homePath ?? "")) {
292
+ if (!cwd && !sessionId && !spaceId && path3.isAbsolute(state.homePath ?? "")) {
236
293
  return { scope: state.homePath, fallbackCwd: state.homePath };
237
294
  }
238
295
  return {};
@@ -241,12 +298,33 @@ function addRecentPath(state, scope, sourcePath) {
241
298
  const existing = state.recentPathsByScope?.[scope] ?? [];
242
299
  state.recentPathsByScope = {
243
300
  ...state.recentPathsByScope,
244
- [scope]: [sourcePath, ...existing].filter((value, index, values) => path.isAbsolute(value) && values.indexOf(value) === index).slice(0, 50)
301
+ [scope]: [sourcePath, ...existing].filter((value, index, values) => path3.isAbsolute(value) && values.indexOf(value) === index).slice(0, 50)
245
302
  };
246
303
  }
304
+ function pathBelongsTo(root, target) {
305
+ const relative = path3.relative(root, target);
306
+ return relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative);
307
+ }
308
+ async function resolveDocumentScope(ctx, sourcePath, fallbackCwd) {
309
+ const spaces = await ctx.spaces.list().catch(() => []);
310
+ const matches = spaces.filter((space2) => path3.isAbsolute(space2.directoryPath ?? "") && pathBelongsTo(space2.directoryPath, sourcePath)).sort((a, b) => (b.directoryPath?.length ?? 0) - (a.directoryPath?.length ?? 0));
311
+ const space = matches[0];
312
+ if (space?.directoryPath) {
313
+ const spaceName = space.name || space.alias || path3.basename(space.directoryPath);
314
+ return { scope: space.directoryPath, spaceId: space.id, spaceName, scopeLabel: spaceName, scopeKind: "space" };
315
+ }
316
+ const workspaceRoot = ctx.workspace.projectPath;
317
+ if (workspaceRoot && pathBelongsTo(workspaceRoot, sourcePath)) {
318
+ return { scope: workspaceRoot, scopeKind: "workspace" };
319
+ }
320
+ if (fallbackCwd && path3.isAbsolute(fallbackCwd) && pathBelongsTo(fallbackCwd, sourcePath)) {
321
+ return { scope: fallbackCwd, scopeKind: "workspace" };
322
+ }
323
+ return { scope: path3.dirname(sourcePath), scopeKind: "external" };
324
+ }
247
325
  async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
248
326
  try {
249
- await mkdir(ctx.storagePath, { recursive: true });
327
+ await mkdir2(ctx.storagePath, { recursive: true });
250
328
  const state = await readLastPathState(ctx);
251
329
  const resolved = resolveRecentScope(state, cwd, sessionId, spaceId);
252
330
  if (resolved.scope) {
@@ -254,7 +332,7 @@ async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
254
332
  const panelPath = state.panels?.[panel.id];
255
333
  if (panelPath) addRecentPath(state, resolved.scope, panelPath);
256
334
  }
257
- await writeFile(stateFile(ctx), JSON.stringify(state), "utf8");
335
+ await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
258
336
  return resolved;
259
337
  } catch (error) {
260
338
  ctx.logger.warn(`Could not persist panel recent scope: ${String(error)}`);
@@ -263,24 +341,28 @@ async function rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId) {
263
341
  }
264
342
  async function rememberLastPath(ctx, panel, sourcePath) {
265
343
  try {
266
- await mkdir(ctx.storagePath, { recursive: true });
344
+ await mkdir2(ctx.storagePath, { recursive: true });
267
345
  const state = await readLastPathState(ctx);
268
346
  state.panels = { ...state.panels, [panel.id]: sourcePath };
269
347
  state.sessions = { ...state.sessions, [sessionBucketKey(panel)]: sourcePath };
270
- const scope = state.panelRecentScopes?.[panel.id];
348
+ let scope = state.panelRecentScopes?.[panel.id];
349
+ if (!scope && panel.view === "appView") {
350
+ scope = (await resolveDocumentScope(ctx, sourcePath)).scope;
351
+ state.panelRecentScopes = { ...state.panelRecentScopes, [panel.id]: scope };
352
+ }
271
353
  if (scope) addRecentPath(state, scope, sourcePath);
272
- await writeFile(stateFile(ctx), JSON.stringify(state), "utf8");
354
+ await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
273
355
  } catch (error) {
274
356
  ctx.logger.warn(`Could not persist last-opened path: ${String(error)}`);
275
357
  }
276
358
  }
277
359
  async function rememberRecentPath(ctx, sourcePath, panel) {
278
360
  try {
279
- await mkdir(ctx.storagePath, { recursive: true });
361
+ await mkdir2(ctx.storagePath, { recursive: true });
280
362
  const state = await readLastPathState(ctx);
281
363
  const scope = panel ? state.panelRecentScopes?.[panel.id] : resolveRecentScope(state, ctx.session.cwd ?? "", ctx.session.id ?? "", ctx.session.spaceId ?? "").scope;
282
364
  if (scope) addRecentPath(state, scope, sourcePath);
283
- await writeFile(stateFile(ctx), JSON.stringify(state), "utf8");
365
+ await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
284
366
  } catch (error) {
285
367
  ctx.logger.warn(`Could not persist recent path: ${String(error)}`);
286
368
  }
@@ -321,18 +403,18 @@ async function collectRecentDocuments(ctx, requestedCwd, sessionId, spaceId) {
321
403
  if (!resolved.scope) return { documents: [] };
322
404
  const scope = resolved.scope;
323
405
  const candidates = (state.recentPathsByScope?.[scope] ?? []).filter(
324
- (value, index, values) => typeof value === "string" && path.isAbsolute(value) && isMarkdownPath(value) && values.indexOf(value) === index
406
+ (value, index, values) => typeof value === "string" && path3.isAbsolute(value) && isMarkdownPath(value) && values.indexOf(value) === index
325
407
  );
326
408
  const documents = await Promise.all(
327
409
  candidates.map(async (filePath) => {
328
410
  try {
329
411
  const info = await stat(filePath);
330
412
  if (!info.isFile()) return void 0;
331
- const markdown = await readFile(filePath, "utf8");
332
- const fileName = path.basename(filePath);
413
+ const markdown = await readFile2(filePath, "utf8");
414
+ const fileName = path3.basename(filePath);
333
415
  return {
334
416
  path: filePath,
335
- relativePath: path.relative(scope, filePath),
417
+ relativePath: path3.relative(scope, filePath),
336
418
  fileName,
337
419
  title: deriveTitle(markdown, fileName.replace(/\.[^.]+$/, "")),
338
420
  preview: derivePreview(markdown),
@@ -348,13 +430,39 @@ async function collectRecentDocuments(ctx, requestedCwd, sessionId, spaceId) {
348
430
  fallbackCwd: resolved.fallbackCwd
349
431
  };
350
432
  }
433
+ async function collectLibraryDocuments(ctx) {
434
+ const state = await readLastPathState(ctx);
435
+ const candidates = Object.values(state.recentPathsByScope ?? {}).flat().concat(Object.values(state.panels ?? {})).filter((value, index, values) => path3.isAbsolute(value) && isMarkdownPath(value) && values.indexOf(value) === index);
436
+ const documents = await Promise.all(candidates.map(async (filePath) => {
437
+ try {
438
+ const [info, markdown, scope] = await Promise.all([stat(filePath), readFile2(filePath, "utf8"), resolveDocumentScope(ctx, filePath, state.homePath)]);
439
+ if (!info.isFile()) return void 0;
440
+ const fileName = path3.basename(filePath);
441
+ return {
442
+ path: filePath,
443
+ relativePath: path3.relative(scope.scope, filePath),
444
+ fileName,
445
+ title: deriveTitle(markdown, fileName.replace(/\.[^.]+$/, "")),
446
+ preview: derivePreview(markdown),
447
+ modifiedAt: info.mtimeMs,
448
+ spaceId: scope.spaceId,
449
+ spaceName: scope.spaceName,
450
+ scopeLabel: scope.scopeLabel,
451
+ scopeKind: scope.scopeKind
452
+ };
453
+ } catch {
454
+ return void 0;
455
+ }
456
+ }));
457
+ return documents.filter((entry) => Boolean(entry)).sort((a, b) => b.modifiedAt - a.modifiedAt).slice(0, RECENT_LIMIT);
458
+ }
351
459
  var livePanelDocuments = /* @__PURE__ */ new Map();
352
460
  async function sendDocument(panel, state) {
353
461
  livePanelDocuments.set(panel.id, state);
354
462
  await panel.postMessage({ type: "document", ...state });
355
463
  }
356
464
  async function sendLiveDocument(ctx, panel, liveDocument) {
357
- if (liveDocument.path && path.isAbsolute(liveDocument.path)) {
465
+ if (liveDocument.path && path3.isAbsolute(liveDocument.path)) {
358
466
  try {
359
467
  const { markdown, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, liveDocument.path);
360
468
  await sendDocument(panel, { ...liveDocument, markdown, title: documentTitle(markdown, liveDocument.path), draftRestored, draftConflict, diskMarkdown });
@@ -369,10 +477,10 @@ function payloadPath(panel) {
369
477
  const payload = panel.payload;
370
478
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return void 0;
371
479
  const value = payload.path;
372
- return typeof value === "string" && path.isAbsolute(value) ? value : void 0;
480
+ return typeof value === "string" && path3.isAbsolute(value) ? value : void 0;
373
481
  }
374
482
  async function restoreDocument(ctx, panel) {
375
- if (!panel.sessionId) return false;
483
+ if (!panel.sessionId && panel.view !== "appView") return false;
376
484
  const sourcePath = await readLastPath(ctx, panel) ?? payloadPath(panel);
377
485
  if (!sourcePath) return false;
378
486
  try {
@@ -386,49 +494,6 @@ async function restoreDocument(ctx, panel) {
386
494
  return false;
387
495
  }
388
496
  }
389
- var BMMD_BIN_PATH = fileURLToPath(new URL("./bmmd/bin/bmmd.mjs", import.meta.url));
390
- async function runBmmd(args, input) {
391
- const binPath = BMMD_BIN_PATH;
392
- return new Promise((resolve, reject) => {
393
- const child = spawn(process.execPath, [binPath, ...args], { stdio: ["pipe", "pipe", "pipe"] });
394
- let output = "";
395
- let errors = "";
396
- child.stdout.setEncoding("utf8");
397
- child.stderr.setEncoding("utf8");
398
- child.stdout.on("data", (chunk) => {
399
- output += chunk;
400
- });
401
- child.stderr.on("data", (chunk) => {
402
- errors += chunk;
403
- });
404
- child.on("error", reject);
405
- child.on("close", (code) => {
406
- if (code === 0) resolve(output);
407
- else reject(new Error(errors.trim() || `bmmd exited with code ${code}`));
408
- });
409
- child.stdin.end(input, "utf8");
410
- });
411
- }
412
- var FINCH_FILE_IMAGE_RE = /finch-file:\/\/local\?path=[^\s)"']+/g;
413
- var FINCH_IMAGE_PLACEHOLDER_ORIGIN = "https://finch-local.invalid/markdown-image/";
414
- function substituteFinchFileImagesForBm(markdown) {
415
- const urls = /* @__PURE__ */ new Map();
416
- let sequence = 0;
417
- const substituted = markdown.replace(FINCH_FILE_IMAGE_RE, (originalUrl) => {
418
- const placeholder = `${FINCH_IMAGE_PLACEHOLDER_ORIGIN}${createHash("sha256").update(`${originalUrl}:${sequence++}`).digest("hex")}`;
419
- urls.set(placeholder, originalUrl);
420
- return placeholder;
421
- });
422
- return { markdown: substituted, urls };
423
- }
424
- async function renderWithBm(markdown, markdownStyle, customCss) {
425
- const args = ["render", "--platform", "wechat", "--markdown-style", markdownStyle || "kami"];
426
- if (customCss && customCss.trim()) args.push("--custom-css", customCss);
427
- const prepared = substituteFinchFileImagesForBm(markdown);
428
- let html = await runBmmd(args, prepared.markdown);
429
- for (const [placeholder, originalUrl] of prepared.urls) html = html.split(placeholder).join(originalUrl);
430
- return html;
431
- }
432
497
  var panelWatchers = /* @__PURE__ */ new Map();
433
498
  var lastPanel;
434
499
  function stopWatching(panelId) {
@@ -447,7 +512,7 @@ function watchSource(ctx, panel, sourcePath) {
447
512
  if (entry.timer) clearTimeout(entry.timer);
448
513
  entry.timer = setTimeout(async () => {
449
514
  try {
450
- const markdown = await readFile(sourcePath, "utf8");
515
+ const markdown = await readFile2(sourcePath, "utf8");
451
516
  await sendDocument(panel, { path: sourcePath, markdown, title: documentTitle(markdown, sourcePath) });
452
517
  } catch (error) {
453
518
  ctx.logger.warn(`Source refresh failed: ${String(error)}`);
@@ -496,16 +561,98 @@ async function revealInFileManager(ctx, targetPath) {
496
561
  const info = await stat(targetPath).catch(() => void 0);
497
562
  const isFile = info?.isFile() ?? false;
498
563
  if (process.platform === "darwin") {
499
- spawn("open", isFile ? ["-R", targetPath] : [targetPath], { stdio: "ignore", detached: true }).unref();
564
+ spawn2("open", isFile ? ["-R", targetPath] : [targetPath], { stdio: "ignore", detached: true }).unref();
500
565
  } else if (process.platform === "win32") {
501
- spawn("explorer", isFile ? [`/select,${targetPath}`] : [targetPath], { stdio: "ignore", detached: true }).unref();
566
+ spawn2("explorer", isFile ? [`/select,${targetPath}`] : [targetPath], { stdio: "ignore", detached: true }).unref();
502
567
  } else {
503
- spawn("xdg-open", [isFile ? path.dirname(targetPath) : targetPath], { stdio: "ignore", detached: true }).unref();
568
+ spawn2("xdg-open", [isFile ? path3.dirname(targetPath) : targetPath], { stdio: "ignore", detached: true }).unref();
504
569
  }
505
570
  } catch (error) {
506
571
  ctx.logger.warn(`Could not open file manager for ${targetPath}: ${String(error)}`);
507
572
  }
508
573
  }
574
+ async function readRewriteSession(ctx, sourcePath) {
575
+ const state = await readLastPathState(ctx);
576
+ const id = state.rewriteSessions?.[sourcePath];
577
+ if (!id) return void 0;
578
+ const session = await ctx.sessions.get(id).catch(() => void 0);
579
+ return session ? id : void 0;
580
+ }
581
+ async function rememberRewriteSession(ctx, sourcePath, sessionId) {
582
+ try {
583
+ const state = await readLastPathState(ctx);
584
+ state.rewriteSessions = { ...state.rewriteSessions, [sourcePath]: sessionId };
585
+ await writeFile2(stateFile(ctx), JSON.stringify(state), "utf8");
586
+ } catch (error) {
587
+ ctx.logger.warn(`Could not persist rewrite session: ${String(error)}`);
588
+ }
589
+ }
590
+ async function startRewriteSession(ctx, panel, message) {
591
+ const sourcePath = String(message.path ?? "").trim();
592
+ const selectedText = String(message.selectedText ?? "").trim();
593
+ const rewriteMode = message.rewriteMode === "continue" ? "continue" : "replace";
594
+ const requirement = String(message.requirement ?? "").trim() || (rewriteMode === "continue" ? "\u81EA\u7136\u5730\u627F\u63A5\u4E0A\u4E0B\u6587\u7EE7\u7EED\u5199\u4F5C" : "\u8BA9\u8868\u8FBE\u66F4\u6E05\u6670\u3001\u81EA\u7136\uFF0C\u5E76\u4FDD\u6301\u539F\u610F");
595
+ const hasTarget = rewriteMode === "continue" ? !!message.startLine : !!selectedText;
596
+ if (panel.view !== "appView" || !path3.isAbsolute(sourcePath) || !hasTarget) {
597
+ await panel.postMessage({ type: "rewriteSessionFailed", message: "\u6539\u5199\u9700\u8981 App View \u4E2D\u5DF2\u4FDD\u5B58\u7684\u672C\u5730\u6587\u6863\u548C\u9009\u4E2D\u6587\u672C\u6216\u7EED\u5199\u4F4D\u7F6E\u3002" });
598
+ return;
599
+ }
600
+ const scope = await resolveDocumentScope(ctx, sourcePath);
601
+ let sessionId = await readRewriteSession(ctx, sourcePath);
602
+ if (!sessionId) {
603
+ const session = await ctx.sessions.create({
604
+ ...scope.spaceId ? { space: { spaceId: scope.spaceId } } : {},
605
+ title: `\u6539\u5199\uFF1A${path3.basename(sourcePath)}`,
606
+ activity: "interactive",
607
+ permissionMode: "acceptCalls"
608
+ });
609
+ sessionId = session.sessionId;
610
+ await rememberRewriteSession(ctx, sourcePath, sessionId);
611
+ }
612
+ const lineText = message.startLine ? `\u4F4D\u7F6E\uFF1A\u7B2C ${message.startLine}${message.endLine && message.endLine !== message.startLine ? `\u2013${message.endLine}` : ""} \u884C\u3002` : "";
613
+ const prompt = rewriteMode === "continue" ? `\u8BF7\u5728\u4E0B\u9762\u8FD9\u4EFD Markdown \u6587\u4EF6\u7684\u6307\u5B9A\u4F4D\u7F6E\u7EED\u5199\u5185\u5BB9\uFF0C\u5E76\u628A\u7ED3\u679C\u5199\u56DE\u6587\u4EF6\u3002
614
+
615
+ \u6587\u4EF6\uFF1A${sourcePath}
616
+ ${lineText}
617
+ \u8981\u6C42\uFF1A${requirement}
618
+
619
+ \u8BF7\u8BFB\u53D6\u6587\u4EF6\u5F53\u524D\u5185\u5BB9\uFF1A\u7B2C ${message.startLine} \u884C\u5F53\u524D\u662F\u4E00\u4E2A\u7A7A\u884C\uFF0C\u8BF7\u628A\u7EED\u5199\u7684\u65B0\u5185\u5BB9\u76F4\u63A5\u5199\u5165\u8FD9\u4E00\u884C\u672C\u8EAB\uFF08\u628A\u8FD9\u4E2A\u7A7A\u884C\u66FF\u6362\u6210\u65B0\u5185\u5BB9\uFF09\uFF0C\u4E0D\u8981\u5728\u5B83\u524D\u540E\u989D\u5916\u63D2\u5165\u65B0\u7684\u7A7A\u884C\uFF0C\u4E5F\u4E0D\u8981\u6539\u52A8\u7B2C ${message.startLine} \u884C\u4E4B\u5916\u7684\u539F\u6709\u5185\u5BB9\uFF1B\u8C03\u7528 markdown_editor_document \u7684 apply\uFF0C\u4EE5 edits \u505A\u7CBE\u786E\u7684\u5C40\u90E8\u66FF\u6362\u3002\u4E0D\u8981\u53EA\u7ED9\u5EFA\u8BAE\uFF0C\u4E0D\u8981\u91CD\u53D1\u5168\u6587\uFF1B\u5B8C\u6210\u5199\u56DE\u540E\u7B80\u77ED\u8BF4\u660E\u3002` : `\u8BF7\u76F4\u63A5\u6539\u5199\u4E0B\u9762\u8FD9\u6BB5 Markdown\uFF0C\u5E76\u628A\u7ED3\u679C\u5199\u56DE\u6587\u4EF6\u3002
620
+
621
+ \u6587\u4EF6\uFF1A${sourcePath}
622
+ ${lineText}
623
+ \u8981\u6C42\uFF1A${requirement}
624
+
625
+ \u539F\u6587\uFF1A
626
+ ${selectedText}
627
+
628
+ \u8BF7\u8BFB\u53D6\u6587\u4EF6\u5F53\u524D\u5185\u5BB9\uFF0C\u8C03\u7528 markdown_editor_document \u7684 apply\uFF0C\u4EE5 edits \u505A\u552F\u4E00\u3001\u7CBE\u786E\u7684\u5C40\u90E8\u66FF\u6362\u3002\u4E0D\u8981\u53EA\u7ED9\u5EFA\u8BAE\uFF0C\u4E0D\u8981\u91CD\u53D1\u5168\u6587\uFF1B\u5B8C\u6210\u5199\u56DE\u540E\u7B80\u77ED\u8BF4\u660E\u3002`;
629
+ const receipt = await ctx.sessions.send(sessionId, {
630
+ text: prompt,
631
+ idempotencyKey: `rewrite-${createHash3("sha256").update(`${sourcePath}:${rewriteMode}:${selectedText}:${requirement}:${message.startLine ?? ""}:${Date.now()}`).digest("hex")}`
632
+ });
633
+ if (receipt.state === "rejected") {
634
+ await panel.postMessage({ type: "rewriteSessionFailed", message: "\u6539\u5199\u4F1A\u8BDD\u961F\u5217\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" });
635
+ return;
636
+ }
637
+ await panel.postMessage({
638
+ type: "rewriteSessionStarted",
639
+ sessionId,
640
+ spaceName: scope.spaceName,
641
+ title: `${rewriteMode === "continue" ? "\u7EED\u5199" : "\u6539\u5199"}\uFF1A${path3.basename(sourcePath)}`,
642
+ startLine: message.startLine,
643
+ endLine: message.endLine ?? message.startLine,
644
+ rewriteMode
645
+ });
646
+ void ctx.sessions.waitForTurn(sessionId, receipt.turnId, { timeoutMs: 6e5 }).then(async (result2) => {
647
+ const verb = rewriteMode === "continue" ? "\u7EED\u5199" : "\u6539\u5199";
648
+ await panel.postMessage({
649
+ type: result2.state === "completed" ? "rewriteSessionFinished" : "rewriteSessionFailed",
650
+ sessionId,
651
+ message: result2.state === "completed" ? `${verb}\u5DF2\u5B8C\u6210\u3002` : result2.state === "timeout" ? `${verb}\u4ECD\u5728\u4F1A\u8BDD\u4E2D\u7EE7\u7EED\u3002` : `${verb}\u4F1A\u8BDD\u672A\u5B8C\u6210\u3002`
652
+ }).catch(() => {
653
+ });
654
+ });
655
+ }
509
656
  async function handleMessage(ctx, panel, raw) {
510
657
  const message = raw;
511
658
  switch (message.type) {
@@ -555,7 +702,8 @@ async function handleMessage(ctx, panel, raw) {
555
702
  try {
556
703
  const handle = ctx.ui.pickFile({
557
704
  title: "\u9009\u62E9Markdown\u6587\u4EF6",
558
- filter: { extensions: [".md", ".markdown"] }
705
+ filter: { extensions: [".md", ".markdown"] },
706
+ allowSpaceSwitch: panel.view === "appView"
559
707
  });
560
708
  ctx.logger.info("ctx.ui.pickFile() call returned a handle, awaiting resolution\u2026");
561
709
  const picked = await handle;
@@ -581,7 +729,7 @@ async function handleMessage(ctx, panel, raw) {
581
729
  }
582
730
  case "loadPath": {
583
731
  const sourcePath = String(message.path ?? "").trim();
584
- if (!path.isAbsolute(sourcePath)) {
732
+ if (!path3.isAbsolute(sourcePath)) {
585
733
  await panel.postMessage({ type: "error", message: "Please provide an absolute Markdown path." });
586
734
  return;
587
735
  }
@@ -597,7 +745,7 @@ async function handleMessage(ctx, panel, raw) {
597
745
  }
598
746
  case "watchPath": {
599
747
  const sourcePath = String(message.path ?? "").trim();
600
- if (!path.isAbsolute(sourcePath)) return;
748
+ if (!path3.isAbsolute(sourcePath)) return;
601
749
  watchSource(ctx, panel, sourcePath);
602
750
  await rememberLastPath(ctx, panel, sourcePath);
603
751
  await panel.postMessage({ type: "watchStarted", path: sourcePath });
@@ -614,7 +762,7 @@ async function handleMessage(ctx, panel, raw) {
614
762
  }
615
763
  case "openPath": {
616
764
  const targetPath = String(message.path ?? "").trim();
617
- if (targetPath && path.isAbsolute(targetPath)) await revealInFileManager(ctx, targetPath);
765
+ if (targetPath && path3.isAbsolute(targetPath)) await revealInFileManager(ctx, targetPath);
618
766
  return;
619
767
  }
620
768
  case "goHome": {
@@ -627,20 +775,29 @@ async function handleMessage(ctx, panel, raw) {
627
775
  const sessionId = String(message.sessionId ?? "").trim();
628
776
  const spaceId = String(message.spaceId ?? "").trim();
629
777
  try {
630
- await rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId);
631
- const recent = await collectRecentDocuments(ctx, cwd, sessionId, spaceId);
632
- await panel.postMessage({ type: "recentDocuments", cwd, documents: recent.documents, fallbackCwd: recent.fallbackCwd });
778
+ if (panel.view === "appView") {
779
+ const documents = await collectLibraryDocuments(ctx);
780
+ await panel.postMessage({ type: "recentDocuments", cwd, documents, library: true });
781
+ } else {
782
+ await rememberPanelRecentScope(ctx, panel, cwd, sessionId, spaceId);
783
+ const recent = await collectRecentDocuments(ctx, cwd, sessionId, spaceId);
784
+ await panel.postMessage({ type: "recentDocuments", cwd, documents: recent.documents, fallbackCwd: recent.fallbackCwd });
785
+ }
633
786
  } catch (error) {
634
787
  ctx.logger.warn(`Could not collect recent documents: ${String(error)}`);
635
- await panel.postMessage({ type: "recentDocuments", cwd, documents: [] });
788
+ await panel.postMessage({ type: "recentDocuments", cwd, documents: [], library: panel.view === "appView" });
636
789
  }
637
790
  return;
638
791
  }
792
+ case "requestRewrite": {
793
+ await startRewriteSession(ctx, panel, message);
794
+ return;
795
+ }
639
796
  case "saveMarkdown": {
640
797
  const sourcePath = String(message.path ?? "").trim();
641
- if (!path.isAbsolute(sourcePath)) return;
798
+ if (!path3.isAbsolute(sourcePath)) return;
642
799
  try {
643
- await writeFile(sourcePath, String(message.markdown ?? ""), "utf8");
800
+ await writeFile2(sourcePath, String(message.markdown ?? ""), "utf8");
644
801
  cancelPendingDraftWrite(panel.id);
645
802
  await deleteDraft(ctx, sourcePath);
646
803
  await panel.postMessage({ type: "savedMarkdown", path: sourcePath, requestId: message.requestId });
@@ -651,14 +808,14 @@ async function handleMessage(ctx, panel, raw) {
651
808
  }
652
809
  case "saveDraft": {
653
810
  const sourcePath = String(message.path ?? "").trim();
654
- if (!path.isAbsolute(sourcePath)) return;
811
+ if (!path3.isAbsolute(sourcePath)) return;
655
812
  const base = typeof message.base === "string" ? message.base : void 0;
656
813
  scheduleDraftWrite(ctx, panel.id, sourcePath, String(message.markdown ?? ""), base);
657
814
  return;
658
815
  }
659
816
  case "discardDraft": {
660
817
  const sourcePath = String(message.path ?? "").trim();
661
- if (!path.isAbsolute(sourcePath)) return;
818
+ if (!path3.isAbsolute(sourcePath)) return;
662
819
  cancelPendingDraftWrite(panel.id);
663
820
  await deleteDraft(ctx, sourcePath);
664
821
  return;
@@ -694,14 +851,14 @@ async function handleMessage(ctx, panel, raw) {
694
851
  case "applyReplacement": {
695
852
  const sourcePath = String(message.path ?? "").trim();
696
853
  const markdown = String(message.markdown ?? "");
697
- if (!sourcePath || !path.isAbsolute(sourcePath)) {
854
+ if (!sourcePath || !path3.isAbsolute(sourcePath)) {
698
855
  await panel.postMessage({ type: "error", message: "Pasted documents can be revised in the editor, but source-file apply needs an absolute path." });
699
856
  return;
700
857
  }
701
858
  try {
702
- const current = await readFile(sourcePath, "utf8");
859
+ const current = await readFile2(sourcePath, "utf8");
703
860
  const appliedMarkdown = preserveTextEnvelope(current, markdown);
704
- await writeFile(sourcePath, appliedMarkdown, "utf8");
861
+ await writeFile2(sourcePath, appliedMarkdown, "utf8");
705
862
  await rememberRecentPath(ctx, sourcePath, panel);
706
863
  await panel.postMessage({ type: "applied", path: sourcePath, title: documentTitle(appliedMarkdown, sourcePath) });
707
864
  } catch (error) {
@@ -727,12 +884,12 @@ async function handleMessage(ctx, panel, raw) {
727
884
  try {
728
885
  const ext = PASTE_IMAGE_EXT[String(message.mimeType ?? "").toLowerCase()] ?? "png";
729
886
  const buffer = Buffer.from(dataBase64, "base64");
730
- const digest = createHash("sha256").update(buffer).digest("hex").slice(0, 20);
731
- const dir = path.join(ctx.storagePath, "assets");
732
- await mkdir(dir, { recursive: true });
733
- const targetPath = path.join(dir, `${digest}.${ext}`);
887
+ const digest = createHash3("sha256").update(buffer).digest("hex").slice(0, 20);
888
+ const dir = path3.join(ctx.storagePath, "assets");
889
+ await mkdir2(dir, { recursive: true });
890
+ const targetPath = path3.join(dir, `${digest}.${ext}`);
734
891
  const alreadyExists = await stat(targetPath).then(() => true).catch(() => false);
735
- if (!alreadyExists) await writeFile(targetPath, buffer);
892
+ if (!alreadyExists) await writeFile2(targetPath, buffer);
736
893
  const url = `finch-file://local?path=${encodeURIComponent(targetPath)}`;
737
894
  await panel.postMessage({ type: "pastedImage", requestId: message.requestId, url, path: targetPath });
738
895
  } catch (error) {
@@ -765,15 +922,15 @@ async function handleMessage(ctx, panel, raw) {
765
922
  }
766
923
  try {
767
924
  const sourcePath = String(message.path ?? "").trim();
768
- let dir = path.isAbsolute(sourcePath) ? path.dirname(sourcePath) : "";
925
+ let dir = path3.isAbsolute(sourcePath) ? path3.dirname(sourcePath) : "";
769
926
  if (!dir) {
770
- const downloads = path.join(os.homedir(), "Downloads");
927
+ const downloads = path3.join(os.homedir(), "Downloads");
771
928
  dir = await stat(downloads).then((s) => s.isDirectory()).catch(() => false) ? downloads : ctx.storagePath;
772
929
  }
773
930
  const rawName = String(message.fileName ?? "").trim() || documentTitle(String(message.markdown ?? ""), sourcePath || void 0);
774
931
  const safeName = rawName.replace(/[\\/:*?"<>|]/g, " ").trim().slice(0, 120) || "article";
775
- const targetPath = path.join(dir, `${safeName}.${ext}`);
776
- await writeFile(targetPath, Buffer.from(dataBase64, "base64"));
932
+ const targetPath = path3.join(dir, `${safeName}.${ext}`);
933
+ await writeFile2(targetPath, Buffer.from(dataBase64, "base64"));
777
934
  await panel.postMessage({ type: "exported", path: targetPath, requestId: message.requestId });
778
935
  } catch (error) {
779
936
  await panel.postMessage({ type: "error", message: `Could not export file: ${error instanceof Error ? error.message : String(error)}` });
@@ -873,7 +1030,7 @@ action:
873
1030
  const action = String(input.action ?? "");
874
1031
  if (action === "open" || action === "create" || action === "apply") {
875
1032
  const sourcePath = String(input.path ?? "").trim();
876
- if (!path.isAbsolute(sourcePath)) return result("`path` must be an absolute local path.", true);
1033
+ if (!path3.isAbsolute(sourcePath)) return result("`path` must be an absolute local path.", true);
877
1034
  if (action === "open") {
878
1035
  try {
879
1036
  const { markdown: markdown2, diskMarkdown, draftRestored, draftConflict } = await readFileWithDraft(ctx, sourcePath);
@@ -882,7 +1039,7 @@ action:
882
1039
  watchSource(ctx, panel, sourcePath);
883
1040
  await rememberLastPath(ctx, panel, sourcePath);
884
1041
  await sendDocument(panel, { path: sourcePath, markdown: markdown2, title: documentTitle(markdown2, sourcePath), draftRestored, draftConflict, diskMarkdown });
885
- return result(`Opened Markdown Editor for ${path.basename(sourcePath)}.`);
1042
+ return result(`Opened Markdown Editor for ${path3.basename(sourcePath)}.`);
886
1043
  } catch (error) {
887
1044
  return result(`Could not read ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`, true);
888
1045
  }
@@ -892,14 +1049,14 @@ action:
892
1049
  try {
893
1050
  const alreadyExists = await stat(sourcePath).then(() => true).catch(() => false);
894
1051
  if (alreadyExists) return result(`${sourcePath} already exists. Use action 'open' to view it or 'apply' to revise it instead.`, true);
895
- await mkdir(path.dirname(sourcePath), { recursive: true });
896
- await writeFile(sourcePath, markdown2, "utf8");
1052
+ await mkdir2(path3.dirname(sourcePath), { recursive: true });
1053
+ await writeFile2(sourcePath, markdown2, "utf8");
897
1054
  const panel = ctx.ui.createPanel({ instanceMode: "single", payload: { path: sourcePath } });
898
1055
  await panel.reveal();
899
1056
  watchSource(ctx, panel, sourcePath);
900
1057
  await rememberLastPath(ctx, panel, sourcePath);
901
1058
  await sendDocument(panel, { path: sourcePath, markdown: markdown2, title: documentTitle(markdown2, sourcePath) });
902
- return result(`Created ${path.basename(sourcePath)} and opened it in Markdown Editor.`);
1059
+ return result(`Created ${path3.basename(sourcePath)} and opened it in Markdown Editor.`);
903
1060
  } catch (error) {
904
1061
  return result(`Could not create ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`, true);
905
1062
  }
@@ -915,12 +1072,12 @@ action:
915
1072
  };
916
1073
  });
917
1074
  try {
918
- const current = await readFile(sourcePath, "utf8");
1075
+ const current = await readFile2(sourcePath, "utf8");
919
1076
  const applied = applyEditSpecs(current, edits);
920
1077
  if (!applied.ok) return result(applied.error, true);
921
- await writeFile(sourcePath, applied.content, "utf8");
1078
+ await writeFile2(sourcePath, applied.content, "utf8");
922
1079
  await rememberRecentPath(ctx, sourcePath);
923
- return result(`Applied ${edits.length} targeted edit${edits.length > 1 ? "s" : ""} to ${path.basename(sourcePath)}.`);
1080
+ return result(`Applied ${edits.length} targeted edit${edits.length > 1 ? "s" : ""} to ${path3.basename(sourcePath)}.`);
924
1081
  } catch (error) {
925
1082
  return result(`Could not apply edits: ${error instanceof Error ? error.message : String(error)}`, true);
926
1083
  }
@@ -928,10 +1085,10 @@ action:
928
1085
  const markdown = String(input.markdown ?? "");
929
1086
  if (!markdown) return result("`apply` requires either `edits` (targeted replacements) or non-empty `markdown` (full document).", true);
930
1087
  try {
931
- const current = await readFile(sourcePath, "utf8");
932
- await writeFile(sourcePath, preserveTextEnvelope(current, markdown), "utf8");
1088
+ const current = await readFile2(sourcePath, "utf8");
1089
+ await writeFile2(sourcePath, preserveTextEnvelope(current, markdown), "utf8");
933
1090
  await rememberRecentPath(ctx, sourcePath);
934
- return result(`Applied reviewed Markdown to ${path.basename(sourcePath)}.`);
1091
+ return result(`Applied reviewed Markdown to ${path3.basename(sourcePath)}.`);
935
1092
  } catch (error) {
936
1093
  return result(`Could not apply revision: ${error instanceof Error ? error.message : String(error)}`, true);
937
1094
  }