md-feedback 1.1.1 → 1.2.1
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 +5 -0
- package/dist/mcp-server.js +224 -140
- package/package.json +73 -70
package/README.md
CHANGED
|
@@ -52,6 +52,11 @@ That's it. No install, no setup — `npx` handles everything.
|
|
|
52
52
|
| `batch_apply` | Apply multiple operations in a single transaction |
|
|
53
53
|
| `get_memo_changes` | Get implementation history and progress for a memo |
|
|
54
54
|
|
|
55
|
+
## Safety & Reliability
|
|
56
|
+
|
|
57
|
+
- **File mutex** — concurrent MCP tool calls are serialized per-file, preventing data corruption
|
|
58
|
+
- **Improved anchor matching** — annotations find their intended location more reliably, even with multiple matches
|
|
59
|
+
|
|
55
60
|
## How It Works
|
|
56
61
|
|
|
57
62
|
1. You annotate a markdown plan in the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=yeominux.md-feedback-vscode)
|
package/dist/mcp-server.js
CHANGED
|
@@ -3224,8 +3224,8 @@ var require_utils = __commonJS({
|
|
|
3224
3224
|
}
|
|
3225
3225
|
return ind;
|
|
3226
3226
|
}
|
|
3227
|
-
function removeDotSegments(
|
|
3228
|
-
let input =
|
|
3227
|
+
function removeDotSegments(path3) {
|
|
3228
|
+
let input = path3;
|
|
3229
3229
|
const output = [];
|
|
3230
3230
|
let nextSlash = -1;
|
|
3231
3231
|
let len = 0;
|
|
@@ -3424,8 +3424,8 @@ var require_schemes = __commonJS({
|
|
|
3424
3424
|
wsComponent.secure = void 0;
|
|
3425
3425
|
}
|
|
3426
3426
|
if (wsComponent.resourceName) {
|
|
3427
|
-
const [
|
|
3428
|
-
wsComponent.path =
|
|
3427
|
+
const [path3, query] = wsComponent.resourceName.split("?");
|
|
3428
|
+
wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
|
|
3429
3429
|
wsComponent.query = query;
|
|
3430
3430
|
wsComponent.resourceName = void 0;
|
|
3431
3431
|
}
|
|
@@ -7276,8 +7276,8 @@ function getErrorMap() {
|
|
|
7276
7276
|
|
|
7277
7277
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
|
|
7278
7278
|
var makeIssue = (params) => {
|
|
7279
|
-
const { data, path:
|
|
7280
|
-
const fullPath = [...
|
|
7279
|
+
const { data, path: path3, errorMaps, issueData } = params;
|
|
7280
|
+
const fullPath = [...path3, ...issueData.path || []];
|
|
7281
7281
|
const fullIssue = {
|
|
7282
7282
|
...issueData,
|
|
7283
7283
|
path: fullPath
|
|
@@ -7393,11 +7393,11 @@ var errorUtil;
|
|
|
7393
7393
|
|
|
7394
7394
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
|
|
7395
7395
|
var ParseInputLazyPath = class {
|
|
7396
|
-
constructor(parent, value,
|
|
7396
|
+
constructor(parent, value, path3, key) {
|
|
7397
7397
|
this._cachedPath = [];
|
|
7398
7398
|
this.parent = parent;
|
|
7399
7399
|
this.data = value;
|
|
7400
|
-
this._path =
|
|
7400
|
+
this._path = path3;
|
|
7401
7401
|
this._key = key;
|
|
7402
7402
|
}
|
|
7403
7403
|
get path() {
|
|
@@ -11035,10 +11035,10 @@ function assignProp(target, prop, value) {
|
|
|
11035
11035
|
configurable: true
|
|
11036
11036
|
});
|
|
11037
11037
|
}
|
|
11038
|
-
function getElementAtPath(obj,
|
|
11039
|
-
if (!
|
|
11038
|
+
function getElementAtPath(obj, path3) {
|
|
11039
|
+
if (!path3)
|
|
11040
11040
|
return obj;
|
|
11041
|
-
return
|
|
11041
|
+
return path3.reduce((acc, key) => acc?.[key], obj);
|
|
11042
11042
|
}
|
|
11043
11043
|
function promiseAllObject(promisesObj) {
|
|
11044
11044
|
const keys = Object.keys(promisesObj);
|
|
@@ -11358,11 +11358,11 @@ function aborted(x, startIndex = 0) {
|
|
|
11358
11358
|
}
|
|
11359
11359
|
return false;
|
|
11360
11360
|
}
|
|
11361
|
-
function prefixIssues(
|
|
11361
|
+
function prefixIssues(path3, issues) {
|
|
11362
11362
|
return issues.map((iss) => {
|
|
11363
11363
|
var _a;
|
|
11364
11364
|
(_a = iss).path ?? (_a.path = []);
|
|
11365
|
-
iss.path.unshift(
|
|
11365
|
+
iss.path.unshift(path3);
|
|
11366
11366
|
return iss;
|
|
11367
11367
|
});
|
|
11368
11368
|
}
|
|
@@ -20848,6 +20848,7 @@ var StdioServerTransport = class {
|
|
|
20848
20848
|
// src/file-ops.ts
|
|
20849
20849
|
var import_fs = require("fs");
|
|
20850
20850
|
var import_path = require("path");
|
|
20851
|
+
var import_node_crypto = require("node:crypto");
|
|
20851
20852
|
function resolvePath(filePath) {
|
|
20852
20853
|
return (0, import_path.isAbsolute)(filePath) ? filePath : (0, import_path.resolve)(process.cwd(), filePath);
|
|
20853
20854
|
}
|
|
@@ -20864,9 +20865,16 @@ function readMarkdownFile(filePath) {
|
|
|
20864
20865
|
}
|
|
20865
20866
|
function writeMarkdownFile(filePath, content) {
|
|
20866
20867
|
const resolved = resolvePath(filePath);
|
|
20868
|
+
const dir = (0, import_path.dirname)(resolved);
|
|
20869
|
+
const tmpPath = (0, import_path.join)(dir, `.mf-tmp-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`);
|
|
20867
20870
|
try {
|
|
20868
|
-
(0, import_fs.writeFileSync)(
|
|
20871
|
+
(0, import_fs.writeFileSync)(tmpPath, content, "utf-8");
|
|
20872
|
+
(0, import_fs.renameSync)(tmpPath, resolved);
|
|
20869
20873
|
} catch (err) {
|
|
20874
|
+
try {
|
|
20875
|
+
(0, import_fs.unlinkSync)(tmpPath);
|
|
20876
|
+
} catch {
|
|
20877
|
+
}
|
|
20870
20878
|
throw new Error(`Cannot write file ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20871
20879
|
}
|
|
20872
20880
|
}
|
|
@@ -20887,6 +20895,13 @@ function writeSnapshot(mdFilePath, content) {
|
|
|
20887
20895
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20888
20896
|
const snapshotPath = (0, import_path.join)(snapshotsDir, `snapshot-${ts}.md`);
|
|
20889
20897
|
(0, import_fs.writeFileSync)(snapshotPath, content, "utf-8");
|
|
20898
|
+
const files = (0, import_fs.readdirSync)(snapshotsDir).filter((f) => f.startsWith("snapshot-")).sort();
|
|
20899
|
+
while (files.length > 20) {
|
|
20900
|
+
try {
|
|
20901
|
+
(0, import_fs.unlinkSync)((0, import_path.join)(snapshotsDir, files.shift()));
|
|
20902
|
+
} catch {
|
|
20903
|
+
}
|
|
20904
|
+
}
|
|
20890
20905
|
return snapshotPath;
|
|
20891
20906
|
}
|
|
20892
20907
|
function readProgress(mdFilePath) {
|
|
@@ -21155,6 +21170,12 @@ function generateContext(title, filePath, sections, highlights, docMemos, target
|
|
|
21155
21170
|
}
|
|
21156
21171
|
|
|
21157
21172
|
// ../../packages/shared/src/document-writer.ts
|
|
21173
|
+
function escAttrValue(s) {
|
|
21174
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/\n/g, " ").replace(/-->/g, "-->");
|
|
21175
|
+
}
|
|
21176
|
+
function unescAttrValue(s) {
|
|
21177
|
+
return s.replace(/-->/g, "-->").replace(/ /g, "\n").replace(/"/g, '"').replace(/&/g, "&");
|
|
21178
|
+
}
|
|
21158
21179
|
function hashLine(line) {
|
|
21159
21180
|
let hash = 5381;
|
|
21160
21181
|
for (let i = 0; i < line.length; i++) {
|
|
@@ -21184,11 +21205,10 @@ var ARTIFACT_START_RE = /^<!-- MEMO_ARTIFACT\s*$/;
|
|
|
21184
21205
|
var ARTIFACT_END_RE = /^-->$/;
|
|
21185
21206
|
var DEPENDENCY_RE = /^<!-- MEMO_DEPENDENCY\s+id="([^"]+)"\s+from="([^"]+)"\s+to="([^"]+)"\s+type="([^"]+)" -->$/;
|
|
21186
21207
|
function parseAttrs(lines) {
|
|
21187
|
-
const unesc = (s) => s.replace(/ /g, "\n").replace(/"/g, '"');
|
|
21188
21208
|
const attrs = {};
|
|
21189
21209
|
for (const line of lines) {
|
|
21190
21210
|
const m = line.trim().match(/^(\w+)="([^"]*)"$/);
|
|
21191
|
-
if (m) attrs[m[1]] =
|
|
21211
|
+
if (m) attrs[m[1]] = unescAttrValue(m[2]);
|
|
21192
21212
|
}
|
|
21193
21213
|
return attrs;
|
|
21194
21214
|
}
|
|
@@ -21209,7 +21229,6 @@ function splitDocument(markdown) {
|
|
|
21209
21229
|
const dependencies = [];
|
|
21210
21230
|
const checkpoints = [];
|
|
21211
21231
|
const gates = [];
|
|
21212
|
-
const unknownComments = [];
|
|
21213
21232
|
let cursor = null;
|
|
21214
21233
|
let openResponse = null;
|
|
21215
21234
|
let i = 0;
|
|
@@ -21292,13 +21311,15 @@ function splitDocument(markdown) {
|
|
|
21292
21311
|
}
|
|
21293
21312
|
i++;
|
|
21294
21313
|
const a = parseAttrs(attrLines);
|
|
21314
|
+
const override = a.override;
|
|
21295
21315
|
gates.push({
|
|
21296
21316
|
id: a.id || `gate_${Date.now()}`,
|
|
21297
21317
|
type: a.type || "custom",
|
|
21298
21318
|
status: a.status || "blocked",
|
|
21299
21319
|
blockedBy: a.blockedBy ? a.blockedBy.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
21300
21320
|
canProceedIf: a.canProceedIf || "",
|
|
21301
|
-
doneDefinition: a.doneDefinition || ""
|
|
21321
|
+
doneDefinition: a.doneDefinition || "",
|
|
21322
|
+
...override ? { override } : {}
|
|
21302
21323
|
});
|
|
21303
21324
|
continue;
|
|
21304
21325
|
}
|
|
@@ -21448,8 +21469,7 @@ function splitDocument(markdown) {
|
|
|
21448
21469
|
dependencies,
|
|
21449
21470
|
checkpoints,
|
|
21450
21471
|
gates,
|
|
21451
|
-
cursor
|
|
21452
|
-
unknownComments
|
|
21472
|
+
cursor
|
|
21453
21473
|
};
|
|
21454
21474
|
}
|
|
21455
21475
|
function mergeDocument(parts) {
|
|
@@ -21480,71 +21500,70 @@ function mergeDocument(parts) {
|
|
|
21480
21500
|
return sections.join("\n\n") + "\n";
|
|
21481
21501
|
}
|
|
21482
21502
|
function serializeMemoV2(memo) {
|
|
21483
|
-
const esc2 = (s) => s.replace(/"/g, """).replace(/\n/g, " ");
|
|
21484
21503
|
return [
|
|
21485
21504
|
"<!-- USER_MEMO",
|
|
21486
|
-
` id="${
|
|
21505
|
+
` id="${escAttrValue(memo.id)}"`,
|
|
21487
21506
|
` type="${memo.type}"`,
|
|
21488
21507
|
` status="${memo.status}"`,
|
|
21489
21508
|
` owner="${memo.owner}"`,
|
|
21490
|
-
` source="${
|
|
21509
|
+
` source="${escAttrValue(memo.source)}"`,
|
|
21491
21510
|
` color="${memo.color}"`,
|
|
21492
|
-
` text="${
|
|
21493
|
-
` anchorText="${
|
|
21494
|
-
` anchor="${
|
|
21511
|
+
` text="${escAttrValue(memo.text)}"`,
|
|
21512
|
+
` anchorText="${escAttrValue(memo.anchorText)}"`,
|
|
21513
|
+
` anchor="${escAttrValue(memo.anchor)}"`,
|
|
21495
21514
|
` createdAt="${memo.createdAt}"`,
|
|
21496
21515
|
` updatedAt="${memo.updatedAt}"`,
|
|
21497
21516
|
"-->"
|
|
21498
21517
|
].join("\n");
|
|
21499
21518
|
}
|
|
21500
21519
|
function serializeGate(gate) {
|
|
21501
|
-
|
|
21520
|
+
const lines = [
|
|
21502
21521
|
"<!-- GATE",
|
|
21503
21522
|
` id="${gate.id}"`,
|
|
21504
21523
|
` type="${gate.type}"`,
|
|
21505
21524
|
` status="${gate.status}"`,
|
|
21506
21525
|
` blockedBy="${gate.blockedBy.join(",")}"`,
|
|
21507
|
-
` canProceedIf="${gate.canProceedIf
|
|
21508
|
-
` doneDefinition="${gate.doneDefinition
|
|
21509
|
-
|
|
21510
|
-
|
|
21526
|
+
` canProceedIf="${escAttrValue(gate.canProceedIf)}"`,
|
|
21527
|
+
` doneDefinition="${escAttrValue(gate.doneDefinition)}"`
|
|
21528
|
+
];
|
|
21529
|
+
if (gate.override) {
|
|
21530
|
+
lines.push(` override="${gate.override}"`);
|
|
21531
|
+
}
|
|
21532
|
+
lines.push("-->");
|
|
21533
|
+
return lines.join("\n");
|
|
21511
21534
|
}
|
|
21512
21535
|
function serializeCursor(cursor) {
|
|
21513
21536
|
return [
|
|
21514
21537
|
"<!-- PLAN_CURSOR",
|
|
21515
21538
|
` taskId="${cursor.taskId}"`,
|
|
21516
21539
|
` step="${cursor.step}"`,
|
|
21517
|
-
` nextAction="${cursor.nextAction
|
|
21540
|
+
` nextAction="${escAttrValue(cursor.nextAction)}"`,
|
|
21518
21541
|
` lastSeenHash="${cursor.lastSeenHash}"`,
|
|
21519
21542
|
` updatedAt="${cursor.updatedAt}"`,
|
|
21520
21543
|
"-->"
|
|
21521
21544
|
].join("\n");
|
|
21522
21545
|
}
|
|
21523
21546
|
function serializeCheckpoint(cp) {
|
|
21524
|
-
const note = cp.note.replace(/"/g, """);
|
|
21525
21547
|
const sections = cp.sectionsReviewed.join(",");
|
|
21526
|
-
return `<!-- CHECKPOINT id="${cp.id}" time="${cp.timestamp}" note="${note}" fixes=${cp.fixes} questions=${cp.questions} highlights=${cp.highlights} sections="${sections}" -->`;
|
|
21548
|
+
return `<!-- CHECKPOINT id="${cp.id}" time="${cp.timestamp}" note="${escAttrValue(cp.note)}" fixes=${cp.fixes} questions=${cp.questions} highlights=${cp.highlights} sections="${sections}" -->`;
|
|
21527
21549
|
}
|
|
21528
21550
|
function serializeMemoImpl(impl) {
|
|
21529
|
-
const esc2 = (s) => s.replace(/"/g, """).replace(/\n/g, " ");
|
|
21530
|
-
const ops = JSON.stringify(impl.operations).replace(/"/g, """);
|
|
21531
21551
|
return [
|
|
21532
21552
|
"<!-- MEMO_IMPL",
|
|
21533
|
-
` id="${
|
|
21534
|
-
` memoId="${
|
|
21553
|
+
` id="${escAttrValue(impl.id)}"`,
|
|
21554
|
+
` memoId="${escAttrValue(impl.memoId)}"`,
|
|
21535
21555
|
` status="${impl.status}"`,
|
|
21536
|
-
` operations="${
|
|
21537
|
-
` summary="${
|
|
21556
|
+
` operations="${escAttrValue(JSON.stringify(impl.operations))}"`,
|
|
21557
|
+
` summary="${escAttrValue(impl.summary)}"`,
|
|
21538
21558
|
` appliedAt="${impl.appliedAt}"`,
|
|
21539
21559
|
"-->"
|
|
21540
21560
|
].join("\n");
|
|
21541
21561
|
}
|
|
21542
21562
|
function serializeMemoArtifact(art) {
|
|
21543
|
-
const esc2 = (s) => s.replace(/"/g, """);
|
|
21544
21563
|
return [
|
|
21545
21564
|
"<!-- MEMO_ARTIFACT",
|
|
21546
|
-
` id="${
|
|
21547
|
-
` memoId="${
|
|
21565
|
+
` id="${escAttrValue(art.id)}"`,
|
|
21566
|
+
` memoId="${escAttrValue(art.memoId)}"`,
|
|
21548
21567
|
` files="${art.files.join(",")}"`,
|
|
21549
21568
|
` linkedAt="${art.linkedAt}"`,
|
|
21550
21569
|
"-->"
|
|
@@ -22136,12 +22155,16 @@ function evaluateGate(gate, memos) {
|
|
|
22136
22155
|
function evaluateAllGates(gates, memos) {
|
|
22137
22156
|
return gates.map((gate) => ({
|
|
22138
22157
|
...gate,
|
|
22139
|
-
status: evaluateGate(gate, memos)
|
|
22158
|
+
status: gate.override || evaluateGate(gate, memos)
|
|
22140
22159
|
}));
|
|
22141
22160
|
}
|
|
22142
22161
|
|
|
22162
|
+
// src/tools.ts
|
|
22163
|
+
var import_node_fs2 = require("node:fs");
|
|
22164
|
+
|
|
22143
22165
|
// src/file-safety.ts
|
|
22144
22166
|
var import_node_path = __toESM(require("node:path"));
|
|
22167
|
+
var import_node_fs = require("node:fs");
|
|
22145
22168
|
var DEFAULT_BLOCKLIST = [
|
|
22146
22169
|
"**/.env",
|
|
22147
22170
|
".env",
|
|
@@ -22184,6 +22207,16 @@ function validateFilePath(config2, filePath) {
|
|
|
22184
22207
|
if (!resolved.startsWith(normalizedRoot + import_node_path.default.sep) && resolved !== normalizedRoot) {
|
|
22185
22208
|
return { safe: false, reason: `Path "${filePath}" resolves outside workspace root` };
|
|
22186
22209
|
}
|
|
22210
|
+
if ((0, import_node_fs.existsSync)(resolved)) {
|
|
22211
|
+
try {
|
|
22212
|
+
const realResolved = (0, import_node_fs.realpathSync)(resolved);
|
|
22213
|
+
const realRoot = (0, import_node_fs.realpathSync)(normalizedRoot);
|
|
22214
|
+
if (!realResolved.startsWith(realRoot + import_node_path.default.sep) && realResolved !== realRoot) {
|
|
22215
|
+
return { safe: false, reason: `Path "${filePath}" resolves outside workspace via symlink` };
|
|
22216
|
+
}
|
|
22217
|
+
} catch {
|
|
22218
|
+
}
|
|
22219
|
+
}
|
|
22187
22220
|
const relative = import_node_path.default.relative(normalizedRoot, resolved).replace(/\\/g, "/");
|
|
22188
22221
|
if (matchesAny(config2.blocklist, relative)) {
|
|
22189
22222
|
return { safe: false, reason: `Path "${filePath}" matches blocklist pattern` };
|
|
@@ -22272,6 +22305,26 @@ function computeMetrics(memos, impls, gates, checkpoints, artifacts, dependencie
|
|
|
22272
22305
|
};
|
|
22273
22306
|
}
|
|
22274
22307
|
|
|
22308
|
+
// src/file-mutex.ts
|
|
22309
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
22310
|
+
var locks = /* @__PURE__ */ new Map();
|
|
22311
|
+
async function withFileLock(filePath, fn) {
|
|
22312
|
+
const key = import_node_path2.default.resolve(filePath);
|
|
22313
|
+
while (locks.has(key)) {
|
|
22314
|
+
await locks.get(key);
|
|
22315
|
+
}
|
|
22316
|
+
let resolve2;
|
|
22317
|
+
locks.set(key, new Promise((r) => {
|
|
22318
|
+
resolve2 = r;
|
|
22319
|
+
}));
|
|
22320
|
+
try {
|
|
22321
|
+
return await fn();
|
|
22322
|
+
} finally {
|
|
22323
|
+
locks.delete(key);
|
|
22324
|
+
resolve2();
|
|
22325
|
+
}
|
|
22326
|
+
}
|
|
22327
|
+
|
|
22275
22328
|
// src/tools.ts
|
|
22276
22329
|
function computeLineHash(line) {
|
|
22277
22330
|
let hash = 5381;
|
|
@@ -22281,6 +22334,17 @@ function computeLineHash(line) {
|
|
|
22281
22334
|
return hash.toString(16).padStart(8, "0").slice(0, 8);
|
|
22282
22335
|
}
|
|
22283
22336
|
function registerTools(server2) {
|
|
22337
|
+
const safety = createFileSafety();
|
|
22338
|
+
function safeRead(file) {
|
|
22339
|
+
const check2 = validateFilePath(safety, file);
|
|
22340
|
+
if (!check2.safe) throw new Error(check2.reason);
|
|
22341
|
+
return readMarkdownFile(file);
|
|
22342
|
+
}
|
|
22343
|
+
function safeWrite(file, content) {
|
|
22344
|
+
const check2 = validateFilePath(safety, file);
|
|
22345
|
+
if (!check2.safe) throw new Error(check2.reason);
|
|
22346
|
+
writeMarkdownFile(file, content);
|
|
22347
|
+
}
|
|
22284
22348
|
server2.tool(
|
|
22285
22349
|
"create_checkpoint",
|
|
22286
22350
|
"Create a review checkpoint in an annotated markdown file. Records current annotation counts and reviewed sections.",
|
|
@@ -22288,11 +22352,11 @@ function registerTools(server2) {
|
|
|
22288
22352
|
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
22289
22353
|
note: external_exports.string().describe('Checkpoint note (e.g., "Phase 1 review done")')
|
|
22290
22354
|
},
|
|
22291
|
-
async ({ file, note }) => {
|
|
22355
|
+
async ({ file, note }) => withFileLock(file, async () => {
|
|
22292
22356
|
try {
|
|
22293
|
-
const markdown =
|
|
22357
|
+
const markdown = safeRead(file);
|
|
22294
22358
|
const { checkpoint, updatedMarkdown } = createCheckpoint(markdown, note);
|
|
22295
|
-
|
|
22359
|
+
safeWrite(file, updatedMarkdown);
|
|
22296
22360
|
return {
|
|
22297
22361
|
content: [{
|
|
22298
22362
|
type: "text",
|
|
@@ -22308,7 +22372,7 @@ function registerTools(server2) {
|
|
|
22308
22372
|
isError: true
|
|
22309
22373
|
};
|
|
22310
22374
|
}
|
|
22311
|
-
}
|
|
22375
|
+
})
|
|
22312
22376
|
);
|
|
22313
22377
|
server2.tool(
|
|
22314
22378
|
"get_checkpoints",
|
|
@@ -22318,7 +22382,7 @@ function registerTools(server2) {
|
|
|
22318
22382
|
},
|
|
22319
22383
|
async ({ file }) => {
|
|
22320
22384
|
try {
|
|
22321
|
-
const markdown =
|
|
22385
|
+
const markdown = safeRead(file);
|
|
22322
22386
|
const checkpoints = extractCheckpoints(markdown);
|
|
22323
22387
|
return {
|
|
22324
22388
|
content: [{
|
|
@@ -22346,7 +22410,7 @@ function registerTools(server2) {
|
|
|
22346
22410
|
},
|
|
22347
22411
|
async ({ file, target }) => {
|
|
22348
22412
|
try {
|
|
22349
|
-
const markdown =
|
|
22413
|
+
const markdown = safeRead(file);
|
|
22350
22414
|
const doc = buildHandoffDocument(markdown, file);
|
|
22351
22415
|
const handoff = formatHandoffMarkdown(doc, target || "standalone");
|
|
22352
22416
|
return {
|
|
@@ -22374,7 +22438,7 @@ function registerTools(server2) {
|
|
|
22374
22438
|
},
|
|
22375
22439
|
async ({ file }) => {
|
|
22376
22440
|
try {
|
|
22377
|
-
const markdown =
|
|
22441
|
+
const markdown = safeRead(file);
|
|
22378
22442
|
const counts = getAnnotationCounts(markdown);
|
|
22379
22443
|
const checkpoints = extractCheckpoints(markdown);
|
|
22380
22444
|
const sections = getSectionsWithAnnotations(markdown);
|
|
@@ -22410,7 +22474,7 @@ function registerTools(server2) {
|
|
|
22410
22474
|
},
|
|
22411
22475
|
async ({ file }) => {
|
|
22412
22476
|
try {
|
|
22413
|
-
const markdown =
|
|
22477
|
+
const markdown = safeRead(file);
|
|
22414
22478
|
const doc = parseHandoffFile(markdown);
|
|
22415
22479
|
if (!doc) {
|
|
22416
22480
|
return {
|
|
@@ -22446,7 +22510,7 @@ function registerTools(server2) {
|
|
|
22446
22510
|
},
|
|
22447
22511
|
async ({ file }) => {
|
|
22448
22512
|
try {
|
|
22449
|
-
const markdown =
|
|
22513
|
+
const markdown = safeRead(file);
|
|
22450
22514
|
const parts = splitDocument(markdown);
|
|
22451
22515
|
const annotations = parts.memos.map((m) => ({
|
|
22452
22516
|
id: m.id,
|
|
@@ -22486,13 +22550,14 @@ function registerTools(server2) {
|
|
|
22486
22550
|
},
|
|
22487
22551
|
async ({ file }) => {
|
|
22488
22552
|
try {
|
|
22489
|
-
const markdown =
|
|
22553
|
+
const markdown = safeRead(file);
|
|
22490
22554
|
const parts = splitDocument(markdown);
|
|
22491
22555
|
const allSections = getAllSections(markdown);
|
|
22492
22556
|
const reviewedSections = getSectionsWithAnnotations(markdown);
|
|
22493
22557
|
const gates = evaluateAllGates(parts.gates, parts.memos);
|
|
22494
22558
|
const open = parts.memos.filter((m) => m.status === "open").length;
|
|
22495
22559
|
const inProgress = parts.memos.filter((m) => m.status === "in_progress").length;
|
|
22560
|
+
const needsReview = parts.memos.filter((m) => m.status === "needs_review").length;
|
|
22496
22561
|
const answered = parts.memos.filter((m) => m.status === "answered").length;
|
|
22497
22562
|
const done = parts.memos.filter((m) => m.status === "done").length;
|
|
22498
22563
|
const failed = parts.memos.filter((m) => m.status === "failed").length;
|
|
@@ -22518,6 +22583,7 @@ function registerTools(server2) {
|
|
|
22518
22583
|
total: parts.memos.length,
|
|
22519
22584
|
open,
|
|
22520
22585
|
inProgress,
|
|
22586
|
+
needsReview,
|
|
22521
22587
|
answered,
|
|
22522
22588
|
done,
|
|
22523
22589
|
failed,
|
|
@@ -22563,29 +22629,59 @@ function registerTools(server2) {
|
|
|
22563
22629
|
text: external_exports.string().describe("The review feedback or note to attach"),
|
|
22564
22630
|
occurrence: external_exports.number().int().min(1).optional().describe("Which occurrence of anchorText to annotate (1-indexed, default 1). Use when the same text appears multiple times.")
|
|
22565
22631
|
},
|
|
22566
|
-
async ({ file, anchorText, type, text, occurrence }) => {
|
|
22632
|
+
async ({ file, anchorText, type, text, occurrence }) => withFileLock(file, async () => {
|
|
22567
22633
|
try {
|
|
22568
|
-
const markdown =
|
|
22634
|
+
const markdown = safeRead(file);
|
|
22569
22635
|
const parts = splitDocument(markdown);
|
|
22570
|
-
const targetOccurrence = occurrence ?? 1;
|
|
22571
22636
|
const bodyLines = parts.body.split("\n");
|
|
22572
22637
|
let anchorLine = -1;
|
|
22573
|
-
|
|
22574
|
-
|
|
22575
|
-
|
|
22576
|
-
|
|
22577
|
-
|
|
22578
|
-
|
|
22579
|
-
|
|
22638
|
+
if (occurrence) {
|
|
22639
|
+
let matchCount = 0;
|
|
22640
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
22641
|
+
if (bodyLines[i].includes(anchorText)) {
|
|
22642
|
+
matchCount++;
|
|
22643
|
+
if (matchCount === occurrence) {
|
|
22644
|
+
anchorLine = i;
|
|
22645
|
+
break;
|
|
22646
|
+
}
|
|
22647
|
+
}
|
|
22648
|
+
}
|
|
22649
|
+
if (anchorLine === -1) {
|
|
22650
|
+
const errMsg = matchCount === 0 ? `Anchor text not found: "${anchorText}"` : `Anchor text "${anchorText}" has ${matchCount} occurrence(s), but occurrence=${occurrence} requested`;
|
|
22651
|
+
return {
|
|
22652
|
+
content: [{ type: "text", text: JSON.stringify({ error: errMsg }) }],
|
|
22653
|
+
isError: true
|
|
22654
|
+
};
|
|
22655
|
+
}
|
|
22656
|
+
} else {
|
|
22657
|
+
const matches = [];
|
|
22658
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
22659
|
+
if (bodyLines[i].includes(anchorText)) matches.push(i);
|
|
22660
|
+
}
|
|
22661
|
+
if (matches.length === 0) {
|
|
22662
|
+
return {
|
|
22663
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Anchor text not found: "${anchorText}"` }) }],
|
|
22664
|
+
isError: true
|
|
22665
|
+
};
|
|
22666
|
+
} else if (matches.length === 1) {
|
|
22667
|
+
anchorLine = matches[0];
|
|
22668
|
+
} else {
|
|
22669
|
+
const exactMatch = matches.find((i) => bodyLines[i].trim() === anchorText.trim());
|
|
22670
|
+
if (exactMatch !== void 0) {
|
|
22671
|
+
anchorLine = exactMatch;
|
|
22672
|
+
} else {
|
|
22673
|
+
let bestIdx = matches[0];
|
|
22674
|
+
let bestSurplus = Infinity;
|
|
22675
|
+
for (const idx of matches) {
|
|
22676
|
+
const surplus = bodyLines[idx].length - anchorText.length;
|
|
22677
|
+
if (surplus < bestSurplus) {
|
|
22678
|
+
bestSurplus = surplus;
|
|
22679
|
+
bestIdx = idx;
|
|
22680
|
+
}
|
|
22681
|
+
}
|
|
22682
|
+
anchorLine = bestIdx;
|
|
22580
22683
|
}
|
|
22581
22684
|
}
|
|
22582
|
-
}
|
|
22583
|
-
if (anchorLine === -1) {
|
|
22584
|
-
const errMsg = matchCount === 0 ? `Anchor text not found: "${anchorText}"` : `Anchor text "${anchorText}" has ${matchCount} occurrence(s), but occurrence=${targetOccurrence} requested`;
|
|
22585
|
-
return {
|
|
22586
|
-
content: [{ type: "text", text: JSON.stringify({ error: errMsg }) }],
|
|
22587
|
-
isError: true
|
|
22588
|
-
};
|
|
22589
22685
|
}
|
|
22590
22686
|
const lineHash = computeLineHash(bodyLines[anchorLine]);
|
|
22591
22687
|
const lineNum = anchorLine + 1;
|
|
@@ -22624,7 +22720,7 @@ function registerTools(server2) {
|
|
|
22624
22720
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22625
22721
|
};
|
|
22626
22722
|
const updated = mergeDocument(parts);
|
|
22627
|
-
|
|
22723
|
+
safeWrite(file, updated);
|
|
22628
22724
|
return {
|
|
22629
22725
|
content: [{
|
|
22630
22726
|
type: "text",
|
|
@@ -22640,7 +22736,7 @@ function registerTools(server2) {
|
|
|
22640
22736
|
isError: true
|
|
22641
22737
|
};
|
|
22642
22738
|
}
|
|
22643
|
-
}
|
|
22739
|
+
})
|
|
22644
22740
|
);
|
|
22645
22741
|
server2.tool(
|
|
22646
22742
|
"respond_to_memo",
|
|
@@ -22650,9 +22746,9 @@ function registerTools(server2) {
|
|
|
22650
22746
|
memoId: external_exports.string().describe("The memo ID to respond to"),
|
|
22651
22747
|
response: external_exports.string().describe("The response text (markdown supported)")
|
|
22652
22748
|
},
|
|
22653
|
-
async ({ file, memoId, response }) => {
|
|
22749
|
+
async ({ file, memoId, response }) => withFileLock(file, async () => {
|
|
22654
22750
|
try {
|
|
22655
|
-
const markdown =
|
|
22751
|
+
const markdown = safeRead(file);
|
|
22656
22752
|
const parts = splitDocument(markdown);
|
|
22657
22753
|
const memo = parts.memos.find((m) => m.id === memoId);
|
|
22658
22754
|
if (!memo) {
|
|
@@ -22733,7 +22829,7 @@ function registerTools(server2) {
|
|
|
22733
22829
|
parts.gates = evaluateAllGates(parts.gates, parts.memos);
|
|
22734
22830
|
}
|
|
22735
22831
|
const updated = mergeDocument(parts);
|
|
22736
|
-
|
|
22832
|
+
safeWrite(file, updated);
|
|
22737
22833
|
return {
|
|
22738
22834
|
content: [{
|
|
22739
22835
|
type: "text",
|
|
@@ -22754,7 +22850,7 @@ function registerTools(server2) {
|
|
|
22754
22850
|
isError: true
|
|
22755
22851
|
};
|
|
22756
22852
|
}
|
|
22757
|
-
}
|
|
22853
|
+
})
|
|
22758
22854
|
);
|
|
22759
22855
|
server2.tool(
|
|
22760
22856
|
"update_memo_status",
|
|
@@ -22762,12 +22858,12 @@ function registerTools(server2) {
|
|
|
22762
22858
|
{
|
|
22763
22859
|
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
22764
22860
|
memoId: external_exports.string().describe("The memo ID to update"),
|
|
22765
|
-
status: external_exports.enum(["open", "in_progress", "answered", "done", "failed", "wontfix"]).describe("New status"),
|
|
22861
|
+
status: external_exports.enum(["open", "in_progress", "needs_review", "answered", "done", "failed", "wontfix"]).describe("New status"),
|
|
22766
22862
|
owner: external_exports.enum(["human", "agent", "tool"]).optional().describe("Optionally change the owner")
|
|
22767
22863
|
},
|
|
22768
|
-
async ({ file, memoId, status, owner }) => {
|
|
22864
|
+
async ({ file, memoId, status, owner }) => withFileLock(file, async () => {
|
|
22769
22865
|
try {
|
|
22770
|
-
const markdown =
|
|
22866
|
+
const markdown = safeRead(file);
|
|
22771
22867
|
const parts = splitDocument(markdown);
|
|
22772
22868
|
const memo = parts.memos.find((m) => m.id === memoId);
|
|
22773
22869
|
if (!memo) {
|
|
@@ -22804,7 +22900,7 @@ function registerTools(server2) {
|
|
|
22804
22900
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22805
22901
|
};
|
|
22806
22902
|
const updated = mergeDocument(parts);
|
|
22807
|
-
|
|
22903
|
+
safeWrite(file, updated);
|
|
22808
22904
|
return {
|
|
22809
22905
|
content: [{
|
|
22810
22906
|
type: "text",
|
|
@@ -22820,7 +22916,7 @@ function registerTools(server2) {
|
|
|
22820
22916
|
isError: true
|
|
22821
22917
|
};
|
|
22822
22918
|
}
|
|
22823
|
-
}
|
|
22919
|
+
})
|
|
22824
22920
|
);
|
|
22825
22921
|
server2.tool(
|
|
22826
22922
|
"update_cursor",
|
|
@@ -22831,9 +22927,9 @@ function registerTools(server2) {
|
|
|
22831
22927
|
step: external_exports.string().describe('Current step (e.g., "3/7" or "Phase 2")'),
|
|
22832
22928
|
nextAction: external_exports.string().describe("Description of the next action to take")
|
|
22833
22929
|
},
|
|
22834
|
-
async ({ file, taskId, step, nextAction }) => {
|
|
22930
|
+
async ({ file, taskId, step, nextAction }) => withFileLock(file, async () => {
|
|
22835
22931
|
try {
|
|
22836
|
-
const markdown =
|
|
22932
|
+
const markdown = safeRead(file);
|
|
22837
22933
|
const parts = splitDocument(markdown);
|
|
22838
22934
|
if (parts.memos.length > 0 && !parts.memos.some((m) => m.id === taskId)) {
|
|
22839
22935
|
return {
|
|
@@ -22854,7 +22950,7 @@ function registerTools(server2) {
|
|
|
22854
22950
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22855
22951
|
};
|
|
22856
22952
|
const updated = mergeDocument(parts);
|
|
22857
|
-
|
|
22953
|
+
safeWrite(file, updated);
|
|
22858
22954
|
return {
|
|
22859
22955
|
content: [{
|
|
22860
22956
|
type: "text",
|
|
@@ -22870,7 +22966,7 @@ function registerTools(server2) {
|
|
|
22870
22966
|
isError: true
|
|
22871
22967
|
};
|
|
22872
22968
|
}
|
|
22873
|
-
}
|
|
22969
|
+
})
|
|
22874
22970
|
);
|
|
22875
22971
|
server2.tool(
|
|
22876
22972
|
"evaluate_gates",
|
|
@@ -22880,7 +22976,7 @@ function registerTools(server2) {
|
|
|
22880
22976
|
},
|
|
22881
22977
|
async ({ file }) => {
|
|
22882
22978
|
try {
|
|
22883
|
-
const markdown =
|
|
22979
|
+
const markdown = safeRead(file);
|
|
22884
22980
|
const parts = splitDocument(markdown);
|
|
22885
22981
|
const gates = evaluateAllGates(parts.gates, parts.memos);
|
|
22886
22982
|
return {
|
|
@@ -22917,7 +23013,7 @@ function registerTools(server2) {
|
|
|
22917
23013
|
},
|
|
22918
23014
|
async ({ file, target }) => {
|
|
22919
23015
|
try {
|
|
22920
|
-
const markdown =
|
|
23016
|
+
const markdown = safeRead(file);
|
|
22921
23017
|
if (target === "handoff") {
|
|
22922
23018
|
const doc = buildHandoffDocument(markdown, file);
|
|
22923
23019
|
const handoff = formatHandoffMarkdown(doc, "standalone");
|
|
@@ -22955,7 +23051,7 @@ function registerTools(server2) {
|
|
|
22955
23051
|
);
|
|
22956
23052
|
server2.tool(
|
|
22957
23053
|
"apply_memo",
|
|
22958
|
-
"Apply an implementation action to a memo. Supports text_replace (
|
|
23054
|
+
"Apply an implementation action to a memo. Supports text_replace (replaces all occurrences in current document), file_patch (overwrites target file \u2014 snapshot saved first), and file_create (create a new file). Creates a snapshot before modification, records the implementation, and updates memo status to done.",
|
|
22959
23055
|
{
|
|
22960
23056
|
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
22961
23057
|
memoId: external_exports.string().describe("The memo ID to apply implementation to"),
|
|
@@ -22967,9 +23063,9 @@ function registerTools(server2) {
|
|
|
22967
23063
|
patch: external_exports.string().optional().describe("For file_patch: the patch content"),
|
|
22968
23064
|
content: external_exports.string().optional().describe("For file_create: the file content to write")
|
|
22969
23065
|
},
|
|
22970
|
-
async ({ file, memoId, action, dryRun, oldText, newText, targetFile, patch, content: fileContent }) => {
|
|
23066
|
+
async ({ file, memoId, action, dryRun, oldText, newText, targetFile, patch, content: fileContent }) => withFileLock(file, async () => {
|
|
22971
23067
|
try {
|
|
22972
|
-
const markdown =
|
|
23068
|
+
const markdown = safeRead(file);
|
|
22973
23069
|
const parts = splitDocument(markdown);
|
|
22974
23070
|
const memo = parts.memos.find((m) => m.id === memoId);
|
|
22975
23071
|
if (!memo) {
|
|
@@ -23020,16 +23116,6 @@ function registerTools(server2) {
|
|
|
23020
23116
|
}]
|
|
23021
23117
|
};
|
|
23022
23118
|
}
|
|
23023
|
-
if ((action === "file_patch" || action === "file_create") && targetFile) {
|
|
23024
|
-
const safety = createFileSafety();
|
|
23025
|
-
const check2 = validateFilePath(safety, targetFile);
|
|
23026
|
-
if (!check2.safe) {
|
|
23027
|
-
return {
|
|
23028
|
-
content: [{ type: "text", text: JSON.stringify({ error: `File safety: ${check2.reason}` }) }],
|
|
23029
|
-
isError: true
|
|
23030
|
-
};
|
|
23031
|
-
}
|
|
23032
|
-
}
|
|
23033
23119
|
writeSnapshot(file, markdown);
|
|
23034
23120
|
if (action === "text_replace") {
|
|
23035
23121
|
if (!parts.body.includes(oldText)) {
|
|
@@ -23038,14 +23124,17 @@ function registerTools(server2) {
|
|
|
23038
23124
|
isError: true
|
|
23039
23125
|
};
|
|
23040
23126
|
}
|
|
23041
|
-
parts.body = parts.body.
|
|
23127
|
+
parts.body = parts.body.split(oldText).join(newText);
|
|
23042
23128
|
} else if (action === "file_patch") {
|
|
23043
|
-
|
|
23129
|
+
if ((0, import_node_fs2.existsSync)(targetFile)) {
|
|
23130
|
+
writeSnapshot(targetFile, readMarkdownFile(targetFile));
|
|
23131
|
+
}
|
|
23132
|
+
safeWrite(targetFile, patch);
|
|
23044
23133
|
} else if (action === "file_create") {
|
|
23045
|
-
|
|
23134
|
+
safeWrite(targetFile, fileContent);
|
|
23046
23135
|
}
|
|
23047
23136
|
parts.impls.push(impl);
|
|
23048
|
-
memo.status = "
|
|
23137
|
+
memo.status = "needs_review";
|
|
23049
23138
|
memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23050
23139
|
if (parts.gates.length > 0) {
|
|
23051
23140
|
parts.gates = evaluateAllGates(parts.gates, parts.memos);
|
|
@@ -23060,7 +23149,7 @@ function registerTools(server2) {
|
|
|
23060
23149
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23061
23150
|
};
|
|
23062
23151
|
const updated = mergeDocument(parts);
|
|
23063
|
-
|
|
23152
|
+
safeWrite(file, updated);
|
|
23064
23153
|
return {
|
|
23065
23154
|
content: [{
|
|
23066
23155
|
type: "text",
|
|
@@ -23076,7 +23165,7 @@ function registerTools(server2) {
|
|
|
23076
23165
|
isError: true
|
|
23077
23166
|
};
|
|
23078
23167
|
}
|
|
23079
|
-
}
|
|
23168
|
+
})
|
|
23080
23169
|
);
|
|
23081
23170
|
server2.tool(
|
|
23082
23171
|
"link_artifacts",
|
|
@@ -23086,9 +23175,9 @@ function registerTools(server2) {
|
|
|
23086
23175
|
memoId: external_exports.string().describe("The memo ID to link artifacts to"),
|
|
23087
23176
|
files: external_exports.array(external_exports.string()).describe("Array of relative file paths to link")
|
|
23088
23177
|
},
|
|
23089
|
-
async ({ file, memoId, files: artifactFiles }) => {
|
|
23178
|
+
async ({ file, memoId, files: artifactFiles }) => withFileLock(file, async () => {
|
|
23090
23179
|
try {
|
|
23091
|
-
const markdown =
|
|
23180
|
+
const markdown = safeRead(file);
|
|
23092
23181
|
const parts = splitDocument(markdown);
|
|
23093
23182
|
const memo = parts.memos.find((m) => m.id === memoId);
|
|
23094
23183
|
if (!memo) {
|
|
@@ -23105,7 +23194,7 @@ function registerTools(server2) {
|
|
|
23105
23194
|
};
|
|
23106
23195
|
parts.artifacts.push(artifact);
|
|
23107
23196
|
const updated = mergeDocument(parts);
|
|
23108
|
-
|
|
23197
|
+
safeWrite(file, updated);
|
|
23109
23198
|
return {
|
|
23110
23199
|
content: [{
|
|
23111
23200
|
type: "text",
|
|
@@ -23121,7 +23210,7 @@ function registerTools(server2) {
|
|
|
23121
23210
|
isError: true
|
|
23122
23211
|
};
|
|
23123
23212
|
}
|
|
23124
|
-
}
|
|
23213
|
+
})
|
|
23125
23214
|
);
|
|
23126
23215
|
server2.tool(
|
|
23127
23216
|
"update_memo_progress",
|
|
@@ -23129,12 +23218,12 @@ function registerTools(server2) {
|
|
|
23129
23218
|
{
|
|
23130
23219
|
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
23131
23220
|
memoId: external_exports.string().describe("The memo ID to update progress for"),
|
|
23132
|
-
status: external_exports.enum(["in_progress", "done", "failed"]).describe("New progress status"),
|
|
23221
|
+
status: external_exports.enum(["in_progress", "needs_review", "done", "failed"]).describe("New progress status"),
|
|
23133
23222
|
message: external_exports.string().describe("Progress message describing what was done or what failed")
|
|
23134
23223
|
},
|
|
23135
|
-
async ({ file, memoId, status, message }) => {
|
|
23224
|
+
async ({ file, memoId, status, message }) => withFileLock(file, async () => {
|
|
23136
23225
|
try {
|
|
23137
|
-
const markdown =
|
|
23226
|
+
const markdown = safeRead(file);
|
|
23138
23227
|
const parts = splitDocument(markdown);
|
|
23139
23228
|
const memo = parts.memos.find((m) => m.id === memoId);
|
|
23140
23229
|
if (!memo) {
|
|
@@ -23165,7 +23254,7 @@ function registerTools(server2) {
|
|
|
23165
23254
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23166
23255
|
};
|
|
23167
23256
|
const updated = mergeDocument(parts);
|
|
23168
|
-
|
|
23257
|
+
safeWrite(file, updated);
|
|
23169
23258
|
return {
|
|
23170
23259
|
content: [{
|
|
23171
23260
|
type: "text",
|
|
@@ -23181,7 +23270,7 @@ function registerTools(server2) {
|
|
|
23181
23270
|
isError: true
|
|
23182
23271
|
};
|
|
23183
23272
|
}
|
|
23184
|
-
}
|
|
23273
|
+
})
|
|
23185
23274
|
);
|
|
23186
23275
|
server2.tool(
|
|
23187
23276
|
"rollback_memo",
|
|
@@ -23190,9 +23279,9 @@ function registerTools(server2) {
|
|
|
23190
23279
|
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
23191
23280
|
memoId: external_exports.string().describe("The memo ID to rollback")
|
|
23192
23281
|
},
|
|
23193
|
-
async ({ file, memoId }) => {
|
|
23282
|
+
async ({ file, memoId }) => withFileLock(file, async () => {
|
|
23194
23283
|
try {
|
|
23195
|
-
const markdown =
|
|
23284
|
+
const markdown = safeRead(file);
|
|
23196
23285
|
const parts = splitDocument(markdown);
|
|
23197
23286
|
const memo = parts.memos.find((m) => m.id === memoId);
|
|
23198
23287
|
if (!memo) {
|
|
@@ -23212,7 +23301,7 @@ function registerTools(server2) {
|
|
|
23212
23301
|
for (const op of latestImpl.operations) {
|
|
23213
23302
|
if (op.type === "text_replace") {
|
|
23214
23303
|
if (parts.body.includes(op.after)) {
|
|
23215
|
-
parts.body = parts.body.
|
|
23304
|
+
parts.body = parts.body.split(op.after).join(op.before);
|
|
23216
23305
|
}
|
|
23217
23306
|
}
|
|
23218
23307
|
}
|
|
@@ -23232,7 +23321,7 @@ function registerTools(server2) {
|
|
|
23232
23321
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23233
23322
|
};
|
|
23234
23323
|
const updated = mergeDocument(parts);
|
|
23235
|
-
|
|
23324
|
+
safeWrite(file, updated);
|
|
23236
23325
|
return {
|
|
23237
23326
|
content: [{
|
|
23238
23327
|
type: "text",
|
|
@@ -23252,7 +23341,7 @@ function registerTools(server2) {
|
|
|
23252
23341
|
isError: true
|
|
23253
23342
|
};
|
|
23254
23343
|
}
|
|
23255
|
-
}
|
|
23344
|
+
})
|
|
23256
23345
|
);
|
|
23257
23346
|
server2.tool(
|
|
23258
23347
|
"batch_apply",
|
|
@@ -23269,26 +23358,18 @@ function registerTools(server2) {
|
|
|
23269
23358
|
content: external_exports.string().optional().describe("For file_create: the file content to write")
|
|
23270
23359
|
})).describe("Array of operations to apply")
|
|
23271
23360
|
},
|
|
23272
|
-
async ({ file, operations }) => {
|
|
23361
|
+
async ({ file, operations }) => withFileLock(file, async () => {
|
|
23273
23362
|
try {
|
|
23274
|
-
const markdown =
|
|
23363
|
+
const markdown = safeRead(file);
|
|
23275
23364
|
const parts = splitDocument(markdown);
|
|
23276
23365
|
writeSnapshot(file, markdown);
|
|
23277
23366
|
const results = [];
|
|
23278
|
-
const safety = createFileSafety();
|
|
23279
23367
|
for (const op of operations) {
|
|
23280
23368
|
const memo = parts.memos.find((m) => m.id === op.memoId);
|
|
23281
23369
|
if (!memo) {
|
|
23282
23370
|
results.push({ memoId: op.memoId, implId: "", status: `error: memo not found` });
|
|
23283
23371
|
continue;
|
|
23284
23372
|
}
|
|
23285
|
-
if ((op.action === "file_patch" || op.action === "file_create") && op.targetFile) {
|
|
23286
|
-
const check2 = validateFilePath(safety, op.targetFile);
|
|
23287
|
-
if (!check2.safe) {
|
|
23288
|
-
results.push({ memoId: op.memoId, implId: "", status: `error: file safety: ${check2.reason}` });
|
|
23289
|
-
continue;
|
|
23290
|
-
}
|
|
23291
|
-
}
|
|
23292
23373
|
let operation;
|
|
23293
23374
|
if (op.action === "text_replace") {
|
|
23294
23375
|
if (!op.oldText || op.newText === void 0) {
|
|
@@ -23300,21 +23381,24 @@ function registerTools(server2) {
|
|
|
23300
23381
|
results.push({ memoId: op.memoId, implId: "", status: "error: oldText not found in body" });
|
|
23301
23382
|
continue;
|
|
23302
23383
|
}
|
|
23303
|
-
parts.body = parts.body.
|
|
23384
|
+
parts.body = parts.body.split(op.oldText).join(op.newText);
|
|
23304
23385
|
} else if (op.action === "file_patch") {
|
|
23305
23386
|
if (!op.targetFile || !op.patch) {
|
|
23306
23387
|
results.push({ memoId: op.memoId, implId: "", status: "error: file_patch requires targetFile and patch" });
|
|
23307
23388
|
continue;
|
|
23308
23389
|
}
|
|
23309
23390
|
operation = { type: "file_patch", file: op.targetFile, patch: op.patch };
|
|
23310
|
-
|
|
23391
|
+
if ((0, import_node_fs2.existsSync)(op.targetFile)) {
|
|
23392
|
+
writeSnapshot(op.targetFile, readMarkdownFile(op.targetFile));
|
|
23393
|
+
}
|
|
23394
|
+
safeWrite(op.targetFile, op.patch);
|
|
23311
23395
|
} else {
|
|
23312
23396
|
if (!op.targetFile || op.content === void 0) {
|
|
23313
23397
|
results.push({ memoId: op.memoId, implId: "", status: "error: file_create requires targetFile and content" });
|
|
23314
23398
|
continue;
|
|
23315
23399
|
}
|
|
23316
23400
|
operation = { type: "file_create", file: op.targetFile, content: op.content };
|
|
23317
|
-
|
|
23401
|
+
safeWrite(op.targetFile, op.content);
|
|
23318
23402
|
}
|
|
23319
23403
|
const impl = {
|
|
23320
23404
|
id: `impl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
@@ -23325,7 +23409,7 @@ function registerTools(server2) {
|
|
|
23325
23409
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23326
23410
|
};
|
|
23327
23411
|
parts.impls.push(impl);
|
|
23328
|
-
memo.status = "
|
|
23412
|
+
memo.status = "needs_review";
|
|
23329
23413
|
memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23330
23414
|
results.push({ memoId: op.memoId, implId: impl.id, status: "applied" });
|
|
23331
23415
|
}
|
|
@@ -23343,7 +23427,7 @@ function registerTools(server2) {
|
|
|
23343
23427
|
};
|
|
23344
23428
|
writeTransaction(file, { type: "batch_apply", results, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23345
23429
|
const updated = mergeDocument(parts);
|
|
23346
|
-
|
|
23430
|
+
safeWrite(file, updated);
|
|
23347
23431
|
return {
|
|
23348
23432
|
content: [{
|
|
23349
23433
|
type: "text",
|
|
@@ -23359,7 +23443,7 @@ function registerTools(server2) {
|
|
|
23359
23443
|
isError: true
|
|
23360
23444
|
};
|
|
23361
23445
|
}
|
|
23362
|
-
}
|
|
23446
|
+
})
|
|
23363
23447
|
);
|
|
23364
23448
|
server2.tool(
|
|
23365
23449
|
"get_memo_changes",
|
|
@@ -23370,7 +23454,7 @@ function registerTools(server2) {
|
|
|
23370
23454
|
},
|
|
23371
23455
|
async ({ file, memoId }) => {
|
|
23372
23456
|
try {
|
|
23373
|
-
const markdown =
|
|
23457
|
+
const markdown = safeRead(file);
|
|
23374
23458
|
const parts = splitDocument(markdown);
|
|
23375
23459
|
const impls = memoId ? parts.impls.filter((imp) => imp.memoId === memoId) : parts.impls;
|
|
23376
23460
|
const allProgress = readProgress(file);
|
|
@@ -23401,13 +23485,13 @@ function log(msg) {
|
|
|
23401
23485
|
}
|
|
23402
23486
|
var server = new McpServer({
|
|
23403
23487
|
name: "md-feedback",
|
|
23404
|
-
version: "1.
|
|
23488
|
+
version: "1.2.1"
|
|
23405
23489
|
});
|
|
23406
23490
|
registerTools(server);
|
|
23407
23491
|
async function main() {
|
|
23408
23492
|
const transport = new StdioServerTransport();
|
|
23409
23493
|
await server.connect(transport);
|
|
23410
|
-
log(`v${"1.
|
|
23494
|
+
log(`v${"1.2.1"} ready (stdio)`);
|
|
23411
23495
|
}
|
|
23412
23496
|
main().catch((err) => {
|
|
23413
23497
|
log(`fatal: ${err}`);
|
package/package.json
CHANGED
|
@@ -1,70 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "md-feedback",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for markdown plan review — AI agents read annotations, mark tasks done, evaluate quality gates, and generate session handoffs. 19 tools for Claude Code, Cursor, Copilot, and 8 more AI tools.",
|
|
5
|
-
"license": "SUL-1.0",
|
|
6
|
-
"author": "Yeomin Seon",
|
|
7
|
-
"type": "commonjs",
|
|
8
|
-
"bin": {
|
|
9
|
-
"md-feedback": "./bin/md-feedback.cjs"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"dist/mcp-server.js",
|
|
13
|
-
"bin/md-feedback.cjs",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "node esbuild.mjs"
|
|
18
|
-
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
21
|
-
"zod": "^3.23.0"
|
|
22
|
-
},
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"esbuild": "^0.24.2",
|
|
25
|
-
"typescript": "^5.7.0"
|
|
26
|
-
},
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=18"
|
|
29
|
-
},
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "https://github.com/yeominux/md-feedback"
|
|
33
|
-
},
|
|
34
|
-
"bugs": {
|
|
35
|
-
"url": "https://github.com/yeominux/md-feedback/issues"
|
|
36
|
-
},
|
|
37
|
-
"homepage": "https://github.com/yeominux/md-feedback#mcp-server",
|
|
38
|
-
"keywords": [
|
|
39
|
-
"mcp",
|
|
40
|
-
"mcp-server",
|
|
41
|
-
"model-context-protocol",
|
|
42
|
-
"markdown",
|
|
43
|
-
"feedback",
|
|
44
|
-
"ai",
|
|
45
|
-
"annotation",
|
|
46
|
-
"review",
|
|
47
|
-
"plan-review",
|
|
48
|
-
"ai-agent",
|
|
49
|
-
"coding-workflow",
|
|
50
|
-
"handoff",
|
|
51
|
-
"session-handoff",
|
|
52
|
-
"structured-feedback",
|
|
53
|
-
"checkpoint",
|
|
54
|
-
"ai-context",
|
|
55
|
-
"ai-coding",
|
|
56
|
-
"claude-code",
|
|
57
|
-
"cursor-ai",
|
|
58
|
-
"vibe-coding",
|
|
59
|
-
"context-engineering",
|
|
60
|
-
"gates",
|
|
61
|
-
"plan-review-tool",
|
|
62
|
-
"document-annotation",
|
|
63
|
-
"code-review",
|
|
64
|
-
"ai-workflow",
|
|
65
|
-
"copilot",
|
|
66
|
-
"markdown-review",
|
|
67
|
-
"developer-tools",
|
|
68
|
-
"quality-gate"
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "md-feedback",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "MCP server for markdown plan review — AI agents read annotations, mark tasks done, evaluate quality gates, and generate session handoffs. 19 tools for Claude Code, Cursor, Copilot, and 8 more AI tools.",
|
|
5
|
+
"license": "SUL-1.0",
|
|
6
|
+
"author": "Yeomin Seon",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"md-feedback": "./bin/md-feedback.cjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/mcp-server.js",
|
|
13
|
+
"bin/md-feedback.cjs",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "node esbuild.mjs"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
21
|
+
"zod": "^3.23.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"esbuild": "^0.24.2",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/yeominux/md-feedback"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/yeominux/md-feedback/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/yeominux/md-feedback#mcp-server",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"mcp",
|
|
40
|
+
"mcp-server",
|
|
41
|
+
"model-context-protocol",
|
|
42
|
+
"markdown",
|
|
43
|
+
"feedback",
|
|
44
|
+
"ai",
|
|
45
|
+
"annotation",
|
|
46
|
+
"review",
|
|
47
|
+
"plan-review",
|
|
48
|
+
"ai-agent",
|
|
49
|
+
"coding-workflow",
|
|
50
|
+
"handoff",
|
|
51
|
+
"session-handoff",
|
|
52
|
+
"structured-feedback",
|
|
53
|
+
"checkpoint",
|
|
54
|
+
"ai-context",
|
|
55
|
+
"ai-coding",
|
|
56
|
+
"claude-code",
|
|
57
|
+
"cursor-ai",
|
|
58
|
+
"vibe-coding",
|
|
59
|
+
"context-engineering",
|
|
60
|
+
"gates",
|
|
61
|
+
"plan-review-tool",
|
|
62
|
+
"document-annotation",
|
|
63
|
+
"code-review",
|
|
64
|
+
"ai-workflow",
|
|
65
|
+
"copilot",
|
|
66
|
+
"markdown-review",
|
|
67
|
+
"developer-tools",
|
|
68
|
+
"quality-gate",
|
|
69
|
+
"file-safety",
|
|
70
|
+
"concurrent-safety",
|
|
71
|
+
"gate-override"
|
|
72
|
+
]
|
|
73
|
+
}
|