apple-notes-mcp 2.12.0 → 2.13.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/README.md +15 -0
- package/build/index.js +730 -0
- package/docs/markdown-templates.md +83 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1950,6 +1950,21 @@ be deleted.
|
|
|
1950
1950
|
|
|
1951
1951
|
**Returns:** `name`, `path`, and `deleted: true`.
|
|
1952
1952
|
|
|
1953
|
+
#### Template editor (command line)
|
|
1954
|
+
|
|
1955
|
+
`apple-notes-mcp templates edit [name]` starts a local web editor for
|
|
1956
|
+
templates: edit the JSON on one side and see it validated and rendered
|
|
1957
|
+
against built-in sample notes on the other, then save it to the library
|
|
1958
|
+
(create-only unless you tick "replace"). It prints one address with a
|
|
1959
|
+
per-run token, listens on `127.0.0.1` only, refuses cross-origin requests,
|
|
1960
|
+
and stops on Ctrl-C or after 30 idle minutes (`--idle-minutes`). It reads a
|
|
1961
|
+
real note only when you pass `--note <id>`, and then read-only. `--tailnet`
|
|
1962
|
+
listens on this Mac's Tailscale address instead, so another device on your
|
|
1963
|
+
tailnet can open it; it never changes Tailscale or firewall settings. With
|
|
1964
|
+
`--tailnet`, `--note` exposes the full note body to anyone who has the URL. See
|
|
1965
|
+
[docs/markdown-templates.md](docs/markdown-templates.md#editing-in-a-browser)
|
|
1966
|
+
for options and exactly what it exposes.
|
|
1967
|
+
|
|
1953
1968
|
---
|
|
1954
1969
|
|
|
1955
1970
|
#### `get-checklist-state`
|
package/build/index.js
CHANGED
|
@@ -60219,6 +60219,733 @@ function registerPrivateHelperTools(server2, manager, depsFactory = () => defaul
|
|
|
60219
60219
|
);
|
|
60220
60220
|
}
|
|
60221
60221
|
|
|
60222
|
+
// src/utils/localServer.ts
|
|
60223
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
60224
|
+
import { networkInterfaces } from "node:os";
|
|
60225
|
+
function isTailnetIPv4(address) {
|
|
60226
|
+
const match = /^100\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(address);
|
|
60227
|
+
if (!match) return false;
|
|
60228
|
+
const [second, third, fourth] = match.slice(1).map(Number);
|
|
60229
|
+
return second >= 64 && second <= 127 && third <= 255 && fourth <= 255;
|
|
60230
|
+
}
|
|
60231
|
+
function findTailnetAddress(interfaces = networkInterfaces()) {
|
|
60232
|
+
const candidates = [];
|
|
60233
|
+
for (const [name, infos] of Object.entries(interfaces)) {
|
|
60234
|
+
for (const info of infos ?? []) {
|
|
60235
|
+
if (info.family === "IPv4" && !info.internal && isTailnetIPv4(info.address))
|
|
60236
|
+
candidates.push({ address: info.address, interface: name });
|
|
60237
|
+
}
|
|
60238
|
+
}
|
|
60239
|
+
return candidates.find((c) => c.interface.startsWith("utun")) ?? candidates[0];
|
|
60240
|
+
}
|
|
60241
|
+
function newServerToken() {
|
|
60242
|
+
return randomBytes3(32).toString("hex");
|
|
60243
|
+
}
|
|
60244
|
+
function bearerToken(req) {
|
|
60245
|
+
const auth = req.headers.authorization;
|
|
60246
|
+
return auth?.startsWith("Bearer ") ? auth.slice(7) : void 0;
|
|
60247
|
+
}
|
|
60248
|
+
function tokenMatches(given, token) {
|
|
60249
|
+
if (!given) return false;
|
|
60250
|
+
const a = Buffer.from(given, "utf8");
|
|
60251
|
+
const b = Buffer.from(token, "utf8");
|
|
60252
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
60253
|
+
}
|
|
60254
|
+
function hostAuthority(host, port) {
|
|
60255
|
+
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
60256
|
+
}
|
|
60257
|
+
function crossOriginRefusal(req, origin, requireOrigin = false) {
|
|
60258
|
+
const site = req.headers["sec-fetch-site"];
|
|
60259
|
+
if (site && site !== "same-origin" && site !== "none") return "cross-site";
|
|
60260
|
+
const requestOrigin = req.headers.origin;
|
|
60261
|
+
if (requestOrigin !== void 0 && requestOrigin !== origin) return "foreign-origin";
|
|
60262
|
+
if (requireOrigin && requestOrigin !== origin) return "missing-origin";
|
|
60263
|
+
return void 0;
|
|
60264
|
+
}
|
|
60265
|
+
|
|
60266
|
+
// src/services/templateEditor.ts
|
|
60267
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
60268
|
+
import { createServer } from "node:http";
|
|
60269
|
+
|
|
60270
|
+
// src/utils/templateSamples.ts
|
|
60271
|
+
function sampleBlock(text2, style = "body", extra = {}, runs = [{ text: text2 }]) {
|
|
60272
|
+
return {
|
|
60273
|
+
index: 0,
|
|
60274
|
+
start: 0,
|
|
60275
|
+
length: runs.reduce((sum, run) => sum + run.text.length, 0),
|
|
60276
|
+
text: runs.map((run) => run.text).join(""),
|
|
60277
|
+
style,
|
|
60278
|
+
styleType: null,
|
|
60279
|
+
indent: 0,
|
|
60280
|
+
alignment: "left",
|
|
60281
|
+
blockQuote: false,
|
|
60282
|
+
...extra,
|
|
60283
|
+
runs: runs.map((run) => ({ start: 0, length: run.text.length, ...run })),
|
|
60284
|
+
attachments: []
|
|
60285
|
+
};
|
|
60286
|
+
}
|
|
60287
|
+
var sampleAttachmentRun = (id2, uti) => ({
|
|
60288
|
+
text: "\uFFFC",
|
|
60289
|
+
attachment: { id: id2, uti }
|
|
60290
|
+
});
|
|
60291
|
+
function sampleAttachment(id2, uti, extra = {}) {
|
|
60292
|
+
return { id: id2, pk: 0, uti, kind: classifyUti(uti), children: [], ...extra };
|
|
60293
|
+
}
|
|
60294
|
+
function sampleNote(pk, blocks, attachments = []) {
|
|
60295
|
+
let offset = 0;
|
|
60296
|
+
const indexed = blocks.map((b, index) => {
|
|
60297
|
+
const start = offset;
|
|
60298
|
+
offset += b.length + 1;
|
|
60299
|
+
let at = start;
|
|
60300
|
+
const runs = b.runs.map((run) => {
|
|
60301
|
+
const shifted = { ...run, start: at };
|
|
60302
|
+
at += run.length;
|
|
60303
|
+
return shifted;
|
|
60304
|
+
});
|
|
60305
|
+
const markers2 = runs.flatMap(
|
|
60306
|
+
(run) => run.attachment ? [{ ...run.attachment, start: run.start, blockIndex: index }] : []
|
|
60307
|
+
);
|
|
60308
|
+
return { ...b, index, start, runs, attachments: markers2 };
|
|
60309
|
+
});
|
|
60310
|
+
const markers = indexed.flatMap((b) => b.attachments);
|
|
60311
|
+
return {
|
|
60312
|
+
id: `x-coredata://SAMPLE/ICNote/p${pk}`,
|
|
60313
|
+
title: indexed[0]?.text ?? "",
|
|
60314
|
+
doc: {
|
|
60315
|
+
text: indexed.map((b) => b.text).join("\n"),
|
|
60316
|
+
textLength: offset,
|
|
60317
|
+
blocks: indexed,
|
|
60318
|
+
attachments: markers,
|
|
60319
|
+
undecodedFields: { attributeRun: {}, paragraphStyle: {} },
|
|
60320
|
+
summary: summarize(indexed, markers)
|
|
60321
|
+
},
|
|
60322
|
+
attachments: new Map(attachments.map((a) => [a.id, a])),
|
|
60323
|
+
ordered: attachments.map((a, i) => ({ ...a, pk: i + 1 }))
|
|
60324
|
+
};
|
|
60325
|
+
}
|
|
60326
|
+
function sampleMeta(uuid2) {
|
|
60327
|
+
return {
|
|
60328
|
+
uuid: uuid2,
|
|
60329
|
+
created: "2026-01-05T14:30:00.000Z",
|
|
60330
|
+
modified: "2026-02-10T09:15:00.000Z",
|
|
60331
|
+
folder: "Sample Folder",
|
|
60332
|
+
account: "iCloud"
|
|
60333
|
+
};
|
|
60334
|
+
}
|
|
60335
|
+
function templateSamples() {
|
|
60336
|
+
return [
|
|
60337
|
+
{
|
|
60338
|
+
id: "structure",
|
|
60339
|
+
label: "Headings, lists and checklists",
|
|
60340
|
+
meta: sampleMeta("00000000-0000-4000-8000-000000000001"),
|
|
60341
|
+
note: sampleNote(1, [
|
|
60342
|
+
sampleBlock("Weekly Plan", "title"),
|
|
60343
|
+
sampleBlock("Goals", "heading"),
|
|
60344
|
+
sampleBlock("Details", "subheading"),
|
|
60345
|
+
sampleBlock("A plain paragraph with *characters* that need escaping."),
|
|
60346
|
+
sampleBlock("First step", "numbered"),
|
|
60347
|
+
sampleBlock("Second step", "numbered"),
|
|
60348
|
+
sampleBlock("Nested step", "numbered", { indent: 1 }),
|
|
60349
|
+
sampleBlock("A bullet", "bulleted"),
|
|
60350
|
+
sampleBlock("A dash", "dashed"),
|
|
60351
|
+
sampleBlock("Nested bullet", "bulleted", { indent: 1 }),
|
|
60352
|
+
sampleBlock("Done task", "checklist", { checklist: { id: "sample-a", done: true } }),
|
|
60353
|
+
sampleBlock("Open task", "checklist", { checklist: { id: "sample-b", done: false } }),
|
|
60354
|
+
sampleBlock("A quoted line", "body", { blockQuote: true }),
|
|
60355
|
+
sampleBlock("const answer = 42;", "monospaced"),
|
|
60356
|
+
sampleBlock('console.log("sample");', "monospaced")
|
|
60357
|
+
])
|
|
60358
|
+
},
|
|
60359
|
+
{
|
|
60360
|
+
id: "inline",
|
|
60361
|
+
label: "Inline formatting and links",
|
|
60362
|
+
meta: sampleMeta("00000000-0000-4000-8000-000000000002"),
|
|
60363
|
+
note: sampleNote(2, [
|
|
60364
|
+
sampleBlock("Formatting Sample", "title"),
|
|
60365
|
+
sampleBlock("", "body", {}, [
|
|
60366
|
+
{ text: "Bold", bold: true },
|
|
60367
|
+
{ text: ", " },
|
|
60368
|
+
{ text: "italic", italic: true },
|
|
60369
|
+
{ text: ", " },
|
|
60370
|
+
{ text: "both", bold: true, italic: true },
|
|
60371
|
+
{ text: ", " },
|
|
60372
|
+
{ text: "struck", strikethrough: true },
|
|
60373
|
+
{ text: ", " },
|
|
60374
|
+
{ text: "underlined", underline: true },
|
|
60375
|
+
{ text: "." }
|
|
60376
|
+
]),
|
|
60377
|
+
sampleBlock("", "body", {}, [
|
|
60378
|
+
{ text: "Highlighted", highlight: "purple" },
|
|
60379
|
+
{ text: ", x" },
|
|
60380
|
+
{ text: "2", superscript: true },
|
|
60381
|
+
{ text: ", H" },
|
|
60382
|
+
{ text: "2", subscript: true },
|
|
60383
|
+
{ text: "O, and " },
|
|
60384
|
+
{ text: "red text", color: "#FF3B30" },
|
|
60385
|
+
{ text: "." }
|
|
60386
|
+
]),
|
|
60387
|
+
sampleBlock("", "body", {}, [
|
|
60388
|
+
{ text: "A " },
|
|
60389
|
+
{ text: "web link", link: "https://example.com/page", linkSafe: true },
|
|
60390
|
+
{ text: " and a " },
|
|
60391
|
+
{ text: "bold link", link: "https://example.com/bold", linkSafe: true, bold: true },
|
|
60392
|
+
{ text: "." }
|
|
60393
|
+
])
|
|
60394
|
+
])
|
|
60395
|
+
},
|
|
60396
|
+
{
|
|
60397
|
+
id: "attachments",
|
|
60398
|
+
label: "Attachments and tags",
|
|
60399
|
+
meta: sampleMeta("00000000-0000-4000-8000-000000000003"),
|
|
60400
|
+
note: sampleNote(
|
|
60401
|
+
3,
|
|
60402
|
+
[
|
|
60403
|
+
sampleBlock("Trip Notes", "title"),
|
|
60404
|
+
sampleBlock("", "body", {}, [
|
|
60405
|
+
{ text: "Tagged " },
|
|
60406
|
+
sampleAttachmentRun("SAMPLE-TAG", "com.apple.notes.inlinetextattachment.hashtag"),
|
|
60407
|
+
{ text: " for later." }
|
|
60408
|
+
]),
|
|
60409
|
+
sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-IMG", "public.jpeg")]),
|
|
60410
|
+
sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-PDF", "com.adobe.pdf")]),
|
|
60411
|
+
sampleBlock("", "body", {}, [
|
|
60412
|
+
sampleAttachmentRun("SAMPLE-DIV", "com.apple.notes.inlinetextattachment.dividerline")
|
|
60413
|
+
]),
|
|
60414
|
+
sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-URL", "public.url")]),
|
|
60415
|
+
sampleBlock("", "body", {}, [sampleAttachmentRun("SAMPLE-IMGURL", "public.url")])
|
|
60416
|
+
],
|
|
60417
|
+
[
|
|
60418
|
+
sampleAttachment("SAMPLE-TAG", "com.apple.notes.inlinetextattachment.hashtag", {
|
|
60419
|
+
altText: "#travel"
|
|
60420
|
+
}),
|
|
60421
|
+
sampleAttachment("SAMPLE-IMG", "public.jpeg", { title: "harbor.jpg" }),
|
|
60422
|
+
sampleAttachment("SAMPLE-PDF", "com.adobe.pdf", { title: "itinerary.pdf" }),
|
|
60423
|
+
sampleAttachment("SAMPLE-DIV", "com.apple.notes.inlinetextattachment.dividerline"),
|
|
60424
|
+
sampleAttachment("SAMPLE-URL", "public.url", {
|
|
60425
|
+
title: "Example Domain",
|
|
60426
|
+
url: "https://example.com/"
|
|
60427
|
+
}),
|
|
60428
|
+
sampleAttachment("SAMPLE-IMGURL", "public.url", {
|
|
60429
|
+
title: "Harbor at dusk",
|
|
60430
|
+
url: "https://example.com/photos/harbor.jpg"
|
|
60431
|
+
})
|
|
60432
|
+
]
|
|
60433
|
+
)
|
|
60434
|
+
}
|
|
60435
|
+
];
|
|
60436
|
+
}
|
|
60437
|
+
|
|
60438
|
+
// src/services/templateEditorPage.ts
|
|
60439
|
+
var EDITOR_STYLE = `
|
|
60440
|
+
:root { color-scheme: light dark; --bg: #fafafa; --fg: #1d1d1f; --muted: #6e6e73;
|
|
60441
|
+
--panel: #ffffff; --border: #d2d2d7; --accent: #0a66d8; --bad: #c1272d; --ok: #1f7a3a; }
|
|
60442
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #1c1c1e; --fg: #f2f2f7; --muted: #a1a1a6;
|
|
60443
|
+
--panel: #2c2c2e; --border: #3a3a3c; --accent: #4c9bff; --bad: #ff6b6b; --ok: #4cd07d; } }
|
|
60444
|
+
* { box-sizing: border-box; }
|
|
60445
|
+
body { margin: 0; font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
|
60446
|
+
background: var(--bg); color: var(--fg); }
|
|
60447
|
+
header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; padding: 10px 16px;
|
|
60448
|
+
border-bottom: 1px solid var(--border); background: var(--panel); }
|
|
60449
|
+
header h1 { font-size: 15px; margin: 0 12px 0 0; }
|
|
60450
|
+
label { display: inline-flex; gap: 6px; align-items: center; color: var(--muted); }
|
|
60451
|
+
select, input, button, textarea { font: inherit; color: var(--fg); background: var(--bg);
|
|
60452
|
+
border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; }
|
|
60453
|
+
button { background: var(--accent); color: #fff; border-color: var(--accent); cursor: pointer; }
|
|
60454
|
+
main { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px 16px;
|
|
60455
|
+
height: calc(100vh - 110px); min-height: 360px; }
|
|
60456
|
+
@media (max-width: 800px) { main { grid-template-columns: 1fr; height: auto; } }
|
|
60457
|
+
section { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
|
60458
|
+
h2 { font-size: 13px; margin: 0 0 6px; color: var(--muted); font-weight: 600; }
|
|
60459
|
+
textarea { flex: 1; min-height: 300px; width: 100%; resize: none;
|
|
60460
|
+
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
60461
|
+
pre { flex: 1; margin: 0; overflow: auto; padding: 8px; background: var(--panel);
|
|
60462
|
+
border: 1px solid var(--border); border-radius: 6px; min-height: 300px;
|
|
60463
|
+
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
|
|
60464
|
+
ul { margin: 6px 0 0; padding-left: 18px; max-height: 120px; overflow: auto; }
|
|
60465
|
+
.bad { color: var(--bad); } .ok { color: var(--ok); } .muted { color: var(--muted); }
|
|
60466
|
+
footer { padding: 0 16px 12px; }
|
|
60467
|
+
`;
|
|
60468
|
+
var EDITOR_SCRIPT = `
|
|
60469
|
+
"use strict";
|
|
60470
|
+
const token = new URLSearchParams(location.search).get("token") || "";
|
|
60471
|
+
const $ = (id) => document.getElementById(id);
|
|
60472
|
+
const api = async (path, body) => {
|
|
60473
|
+
const res = await fetch(path, body === undefined
|
|
60474
|
+
? { headers: { Authorization: "Bearer " + token } }
|
|
60475
|
+
: { method: "POST", headers: { Authorization: "Bearer " + token,
|
|
60476
|
+
"Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
60477
|
+
const data = await res.json().catch(() => ({}));
|
|
60478
|
+
if (!res.ok) { const e = new Error((data.error && data.error.message) || res.statusText);
|
|
60479
|
+
e.detail = data.error; throw e; }
|
|
60480
|
+
return data;
|
|
60481
|
+
};
|
|
60482
|
+
const setText = (id, text, cls) => { const el = $(id); el.textContent = text;
|
|
60483
|
+
el.className = cls || ""; };
|
|
60484
|
+
const list = (id, items) => { const ul = $(id); ul.replaceChildren();
|
|
60485
|
+
for (const item of items) { const li = document.createElement("li");
|
|
60486
|
+
li.textContent = item; ul.append(li); } };
|
|
60487
|
+
let timer;
|
|
60488
|
+
async function refresh() {
|
|
60489
|
+
try {
|
|
60490
|
+
const out = await api("/api/preview", { template: $("json").value, sample: $("sample").value });
|
|
60491
|
+
if (out.valid) setText("status", "Valid template.", "ok");
|
|
60492
|
+
else setText("status", out.errors.length + " problem(s):", "bad");
|
|
60493
|
+
list("errors", out.errors.map((e) => e.path + ": " + e.message));
|
|
60494
|
+
list("warnings", out.warnings.map((w) => w.code + (w.attachmentId ? " (" + w.attachmentId + ")" : "")));
|
|
60495
|
+
$("preview").textContent = out.valid ? out.markdown : "";
|
|
60496
|
+
} catch (e) { setText("status", "Preview failed: " + e.message, "bad"); }
|
|
60497
|
+
}
|
|
60498
|
+
const schedule = () => { clearTimeout(timer); timer = setTimeout(refresh, 250); };
|
|
60499
|
+
async function load(name) {
|
|
60500
|
+
try {
|
|
60501
|
+
const out = await api("/api/template?name=" + encodeURIComponent(name));
|
|
60502
|
+
$("json").value = JSON.stringify(out.template, null, 2) + "\\n";
|
|
60503
|
+
$("name").value = out.source === "saved" ? out.name : "";
|
|
60504
|
+
refresh();
|
|
60505
|
+
} catch (e) { setText("status", "Could not open " + name + ": " + e.message, "bad"); }
|
|
60506
|
+
}
|
|
60507
|
+
async function save() {
|
|
60508
|
+
try {
|
|
60509
|
+
const out = await api("/api/save", { name: $("name").value.trim(),
|
|
60510
|
+
template: $("json").value, force: $("force").checked });
|
|
60511
|
+
setText("saved", (out.replaced ? "Replaced " : "Saved ") + out.name + " at " + out.path, "ok");
|
|
60512
|
+
$("force").checked = false;
|
|
60513
|
+
await fillPicker(out.name);
|
|
60514
|
+
} catch (e) {
|
|
60515
|
+
const errors = (e.detail && e.detail.errors) || [];
|
|
60516
|
+
setText("saved", e.message + (errors.length ? " " + errors.map((x) => x.path + ": " + x.message).join("; ") : ""), "bad");
|
|
60517
|
+
}
|
|
60518
|
+
}
|
|
60519
|
+
async function fillPicker(selected) {
|
|
60520
|
+
const state = await api("/api/state");
|
|
60521
|
+
const picker = $("picker"); picker.replaceChildren();
|
|
60522
|
+
const group = (label, names) => { const g = document.createElement("optgroup");
|
|
60523
|
+
g.label = label; for (const n of names) { const o = document.createElement("option");
|
|
60524
|
+
o.value = n; o.textContent = n; g.append(o); } picker.append(g); };
|
|
60525
|
+
group("Built-in", state.builtins);
|
|
60526
|
+
if (state.saved.length) group("Saved", state.saved);
|
|
60527
|
+
picker.value = selected;
|
|
60528
|
+
setText("dir", "Library: " + state.dir, "muted");
|
|
60529
|
+
return state;
|
|
60530
|
+
}
|
|
60531
|
+
(async () => {
|
|
60532
|
+
try {
|
|
60533
|
+
const state = await fillPicker("");
|
|
60534
|
+
const sample = $("sample");
|
|
60535
|
+
for (const s of state.samples) { const o = document.createElement("option");
|
|
60536
|
+
o.value = s.id; o.textContent = s.label; sample.append(o); }
|
|
60537
|
+
$("picker").value = state.initial.name;
|
|
60538
|
+
$("json").value = JSON.stringify(state.initial.template, null, 2) + "\\n";
|
|
60539
|
+
$("name").value = state.initial.source === "saved" ? state.initial.name : "";
|
|
60540
|
+
$("json").addEventListener("input", schedule);
|
|
60541
|
+
sample.addEventListener("change", refresh);
|
|
60542
|
+
$("picker").addEventListener("change", () => load($("picker").value));
|
|
60543
|
+
$("save").addEventListener("click", save);
|
|
60544
|
+
refresh();
|
|
60545
|
+
} catch (e) { setText("status", "Could not start: " + e.message, "bad"); }
|
|
60546
|
+
})();
|
|
60547
|
+
`;
|
|
60548
|
+
var EDITOR_BODY = `
|
|
60549
|
+
<header>
|
|
60550
|
+
<h1>Markdown template editor</h1>
|
|
60551
|
+
<label>Open <select id="picker"></select></label>
|
|
60552
|
+
<label>Preview <select id="sample"></select></label>
|
|
60553
|
+
<span id="dir" class="muted"></span>
|
|
60554
|
+
</header>
|
|
60555
|
+
<main>
|
|
60556
|
+
<section>
|
|
60557
|
+
<h2>Template JSON</h2>
|
|
60558
|
+
<textarea id="json" spellcheck="false" aria-label="Template JSON"></textarea>
|
|
60559
|
+
<div id="status" role="status"></div>
|
|
60560
|
+
<ul id="errors" class="bad"></ul>
|
|
60561
|
+
</section>
|
|
60562
|
+
<section>
|
|
60563
|
+
<h2>Markdown preview</h2>
|
|
60564
|
+
<pre id="preview" aria-label="Markdown preview"></pre>
|
|
60565
|
+
<ul id="warnings" class="muted" aria-label="Warnings"></ul>
|
|
60566
|
+
</section>
|
|
60567
|
+
</main>
|
|
60568
|
+
<footer>
|
|
60569
|
+
<label>Save as <input id="name" placeholder="my-template" maxlength="64" autocomplete="off"></label>
|
|
60570
|
+
<label><input id="force" type="checkbox"> Replace an existing template</label>
|
|
60571
|
+
<button id="save" type="button">Save</button>
|
|
60572
|
+
<span id="saved" role="status"></span>
|
|
60573
|
+
</footer>
|
|
60574
|
+
`;
|
|
60575
|
+
function templateEditorPage(nonce) {
|
|
60576
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="referrer" content="no-referrer"><title>Template Editor</title><style nonce="${nonce}">${EDITOR_STYLE}</style></head><body>${EDITOR_BODY}<script nonce="${nonce}">${EDITOR_SCRIPT}</script></body></html>`;
|
|
60577
|
+
}
|
|
60578
|
+
|
|
60579
|
+
// src/services/templateEditor.ts
|
|
60580
|
+
var DEFAULT_EDITOR_IDLE_MS = 30 * 60 * 1e3;
|
|
60581
|
+
var MAX_EDITOR_BODY_BYTES = MAX_TEMPLATE_BYTES * 2 + 4096;
|
|
60582
|
+
var EditorHttpError = class extends Error {
|
|
60583
|
+
constructor(status, code, message, errors) {
|
|
60584
|
+
super(message);
|
|
60585
|
+
this.status = status;
|
|
60586
|
+
this.code = code;
|
|
60587
|
+
this.errors = errors;
|
|
60588
|
+
}
|
|
60589
|
+
status;
|
|
60590
|
+
code;
|
|
60591
|
+
errors;
|
|
60592
|
+
};
|
|
60593
|
+
function editorPresentedToken(req, url) {
|
|
60594
|
+
return bearerToken(req)?.trim() ?? url.searchParams.get("token") ?? void 0;
|
|
60595
|
+
}
|
|
60596
|
+
var CROSS_ORIGIN_MESSAGE = {
|
|
60597
|
+
"cross-site": "Cross-site requests are refused.",
|
|
60598
|
+
"foreign-origin": "Cross-origin requests are refused.",
|
|
60599
|
+
"missing-origin": "POST requires the editor's own Origin."
|
|
60600
|
+
};
|
|
60601
|
+
async function readEditorJson(req) {
|
|
60602
|
+
const type = req.headers["content-type"] ?? "";
|
|
60603
|
+
if (!/^application\/json(?:\s*;|$)/i.test(type))
|
|
60604
|
+
throw new EditorHttpError(415, "unsupported-media-type", "Send JSON (application/json).");
|
|
60605
|
+
const chunks = [];
|
|
60606
|
+
let size = 0;
|
|
60607
|
+
for await (const chunk of req) {
|
|
60608
|
+
size += chunk.length;
|
|
60609
|
+
if (size > MAX_EDITOR_BODY_BYTES)
|
|
60610
|
+
throw new EditorHttpError(413, "too-large", "Request body too large.");
|
|
60611
|
+
chunks.push(chunk);
|
|
60612
|
+
}
|
|
60613
|
+
let parsed;
|
|
60614
|
+
try {
|
|
60615
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
60616
|
+
} catch {
|
|
60617
|
+
throw new EditorHttpError(400, "bad-request", "Request body is not valid JSON.");
|
|
60618
|
+
}
|
|
60619
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
60620
|
+
throw new EditorHttpError(400, "bad-request", "Request body must be a JSON object.");
|
|
60621
|
+
return parsed;
|
|
60622
|
+
}
|
|
60623
|
+
var editorStringField = (body, key) => {
|
|
60624
|
+
const value = body[key];
|
|
60625
|
+
if (typeof value !== "string")
|
|
60626
|
+
throw new EditorHttpError(400, "bad-request", `"${key}" must be a string.`);
|
|
60627
|
+
return value;
|
|
60628
|
+
};
|
|
60629
|
+
async function startTemplateEditor(options = {}) {
|
|
60630
|
+
const host = options.host ?? "127.0.0.1";
|
|
60631
|
+
const idleMs = options.idleMs ?? DEFAULT_EDITOR_IDLE_MS;
|
|
60632
|
+
const store = options.store ?? new TemplateStore();
|
|
60633
|
+
const token = options.token ?? newServerToken();
|
|
60634
|
+
const samples = templateSamples();
|
|
60635
|
+
const initialName = options.name ?? "standard-markdown";
|
|
60636
|
+
const initial = isBuiltinTemplate(initialName) ? builtinTemplate(initialName) : store.get(initialName);
|
|
60637
|
+
const initialSource = isBuiltinTemplate(initialName) ? "builtin" : "saved";
|
|
60638
|
+
let origin = "";
|
|
60639
|
+
let hostHeader = "";
|
|
60640
|
+
let idleTimer;
|
|
60641
|
+
let closeReason = "closed";
|
|
60642
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
60643
|
+
const loadTemplate = (name) => {
|
|
60644
|
+
if (isBuiltinTemplate(name))
|
|
60645
|
+
return { name, source: "builtin", template: builtinTemplate(name) };
|
|
60646
|
+
return { name, source: "saved", template: store.get(name) };
|
|
60647
|
+
};
|
|
60648
|
+
const state = () => {
|
|
60649
|
+
const listing = store.list();
|
|
60650
|
+
return {
|
|
60651
|
+
builtins: BUILTIN_TEMPLATE_NAMES,
|
|
60652
|
+
saved: listing.templates.map((t) => t.name),
|
|
60653
|
+
dir: listing.dir,
|
|
60654
|
+
samples: [
|
|
60655
|
+
...samples.map((s) => ({ id: s.id, label: s.label })),
|
|
60656
|
+
...options.note ? [{ id: "note", label: `Note: ${options.note.note.title}` }] : []
|
|
60657
|
+
],
|
|
60658
|
+
initial: { name: initialName, source: initialSource, template: initial }
|
|
60659
|
+
};
|
|
60660
|
+
};
|
|
60661
|
+
const preview = (body) => {
|
|
60662
|
+
const text2 = editorStringField(body, "template");
|
|
60663
|
+
const sampleId = typeof body.sample === "string" ? body.sample : samples[0].id;
|
|
60664
|
+
const source = sampleId === "note" && options.note ? options.note : samples.find((s) => s.id === sampleId);
|
|
60665
|
+
if (!source) throw new EditorHttpError(400, "bad-request", `Unknown sample "${sampleId}".`);
|
|
60666
|
+
let portable;
|
|
60667
|
+
try {
|
|
60668
|
+
portable = parseTemplate(text2);
|
|
60669
|
+
} catch (error2) {
|
|
60670
|
+
if (error2 instanceof TemplateValidationError)
|
|
60671
|
+
return { valid: false, errors: error2.errors, markdown: "", warnings: [] };
|
|
60672
|
+
throw error2;
|
|
60673
|
+
}
|
|
60674
|
+
const result = renderNotesWithTemplate(
|
|
60675
|
+
[source.note],
|
|
60676
|
+
{ stats: emptyStats() },
|
|
60677
|
+
{
|
|
60678
|
+
template: resolveTemplate(portable),
|
|
60679
|
+
exportStem: "preview",
|
|
60680
|
+
metaFor: () => source.meta
|
|
60681
|
+
}
|
|
60682
|
+
);
|
|
60683
|
+
return { valid: true, errors: [], markdown: result.markdown, warnings: result.warnings };
|
|
60684
|
+
};
|
|
60685
|
+
const save = (body) => {
|
|
60686
|
+
const name = editorStringField(body, "name");
|
|
60687
|
+
const text2 = editorStringField(body, "template");
|
|
60688
|
+
const force = body.force === true;
|
|
60689
|
+
try {
|
|
60690
|
+
const saved = store.save(name, text2, { force });
|
|
60691
|
+
return { name, path: saved.path, bytes: saved.bytes, replaced: saved.replaced };
|
|
60692
|
+
} catch (error2) {
|
|
60693
|
+
if (error2 instanceof TemplateValidationError)
|
|
60694
|
+
throw new EditorHttpError(
|
|
60695
|
+
422,
|
|
60696
|
+
"invalid-template",
|
|
60697
|
+
"The template is invalid.",
|
|
60698
|
+
error2.errors
|
|
60699
|
+
);
|
|
60700
|
+
if (error2 instanceof TemplateStoreError && error2.code === "template-exists")
|
|
60701
|
+
throw new EditorHttpError(
|
|
60702
|
+
409,
|
|
60703
|
+
error2.code,
|
|
60704
|
+
`A template named "${name}" already exists. Tick "Replace an existing template" to replace it.`
|
|
60705
|
+
);
|
|
60706
|
+
if (error2 instanceof TemplateStoreError)
|
|
60707
|
+
throw new EditorHttpError(400, error2.code, error2.message);
|
|
60708
|
+
throw error2;
|
|
60709
|
+
}
|
|
60710
|
+
};
|
|
60711
|
+
const send = (res, status, type, body, extra = {}) => {
|
|
60712
|
+
res.writeHead(status, {
|
|
60713
|
+
"Content-Type": type,
|
|
60714
|
+
"Cache-Control": "no-store",
|
|
60715
|
+
"Referrer-Policy": "no-referrer",
|
|
60716
|
+
"X-Content-Type-Options": "nosniff",
|
|
60717
|
+
"X-Frame-Options": "DENY",
|
|
60718
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
60719
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
60720
|
+
...extra
|
|
60721
|
+
});
|
|
60722
|
+
res.end(body);
|
|
60723
|
+
};
|
|
60724
|
+
const sendJson = (res, status, value) => send(res, status, "application/json; charset=utf-8", JSON.stringify(value));
|
|
60725
|
+
const touch = () => {
|
|
60726
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
60727
|
+
if (idleMs > 0)
|
|
60728
|
+
idleTimer = setTimeout(() => {
|
|
60729
|
+
closeReason = "idle";
|
|
60730
|
+
void close();
|
|
60731
|
+
}, idleMs);
|
|
60732
|
+
};
|
|
60733
|
+
const handle = async (req, res) => {
|
|
60734
|
+
if (req.headers.host !== hostHeader)
|
|
60735
|
+
throw new EditorHttpError(421, "wrong-host", "Unexpected Host header.");
|
|
60736
|
+
const url = new URL(req.url ?? "/", origin);
|
|
60737
|
+
if (!tokenMatches(editorPresentedToken(req, url), token))
|
|
60738
|
+
throw new EditorHttpError(401, "unauthorized", "Missing or wrong token.");
|
|
60739
|
+
const refusal = crossOriginRefusal(req, origin, req.method === "POST");
|
|
60740
|
+
if (refusal) throw new EditorHttpError(403, "cross-origin", CROSS_ORIGIN_MESSAGE[refusal]);
|
|
60741
|
+
touch();
|
|
60742
|
+
const route = `${req.method} ${url.pathname}`;
|
|
60743
|
+
switch (route) {
|
|
60744
|
+
case "GET /": {
|
|
60745
|
+
const nonce = randomBytes4(16).toString("base64");
|
|
60746
|
+
return send(res, 200, "text/html; charset=utf-8", templateEditorPage(nonce), {
|
|
60747
|
+
"Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`
|
|
60748
|
+
});
|
|
60749
|
+
}
|
|
60750
|
+
case "GET /api/state":
|
|
60751
|
+
return sendJson(res, 200, state());
|
|
60752
|
+
case "GET /api/template": {
|
|
60753
|
+
const name = url.searchParams.get("name") ?? "";
|
|
60754
|
+
try {
|
|
60755
|
+
return sendJson(res, 200, loadTemplate(name));
|
|
60756
|
+
} catch (error2) {
|
|
60757
|
+
if (error2 instanceof TemplateStoreError)
|
|
60758
|
+
throw new EditorHttpError(404, error2.code, error2.message);
|
|
60759
|
+
if (error2 instanceof TemplateValidationError)
|
|
60760
|
+
throw new EditorHttpError(
|
|
60761
|
+
422,
|
|
60762
|
+
"invalid-template",
|
|
60763
|
+
"The saved template is invalid.",
|
|
60764
|
+
error2.errors
|
|
60765
|
+
);
|
|
60766
|
+
throw error2;
|
|
60767
|
+
}
|
|
60768
|
+
}
|
|
60769
|
+
case "POST /api/preview":
|
|
60770
|
+
return sendJson(res, 200, preview(await readEditorJson(req)));
|
|
60771
|
+
case "POST /api/save":
|
|
60772
|
+
return sendJson(res, 200, save(await readEditorJson(req)));
|
|
60773
|
+
default:
|
|
60774
|
+
throw new EditorHttpError(404, "not-found", "Not found.");
|
|
60775
|
+
}
|
|
60776
|
+
};
|
|
60777
|
+
const server2 = createServer((req, res) => {
|
|
60778
|
+
handle(req, res).catch((error2) => {
|
|
60779
|
+
const http = error2 instanceof EditorHttpError ? error2 : new EditorHttpError(
|
|
60780
|
+
500,
|
|
60781
|
+
"internal-error",
|
|
60782
|
+
"The editor could not complete the request."
|
|
60783
|
+
);
|
|
60784
|
+
if (!res.headersSent)
|
|
60785
|
+
sendJson(res, http.status, {
|
|
60786
|
+
error: {
|
|
60787
|
+
code: http.code,
|
|
60788
|
+
message: http.message,
|
|
60789
|
+
...http.errors ? { errors: http.errors } : {}
|
|
60790
|
+
}
|
|
60791
|
+
});
|
|
60792
|
+
else res.end();
|
|
60793
|
+
});
|
|
60794
|
+
});
|
|
60795
|
+
server2.on("connection", (socket) => {
|
|
60796
|
+
sockets.add(socket);
|
|
60797
|
+
socket.on("close", () => sockets.delete(socket));
|
|
60798
|
+
});
|
|
60799
|
+
let resolveClosed;
|
|
60800
|
+
const closed = new Promise((resolve9) => resolveClosed = resolve9);
|
|
60801
|
+
let closing;
|
|
60802
|
+
const close = () => {
|
|
60803
|
+
closing ??= new Promise((resolve9) => {
|
|
60804
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
60805
|
+
server2.close(() => {
|
|
60806
|
+
resolveClosed(closeReason);
|
|
60807
|
+
resolve9();
|
|
60808
|
+
});
|
|
60809
|
+
for (const socket of sockets) socket.destroy();
|
|
60810
|
+
});
|
|
60811
|
+
return closing;
|
|
60812
|
+
};
|
|
60813
|
+
await new Promise((resolve9, reject) => {
|
|
60814
|
+
server2.once("error", reject);
|
|
60815
|
+
server2.listen({ host, port: options.port ?? 0, exclusive: true }, () => {
|
|
60816
|
+
server2.off("error", reject);
|
|
60817
|
+
resolve9();
|
|
60818
|
+
});
|
|
60819
|
+
});
|
|
60820
|
+
const port = server2.address().port;
|
|
60821
|
+
const authority = hostAuthority(host, port);
|
|
60822
|
+
origin = `http://${authority}`;
|
|
60823
|
+
hostHeader = authority;
|
|
60824
|
+
touch();
|
|
60825
|
+
return { url: `${origin}/?token=${token}`, host, port, token, closed, close };
|
|
60826
|
+
}
|
|
60827
|
+
|
|
60828
|
+
// src/services/templateEditorCli.ts
|
|
60829
|
+
var TEMPLATES_USAGE = `Usage: apple-notes-mcp templates edit [name] [options]
|
|
60830
|
+
|
|
60831
|
+
Open a local web editor for Markdown export templates. The editor validates
|
|
60832
|
+
and previews as you type, against built-in sample notes, and saves into the
|
|
60833
|
+
template library (create-only unless you tick "replace").
|
|
60834
|
+
|
|
60835
|
+
name Template to open: standard-markdown, obsidian, or a saved name
|
|
60836
|
+
--port N Port to listen on (default: a free port)
|
|
60837
|
+
--idle-minutes N Stop after N minutes without a request (default 30; 0: never)
|
|
60838
|
+
--note ID Also preview one real note (x-coredata://.../ICNote/pN), read-only
|
|
60839
|
+
--tailnet Listen on this Mac's Tailscale address instead of 127.0.0.1,
|
|
60840
|
+
so other devices on your tailnet can open the editor.
|
|
60841
|
+
Anyone on the tailnet who has the printed URL can read and save
|
|
60842
|
+
templates, and with --note can read the full body of that note.
|
|
60843
|
+
|
|
60844
|
+
Every request needs the per-run token in the printed URL. Press Ctrl-C to stop.
|
|
60845
|
+
`;
|
|
60846
|
+
function parseTemplatesArgs(argv) {
|
|
60847
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) return "help";
|
|
60848
|
+
const [command, ...rest] = argv;
|
|
60849
|
+
if (command !== "edit") throw new Error(`Unknown templates command "${command}".`);
|
|
60850
|
+
const args = {
|
|
60851
|
+
idleMinutes: DEFAULT_EDITOR_IDLE_MS / 6e4,
|
|
60852
|
+
tailnet: false
|
|
60853
|
+
};
|
|
60854
|
+
const number3 = (flag3, value, max) => {
|
|
60855
|
+
if (value === void 0 || !/^\d+$/.test(value) || Number(value) > max)
|
|
60856
|
+
throw new Error(`${flag3} needs a whole number from 0 to ${max}.`);
|
|
60857
|
+
return Number(value);
|
|
60858
|
+
};
|
|
60859
|
+
for (let i = 0; i < rest.length; i++) {
|
|
60860
|
+
const arg = rest[i];
|
|
60861
|
+
if (arg === "--port") args.port = number3(arg, rest[++i], 65535);
|
|
60862
|
+
else if (arg === "--idle-minutes") args.idleMinutes = number3(arg, rest[++i], 24 * 60);
|
|
60863
|
+
else if (arg === "--note") {
|
|
60864
|
+
const id2 = rest[++i];
|
|
60865
|
+
if (!id2 || !/^x-coredata:\/\/[^/\s]+\/ICNote\/p[0-9]{1,18}$/.test(id2))
|
|
60866
|
+
throw new Error("--note needs a note id such as x-coredata://\u2026/ICNote/p123.");
|
|
60867
|
+
args.noteId = id2;
|
|
60868
|
+
} else if (arg === "--tailnet") args.tailnet = true;
|
|
60869
|
+
else if (arg.startsWith("-")) throw new Error(`Unknown option ${arg}.`);
|
|
60870
|
+
else if (args.name === void 0) args.name = arg;
|
|
60871
|
+
else throw new Error(`Unexpected argument "${arg}".`);
|
|
60872
|
+
}
|
|
60873
|
+
return args;
|
|
60874
|
+
}
|
|
60875
|
+
var editorSignals = (stop) => {
|
|
60876
|
+
process.once("SIGINT", stop);
|
|
60877
|
+
process.once("SIGTERM", stop);
|
|
60878
|
+
return () => {
|
|
60879
|
+
process.off("SIGINT", stop);
|
|
60880
|
+
process.off("SIGTERM", stop);
|
|
60881
|
+
};
|
|
60882
|
+
};
|
|
60883
|
+
function tailnetWarning(host, withNote) {
|
|
60884
|
+
const lines = [
|
|
60885
|
+
`Listening on the tailnet address ${host}. Any device on your tailnet that has this URL can read and save templates. Keep the URL private.`,
|
|
60886
|
+
withNote ? "--note is set: the full body of that note is shown in the preview, so anyone with this URL can read the entire note. Stop the editor when you are done." : "--note exposes the full note body to anyone with this URL, so do not combine it with --tailnet unless you want that."
|
|
60887
|
+
];
|
|
60888
|
+
return `${lines.join("\n")}
|
|
60889
|
+
`;
|
|
60890
|
+
}
|
|
60891
|
+
async function runTemplatesCommand(argv, deps = {}) {
|
|
60892
|
+
const out = deps.out ?? ((text2) => process.stdout.write(text2));
|
|
60893
|
+
const err = deps.err ?? ((text2) => process.stderr.write(text2));
|
|
60894
|
+
let args;
|
|
60895
|
+
try {
|
|
60896
|
+
args = parseTemplatesArgs(argv);
|
|
60897
|
+
} catch (error2) {
|
|
60898
|
+
err(`${error2.message}
|
|
60899
|
+
|
|
60900
|
+
${TEMPLATES_USAGE}`);
|
|
60901
|
+
return 2;
|
|
60902
|
+
}
|
|
60903
|
+
if (args === "help") {
|
|
60904
|
+
out(TEMPLATES_USAGE);
|
|
60905
|
+
return 0;
|
|
60906
|
+
}
|
|
60907
|
+
let host = "127.0.0.1";
|
|
60908
|
+
if (args.tailnet) {
|
|
60909
|
+
const found = (deps.tailnetAddress ?? findTailnetAddress)();
|
|
60910
|
+
if (!found) {
|
|
60911
|
+
err(
|
|
60912
|
+
"No Tailscale address found (no 100.64.0.0/10 IPv4 address on this Mac). Connect Tailscale first, or run without --tailnet.\n"
|
|
60913
|
+
);
|
|
60914
|
+
return 1;
|
|
60915
|
+
}
|
|
60916
|
+
host = found.address;
|
|
60917
|
+
}
|
|
60918
|
+
let handle;
|
|
60919
|
+
try {
|
|
60920
|
+
const note = args.noteId ? (deps.readNote ?? ((id2) => ({ note: readExportNote(id2), meta: readExportNoteMeta(id2) })))(args.noteId) : void 0;
|
|
60921
|
+
handle = await (deps.start ?? startTemplateEditor)({
|
|
60922
|
+
host,
|
|
60923
|
+
port: args.port,
|
|
60924
|
+
idleMs: args.idleMinutes * 6e4,
|
|
60925
|
+
name: args.name,
|
|
60926
|
+
note
|
|
60927
|
+
});
|
|
60928
|
+
} catch (error2) {
|
|
60929
|
+
err(`Could not start the template editor: ${error2.message}
|
|
60930
|
+
`);
|
|
60931
|
+
return 1;
|
|
60932
|
+
}
|
|
60933
|
+
out(`Template editor: ${handle.url}
|
|
60934
|
+
`);
|
|
60935
|
+
if (args.tailnet) err(tailnetWarning(handle.host, args.noteId !== void 0));
|
|
60936
|
+
err(
|
|
60937
|
+
args.idleMinutes > 0 ? `Press Ctrl-C to stop. Stops by itself after ${args.idleMinutes} idle minute(s).
|
|
60938
|
+
` : "Press Ctrl-C to stop.\n"
|
|
60939
|
+
);
|
|
60940
|
+
const unregister = (deps.onSignal ?? editorSignals)(() => void handle.close());
|
|
60941
|
+
const reason = await handle.closed;
|
|
60942
|
+
unregister();
|
|
60943
|
+
err(
|
|
60944
|
+
reason === "idle" ? "Template editor stopped after the idle timeout.\n" : "Template editor stopped.\n"
|
|
60945
|
+
);
|
|
60946
|
+
return 0;
|
|
60947
|
+
}
|
|
60948
|
+
|
|
60222
60949
|
// src/index.ts
|
|
60223
60950
|
loadFileConfig();
|
|
60224
60951
|
var require2 = createRequire(import.meta.url);
|
|
@@ -60264,6 +60991,9 @@ if (process.argv[2] === "setup") {
|
|
|
60264
60991
|
process.stdout.write(formatShortcutSetup(report) + "\n");
|
|
60265
60992
|
process.exit(report.ready || !report.checkOnly ? 0 : 1);
|
|
60266
60993
|
}
|
|
60994
|
+
if (process.argv[2] === "templates") {
|
|
60995
|
+
process.exit(await runTemplatesCommand(process.argv.slice(3)));
|
|
60996
|
+
}
|
|
60267
60997
|
var server = new McpServer({
|
|
60268
60998
|
name: "apple-notes",
|
|
60269
60999
|
version: version2,
|
|
@@ -246,3 +246,86 @@ export:
|
|
|
246
246
|
|
|
247
247
|
Each warning carries `noteId` and, when known, `attachmentId`. At most 200
|
|
248
248
|
are listed; `warningsOmitted` counts the rest.
|
|
249
|
+
|
|
250
|
+
## Editing in a browser
|
|
251
|
+
|
|
252
|
+
`apple-notes-mcp templates edit` starts a small local web editor for
|
|
253
|
+
templates. It is a command-line tool, not an MCP tool, and it runs only while
|
|
254
|
+
you leave it running.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npx apple-notes-mcp templates edit # start from standard-markdown
|
|
258
|
+
npx apple-notes-mcp templates edit obsidian # open a built-in or saved template
|
|
259
|
+
npx apple-notes-mcp templates edit --note "x-coredata://…/ICNote/p123"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
It prints one address, such as
|
|
263
|
+
`http://127.0.0.1:52817/?token=3f9c…`. Open it in a browser. The page shows
|
|
264
|
+
the template JSON on the left and the rendered Markdown on the right. Each
|
|
265
|
+
edit is checked by the same validator as `validate-markdown-template`, and
|
|
266
|
+
problems are listed with their JSON paths. The preview uses the same renderer
|
|
267
|
+
as `export-notes-markdown`, against three built-in sample notes (headings and
|
|
268
|
+
lists, inline formatting and links, attachments and tags). Attachments render
|
|
269
|
+
as placeholders; the preview never copies files.
|
|
270
|
+
|
|
271
|
+
Real notes are not read unless you pass `--note` with one note id. That note
|
|
272
|
+
is read once, read-only, from the Notes database when the editor starts, so
|
|
273
|
+
it needs the same Full Disk Access as `export-notes-markdown`. It appears as
|
|
274
|
+
an extra preview choice.
|
|
275
|
+
|
|
276
|
+
**Save** writes the template to the library through the same path as
|
|
277
|
+
`save-markdown-template`: it validates first, is create-only, and replaces an
|
|
278
|
+
existing template only when you tick "Replace an existing template". The
|
|
279
|
+
editor never writes to Notes.
|
|
280
|
+
|
|
281
|
+
| Option | Meaning |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| `name` | Template to open first (default `standard-markdown`) |
|
|
284
|
+
| `--port N` | Port to listen on (default: any free port) |
|
|
285
|
+
| `--idle-minutes N` | Stop after N minutes with no requests (default 30; `0` turns it off) |
|
|
286
|
+
| `--note ID` | Also preview one real note, read-only |
|
|
287
|
+
| `--tailnet` | Listen on this Mac's Tailscale address instead of `127.0.0.1` (see below) |
|
|
288
|
+
|
|
289
|
+
Press Ctrl-C to stop the editor. It also stops by itself after the idle
|
|
290
|
+
period.
|
|
291
|
+
|
|
292
|
+
### What the editor exposes
|
|
293
|
+
|
|
294
|
+
- It listens on `127.0.0.1` only, unless you pass `--tailnet`.
|
|
295
|
+
- Every request must carry the random token from the printed address, either
|
|
296
|
+
as `?token=` or as an `Authorization: Bearer` header. A new token is made
|
|
297
|
+
each run, and the editor never logs it. Anyone who has the address can
|
|
298
|
+
read and save templates until the editor stops, so treat it like a
|
|
299
|
+
password.
|
|
300
|
+
- Requests whose `Host` header is not the editor's own address are refused,
|
|
301
|
+
which blocks DNS rebinding. Requests from another origin are refused, and
|
|
302
|
+
saves must come from the editor's own page with a JSON body, so another web
|
|
303
|
+
page cannot drive it.
|
|
304
|
+
- The page loads nothing from the network. Its script and style carry a
|
|
305
|
+
per-page nonce under a strict Content-Security-Policy, and responses send
|
|
306
|
+
`Referrer-Policy: no-referrer` so the token does not leak in a referrer.
|
|
307
|
+
|
|
308
|
+
### On your tailnet
|
|
309
|
+
|
|
310
|
+
`--tailnet` binds the editor to this Mac's [Tailscale](https://tailscale.com)
|
|
311
|
+
IPv4 address (the `100.64.0.0/10` address on its `utun` interface) so you
|
|
312
|
+
can edit from an iPad or another computer on your tailnet. It is off unless
|
|
313
|
+
you pass the flag, and it refuses to start when this Mac has no Tailscale
|
|
314
|
+
address.
|
|
315
|
+
|
|
316
|
+
The editor finds the address by reading the network interfaces. It does not
|
|
317
|
+
run the `tailscale` command, and it never changes Tailscale settings, Serve
|
|
318
|
+
or Funnel, ACLs, or the macOS firewall. Whether other devices can reach the
|
|
319
|
+
port is decided by your tailnet's access rules. The traffic is carried inside
|
|
320
|
+
Tailscale's encrypted tunnel, but the editor itself speaks plain HTTP.
|
|
321
|
+
|
|
322
|
+
With `--tailnet`, every device on your tailnet that is allowed to reach this
|
|
323
|
+
Mac can connect to the port, and any of them that has the printed address can
|
|
324
|
+
read and save templates. Share the address only with yourself, and stop the
|
|
325
|
+
editor when you are done.
|
|
326
|
+
|
|
327
|
+
**`--note` with `--tailnet` exposes the full note.** `--note <id>` previews one
|
|
328
|
+
real note, and the preview shows that note's whole body. With `--tailnet`,
|
|
329
|
+
anyone on your tailnet who has the printed address can read the entire note,
|
|
330
|
+
not only the templates. Do not combine the two unless you want that. The
|
|
331
|
+
editor prints this warning at startup, and `templates edit --help` repeats it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|