md-feedback 1.0.1 → 1.1.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 +9 -3
- package/dist/mcp-server.js +761 -26
- package/package.json +70 -70
package/README.md
CHANGED
|
@@ -28,16 +28,16 @@ Add to your MCP client config (Claude Code, Cursor, etc.):
|
|
|
28
28
|
|
|
29
29
|
That's it. No install, no setup — `npx` handles everything.
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## 19 MCP Tools
|
|
32
32
|
|
|
33
33
|
| Tool | Description |
|
|
34
34
|
|------|-------------|
|
|
35
|
-
| `get_document_structure` | Full review state: memos, gates, cursor, sections, summary |
|
|
35
|
+
| `get_document_structure` | Full review state: memos, gates, cursor, sections, summary, metrics |
|
|
36
36
|
| `list_annotations` | All annotations with type/status/owner/color |
|
|
37
37
|
| `get_review_status` | Annotation counts and session status |
|
|
38
38
|
| `create_annotation` | Create annotation programmatically with anchor search |
|
|
39
39
|
| `respond_to_memo` | Add AI response to an annotation |
|
|
40
|
-
| `update_memo_status` | Mark a memo as open/answered/wontfix |
|
|
40
|
+
| `update_memo_status` | Mark a memo as open/in_progress/answered/done/failed/wontfix |
|
|
41
41
|
| `update_cursor` | Set plan cursor position (task ID, step, next action) |
|
|
42
42
|
| `evaluate_gates` | Check if merge/release/implement conditions are met |
|
|
43
43
|
| `export_review` | Export for a specific AI tool format |
|
|
@@ -45,6 +45,12 @@ That's it. No install, no setup — `npx` handles everything.
|
|
|
45
45
|
| `get_checkpoints` | List all checkpoints |
|
|
46
46
|
| `generate_handoff` | Generate structured handoff document |
|
|
47
47
|
| `pickup_handoff` | Parse existing handoff for session resumption |
|
|
48
|
+
| `apply_memo` | Apply implementation (text_replace, file_patch, file_create) with dry-run |
|
|
49
|
+
| `link_artifacts` | Link source files to a memo |
|
|
50
|
+
| `update_memo_progress` | Update progress with status and message |
|
|
51
|
+
| `rollback_memo` | Rollback the latest implementation for a memo |
|
|
52
|
+
| `batch_apply` | Apply multiple operations in a single transaction |
|
|
53
|
+
| `get_memo_changes` | Get implementation history and progress for a memo |
|
|
48
54
|
|
|
49
55
|
## How It Works
|
|
50
56
|
|
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(path2) {
|
|
3228
|
+
let input = path2;
|
|
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 [path2, query] = wsComponent.resourceName.split("?");
|
|
3428
|
+
wsComponent.path = path2 && path2 !== "/" ? path2 : 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, errorMaps, issueData } = params;
|
|
7280
|
-
const fullPath = [...
|
|
7279
|
+
const { data, path: path2, errorMaps, issueData } = params;
|
|
7280
|
+
const fullPath = [...path2, ...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, path2, key) {
|
|
7397
7397
|
this._cachedPath = [];
|
|
7398
7398
|
this.parent = parent;
|
|
7399
7399
|
this.data = value;
|
|
7400
|
-
this._path =
|
|
7400
|
+
this._path = path2;
|
|
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, path2) {
|
|
11039
|
+
if (!path2)
|
|
11040
11040
|
return obj;
|
|
11041
|
-
return
|
|
11041
|
+
return path2.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(path2, 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(path2);
|
|
11366
11366
|
return iss;
|
|
11367
11367
|
});
|
|
11368
11368
|
}
|
|
@@ -20870,6 +20870,53 @@ function writeMarkdownFile(filePath, content) {
|
|
|
20870
20870
|
throw new Error(`Cannot write file ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20871
20871
|
}
|
|
20872
20872
|
}
|
|
20873
|
+
function ensureSidecar(mdFilePath) {
|
|
20874
|
+
const resolved = resolvePath(mdFilePath);
|
|
20875
|
+
const dir = (0, import_path.join)((0, import_path.dirname)(resolved), ".md-feedback");
|
|
20876
|
+
if (!(0, import_fs.existsSync)(dir)) {
|
|
20877
|
+
(0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
20878
|
+
}
|
|
20879
|
+
return dir;
|
|
20880
|
+
}
|
|
20881
|
+
function writeSnapshot(mdFilePath, content) {
|
|
20882
|
+
const sidecar = ensureSidecar(mdFilePath);
|
|
20883
|
+
const snapshotsDir = (0, import_path.join)(sidecar, "snapshots");
|
|
20884
|
+
if (!(0, import_fs.existsSync)(snapshotsDir)) {
|
|
20885
|
+
(0, import_fs.mkdirSync)(snapshotsDir, { recursive: true });
|
|
20886
|
+
}
|
|
20887
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20888
|
+
const snapshotPath = (0, import_path.join)(snapshotsDir, `snapshot-${ts}.md`);
|
|
20889
|
+
(0, import_fs.writeFileSync)(snapshotPath, content, "utf-8");
|
|
20890
|
+
return snapshotPath;
|
|
20891
|
+
}
|
|
20892
|
+
function readProgress(mdFilePath) {
|
|
20893
|
+
const sidecar = ensureSidecar(mdFilePath);
|
|
20894
|
+
const progressPath = (0, import_path.join)(sidecar, "progress.json");
|
|
20895
|
+
if (!(0, import_fs.existsSync)(progressPath)) return [];
|
|
20896
|
+
try {
|
|
20897
|
+
return JSON.parse((0, import_fs.readFileSync)(progressPath, "utf-8"));
|
|
20898
|
+
} catch {
|
|
20899
|
+
return [];
|
|
20900
|
+
}
|
|
20901
|
+
}
|
|
20902
|
+
function appendProgress(mdFilePath, entry) {
|
|
20903
|
+
const sidecar = ensureSidecar(mdFilePath);
|
|
20904
|
+
const progressPath = (0, import_path.join)(sidecar, "progress.json");
|
|
20905
|
+
const entries = readProgress(mdFilePath);
|
|
20906
|
+
entries.push(entry);
|
|
20907
|
+
(0, import_fs.writeFileSync)(progressPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
20908
|
+
}
|
|
20909
|
+
function writeTransaction(mdFilePath, transaction) {
|
|
20910
|
+
const sidecar = ensureSidecar(mdFilePath);
|
|
20911
|
+
const txDir = (0, import_path.join)(sidecar, "transactions");
|
|
20912
|
+
if (!(0, import_fs.existsSync)(txDir)) {
|
|
20913
|
+
(0, import_fs.mkdirSync)(txDir, { recursive: true });
|
|
20914
|
+
}
|
|
20915
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20916
|
+
const txPath = (0, import_path.join)(txDir, `tx-${ts}.json`);
|
|
20917
|
+
(0, import_fs.writeFileSync)(txPath, JSON.stringify(transaction, null, 2), "utf-8");
|
|
20918
|
+
return txPath;
|
|
20919
|
+
}
|
|
20873
20920
|
|
|
20874
20921
|
// ../../packages/shared/src/types.ts
|
|
20875
20922
|
var HEX_TO_COLOR_NAME = {
|
|
@@ -20877,6 +20924,9 @@ var HEX_TO_COLOR_NAME = {
|
|
|
20877
20924
|
"#fca5a5": "red",
|
|
20878
20925
|
"#93c5fd": "blue"
|
|
20879
20926
|
};
|
|
20927
|
+
function isResolved(status) {
|
|
20928
|
+
return status === "answered" || status === "done" || status === "failed" || status === "wontfix";
|
|
20929
|
+
}
|
|
20880
20930
|
function colorToType(color) {
|
|
20881
20931
|
if (color === "red") return "fix";
|
|
20882
20932
|
if (color === "blue") return "question";
|
|
@@ -21128,6 +21178,11 @@ var BANNER_CONTENT_RE = /MD Feedback/;
|
|
|
21128
21178
|
var FEEDBACK_NOTES_RE = /^<!-- \/?(USER_FEEDBACK_NOTES|@\/?feedback-notes)\b.*-->$/;
|
|
21129
21179
|
var RESPONSE_OPEN_RE = /^<!-- REVIEW_RESPONSE\s+to="([^"]+)"\s*-->$/;
|
|
21130
21180
|
var RESPONSE_CLOSE_RE = /^<!-- \/REVIEW_RESPONSE\s*-->$/;
|
|
21181
|
+
var IMPL_START_RE = /^<!-- MEMO_IMPL\s*$/;
|
|
21182
|
+
var IMPL_END_RE = /^-->$/;
|
|
21183
|
+
var ARTIFACT_START_RE = /^<!-- MEMO_ARTIFACT\s*$/;
|
|
21184
|
+
var ARTIFACT_END_RE = /^-->$/;
|
|
21185
|
+
var DEPENDENCY_RE = /^<!-- MEMO_DEPENDENCY\s+id="([^"]+)"\s+from="([^"]+)"\s+to="([^"]+)"\s+type="([^"]+)" -->$/;
|
|
21131
21186
|
function parseAttrs(lines) {
|
|
21132
21187
|
const unesc = (s) => s.replace(/ /g, "\n").replace(/"/g, '"');
|
|
21133
21188
|
const attrs = {};
|
|
@@ -21149,6 +21204,9 @@ function splitDocument(markdown) {
|
|
|
21149
21204
|
const bodyLines = [];
|
|
21150
21205
|
const memos = [];
|
|
21151
21206
|
const responses = [];
|
|
21207
|
+
const impls = [];
|
|
21208
|
+
const artifacts = [];
|
|
21209
|
+
const dependencies = [];
|
|
21152
21210
|
const checkpoints = [];
|
|
21153
21211
|
const gates = [];
|
|
21154
21212
|
const unknownComments = [];
|
|
@@ -21262,6 +21320,58 @@ function splitDocument(markdown) {
|
|
|
21262
21320
|
};
|
|
21263
21321
|
continue;
|
|
21264
21322
|
}
|
|
21323
|
+
if (IMPL_START_RE.test(trimmed)) {
|
|
21324
|
+
const attrLines = [];
|
|
21325
|
+
i++;
|
|
21326
|
+
while (i < lines.length && !IMPL_END_RE.test(lines[i].trim())) {
|
|
21327
|
+
attrLines.push(lines[i]);
|
|
21328
|
+
i++;
|
|
21329
|
+
}
|
|
21330
|
+
i++;
|
|
21331
|
+
const a = parseAttrs(attrLines);
|
|
21332
|
+
let operations = [];
|
|
21333
|
+
try {
|
|
21334
|
+
operations = JSON.parse(a.operations || "[]");
|
|
21335
|
+
} catch {
|
|
21336
|
+
}
|
|
21337
|
+
impls.push({
|
|
21338
|
+
id: a.id || `impl_${Date.now().toString(36)}`,
|
|
21339
|
+
memoId: a.memoId || "",
|
|
21340
|
+
status: a.status || "applied",
|
|
21341
|
+
operations,
|
|
21342
|
+
summary: a.summary || "",
|
|
21343
|
+
appliedAt: a.appliedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
21344
|
+
});
|
|
21345
|
+
continue;
|
|
21346
|
+
}
|
|
21347
|
+
if (ARTIFACT_START_RE.test(trimmed)) {
|
|
21348
|
+
const attrLines = [];
|
|
21349
|
+
i++;
|
|
21350
|
+
while (i < lines.length && !ARTIFACT_END_RE.test(lines[i].trim())) {
|
|
21351
|
+
attrLines.push(lines[i]);
|
|
21352
|
+
i++;
|
|
21353
|
+
}
|
|
21354
|
+
i++;
|
|
21355
|
+
const a = parseAttrs(attrLines);
|
|
21356
|
+
artifacts.push({
|
|
21357
|
+
id: a.id || `art_${Date.now().toString(36)}`,
|
|
21358
|
+
memoId: a.memoId || "",
|
|
21359
|
+
files: a.files ? a.files.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
21360
|
+
linkedAt: a.linkedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
21361
|
+
});
|
|
21362
|
+
continue;
|
|
21363
|
+
}
|
|
21364
|
+
const depMatch = trimmed.match(DEPENDENCY_RE);
|
|
21365
|
+
if (depMatch) {
|
|
21366
|
+
dependencies.push({
|
|
21367
|
+
id: depMatch[1],
|
|
21368
|
+
from: depMatch[2],
|
|
21369
|
+
to: depMatch[3],
|
|
21370
|
+
type: depMatch[4]
|
|
21371
|
+
});
|
|
21372
|
+
i++;
|
|
21373
|
+
continue;
|
|
21374
|
+
}
|
|
21265
21375
|
const cpMatch = trimmed.match(CHECKPOINT_RE);
|
|
21266
21376
|
if (cpMatch) {
|
|
21267
21377
|
checkpoints.push({
|
|
@@ -21322,11 +21432,6 @@ function splitDocument(markdown) {
|
|
|
21322
21432
|
openResponse.bodyEndIdx = bodyLines.length - 1;
|
|
21323
21433
|
responses.push(openResponse);
|
|
21324
21434
|
}
|
|
21325
|
-
for (const memo of memos) {
|
|
21326
|
-
if (memo.status === "done") {
|
|
21327
|
-
memo.status = "answered";
|
|
21328
|
-
}
|
|
21329
|
-
}
|
|
21330
21435
|
const respondedMemoIds = new Set(responses.map((r) => r.to));
|
|
21331
21436
|
for (const memo of memos) {
|
|
21332
21437
|
if (memo.status === "open" && respondedMemoIds.has(memo.id)) {
|
|
@@ -21338,6 +21443,9 @@ function splitDocument(markdown) {
|
|
|
21338
21443
|
body: bodyLines.join("\n"),
|
|
21339
21444
|
memos,
|
|
21340
21445
|
responses,
|
|
21446
|
+
impls,
|
|
21447
|
+
artifacts,
|
|
21448
|
+
dependencies,
|
|
21341
21449
|
checkpoints,
|
|
21342
21450
|
gates,
|
|
21343
21451
|
cursor,
|
|
@@ -21351,6 +21459,15 @@ function mergeDocument(parts) {
|
|
|
21351
21459
|
}
|
|
21352
21460
|
const bodyWithMemos = reinsertMemosAndResponses(parts.body, parts.memos, parts.responses || []);
|
|
21353
21461
|
sections.push(bodyWithMemos);
|
|
21462
|
+
for (const impl of parts.impls || []) {
|
|
21463
|
+
sections.push(serializeMemoImpl(impl));
|
|
21464
|
+
}
|
|
21465
|
+
for (const art of parts.artifacts || []) {
|
|
21466
|
+
sections.push(serializeMemoArtifact(art));
|
|
21467
|
+
}
|
|
21468
|
+
for (const dep of parts.dependencies || []) {
|
|
21469
|
+
sections.push(serializeMemoDependency(dep));
|
|
21470
|
+
}
|
|
21354
21471
|
for (const gate of parts.gates) {
|
|
21355
21472
|
sections.push(serializeGate(gate));
|
|
21356
21473
|
}
|
|
@@ -21408,6 +21525,34 @@ function serializeCheckpoint(cp) {
|
|
|
21408
21525
|
const sections = cp.sectionsReviewed.join(",");
|
|
21409
21526
|
return `<!-- CHECKPOINT id="${cp.id}" time="${cp.timestamp}" note="${note}" fixes=${cp.fixes} questions=${cp.questions} highlights=${cp.highlights} sections="${sections}" -->`;
|
|
21410
21527
|
}
|
|
21528
|
+
function serializeMemoImpl(impl) {
|
|
21529
|
+
const esc2 = (s) => s.replace(/"/g, """).replace(/\n/g, " ");
|
|
21530
|
+
const ops = JSON.stringify(impl.operations).replace(/"/g, """);
|
|
21531
|
+
return [
|
|
21532
|
+
"<!-- MEMO_IMPL",
|
|
21533
|
+
` id="${esc2(impl.id)}"`,
|
|
21534
|
+
` memoId="${esc2(impl.memoId)}"`,
|
|
21535
|
+
` status="${impl.status}"`,
|
|
21536
|
+
` operations="${ops}"`,
|
|
21537
|
+
` summary="${esc2(impl.summary)}"`,
|
|
21538
|
+
` appliedAt="${impl.appliedAt}"`,
|
|
21539
|
+
"-->"
|
|
21540
|
+
].join("\n");
|
|
21541
|
+
}
|
|
21542
|
+
function serializeMemoArtifact(art) {
|
|
21543
|
+
const esc2 = (s) => s.replace(/"/g, """);
|
|
21544
|
+
return [
|
|
21545
|
+
"<!-- MEMO_ARTIFACT",
|
|
21546
|
+
` id="${esc2(art.id)}"`,
|
|
21547
|
+
` memoId="${esc2(art.memoId)}"`,
|
|
21548
|
+
` files="${art.files.join(",")}"`,
|
|
21549
|
+
` linkedAt="${art.linkedAt}"`,
|
|
21550
|
+
"-->"
|
|
21551
|
+
].join("\n");
|
|
21552
|
+
}
|
|
21553
|
+
function serializeMemoDependency(dep) {
|
|
21554
|
+
return `<!-- MEMO_DEPENDENCY id="${dep.id}" from="${dep.from}" to="${dep.to}" type="${dep.type}" -->`;
|
|
21555
|
+
}
|
|
21411
21556
|
function reinsertMemosAndResponses(body, memos, responses) {
|
|
21412
21557
|
if (memos.length === 0 && responses.length === 0) return body;
|
|
21413
21558
|
const lines = body.split("\n");
|
|
@@ -21981,11 +22126,11 @@ function parseHandoffFile(markdown) {
|
|
|
21981
22126
|
// ../../packages/shared/src/gate-evaluator.ts
|
|
21982
22127
|
function evaluateGate(gate, memos) {
|
|
21983
22128
|
if (gate.blockedBy.length > 0) {
|
|
21984
|
-
const blocking = gate.blockedBy.map((id) => memos.find((m) => m.id === id)).filter((m) => m != null && m.status
|
|
22129
|
+
const blocking = gate.blockedBy.map((id) => memos.find((m) => m.id === id)).filter((m) => m != null && !isResolved(m.status));
|
|
21985
22130
|
if (blocking.length > 0) return "blocked";
|
|
21986
22131
|
}
|
|
21987
|
-
const
|
|
21988
|
-
if (!
|
|
22132
|
+
const hasUnresolvedMemos = memos.some((m) => !isResolved(m.status));
|
|
22133
|
+
if (!hasUnresolvedMemos) return "done";
|
|
21989
22134
|
return "proceed";
|
|
21990
22135
|
}
|
|
21991
22136
|
function evaluateAllGates(gates, memos) {
|
|
@@ -21995,6 +22140,138 @@ function evaluateAllGates(gates, memos) {
|
|
|
21995
22140
|
}));
|
|
21996
22141
|
}
|
|
21997
22142
|
|
|
22143
|
+
// src/file-safety.ts
|
|
22144
|
+
var import_node_path = __toESM(require("node:path"));
|
|
22145
|
+
var DEFAULT_BLOCKLIST = [
|
|
22146
|
+
"**/.env",
|
|
22147
|
+
".env",
|
|
22148
|
+
"**/.env.*",
|
|
22149
|
+
".env.*",
|
|
22150
|
+
"**/credentials*",
|
|
22151
|
+
"credentials*",
|
|
22152
|
+
"**/secrets*",
|
|
22153
|
+
"secrets*",
|
|
22154
|
+
"**/*.pem",
|
|
22155
|
+
"*.pem",
|
|
22156
|
+
"**/*.key",
|
|
22157
|
+
"*.key",
|
|
22158
|
+
"**/*.p12",
|
|
22159
|
+
"*.p12",
|
|
22160
|
+
"**/node_modules/**",
|
|
22161
|
+
"node_modules/**",
|
|
22162
|
+
"**/.git/**",
|
|
22163
|
+
".git/**"
|
|
22164
|
+
];
|
|
22165
|
+
function createFileSafety(workspaceRoot) {
|
|
22166
|
+
return {
|
|
22167
|
+
workspaceRoot: workspaceRoot || process.cwd(),
|
|
22168
|
+
blocklist: [...DEFAULT_BLOCKLIST],
|
|
22169
|
+
allowlist: []
|
|
22170
|
+
};
|
|
22171
|
+
}
|
|
22172
|
+
function matchGlob(pattern, filePath) {
|
|
22173
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
22174
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
22175
|
+
const regexStr = normalizedPattern.replace(/[.+^${}()|[\]]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*").replace(/\?/g, "[^/]");
|
|
22176
|
+
return new RegExp(`^${regexStr}$`).test(normalized);
|
|
22177
|
+
}
|
|
22178
|
+
function matchesAny(patterns, filePath) {
|
|
22179
|
+
return patterns.some((pattern) => matchGlob(pattern, filePath));
|
|
22180
|
+
}
|
|
22181
|
+
function validateFilePath(config2, filePath) {
|
|
22182
|
+
const resolved = import_node_path.default.resolve(config2.workspaceRoot, filePath);
|
|
22183
|
+
const normalizedRoot = import_node_path.default.resolve(config2.workspaceRoot);
|
|
22184
|
+
if (!resolved.startsWith(normalizedRoot + import_node_path.default.sep) && resolved !== normalizedRoot) {
|
|
22185
|
+
return { safe: false, reason: `Path "${filePath}" resolves outside workspace root` };
|
|
22186
|
+
}
|
|
22187
|
+
const relative = import_node_path.default.relative(normalizedRoot, resolved).replace(/\\/g, "/");
|
|
22188
|
+
if (matchesAny(config2.blocklist, relative)) {
|
|
22189
|
+
return { safe: false, reason: `Path "${filePath}" matches blocklist pattern` };
|
|
22190
|
+
}
|
|
22191
|
+
if (config2.allowlist.length > 0 && !matchesAny(config2.allowlist, relative)) {
|
|
22192
|
+
return { safe: false, reason: `Path "${filePath}" not in allowlist` };
|
|
22193
|
+
}
|
|
22194
|
+
return { safe: true };
|
|
22195
|
+
}
|
|
22196
|
+
|
|
22197
|
+
// src/metrics.ts
|
|
22198
|
+
function computeMetrics(memos, impls, gates, checkpoints, artifacts, dependencies) {
|
|
22199
|
+
const byStatus = {};
|
|
22200
|
+
const byType = {};
|
|
22201
|
+
let resolvedCount = 0;
|
|
22202
|
+
for (const memo of memos) {
|
|
22203
|
+
byStatus[memo.status] = (byStatus[memo.status] || 0) + 1;
|
|
22204
|
+
byType[memo.type] = (byType[memo.type] || 0) + 1;
|
|
22205
|
+
if (isResolved(memo.status)) resolvedCount++;
|
|
22206
|
+
}
|
|
22207
|
+
const resolutionRate = memos.length > 0 ? resolvedCount / memos.length : 0;
|
|
22208
|
+
const implsByStatus = {};
|
|
22209
|
+
let appliedCount = 0;
|
|
22210
|
+
let revertedCount = 0;
|
|
22211
|
+
let failedCount = 0;
|
|
22212
|
+
for (const impl of impls) {
|
|
22213
|
+
implsByStatus[impl.status] = (implsByStatus[impl.status] || 0) + 1;
|
|
22214
|
+
if (impl.status === "applied") appliedCount++;
|
|
22215
|
+
if (impl.status === "reverted") revertedCount++;
|
|
22216
|
+
if (impl.status === "failed") failedCount++;
|
|
22217
|
+
}
|
|
22218
|
+
const gatesByStatus = {};
|
|
22219
|
+
for (const gate of gates) {
|
|
22220
|
+
gatesByStatus[gate.status] = (gatesByStatus[gate.status] || 0) + 1;
|
|
22221
|
+
}
|
|
22222
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
22223
|
+
for (const art of artifacts) {
|
|
22224
|
+
for (const f of art.files) {
|
|
22225
|
+
fileSet.add(f);
|
|
22226
|
+
}
|
|
22227
|
+
}
|
|
22228
|
+
const blockingChains = dependencies.filter((d) => d.type === "blocks").length;
|
|
22229
|
+
let lastCheckpoint = null;
|
|
22230
|
+
if (checkpoints.length > 0) {
|
|
22231
|
+
lastCheckpoint = checkpoints.reduce(
|
|
22232
|
+
(latest, cp) => cp.timestamp > latest ? cp.timestamp : latest,
|
|
22233
|
+
checkpoints[0].timestamp
|
|
22234
|
+
);
|
|
22235
|
+
}
|
|
22236
|
+
let avgResolutionTime = null;
|
|
22237
|
+
const resolvedMemos = memos.filter((m) => isResolved(m.status) && m.createdAt && m.updatedAt);
|
|
22238
|
+
if (resolvedMemos.length > 0) {
|
|
22239
|
+
let totalMs = 0;
|
|
22240
|
+
let validCount = 0;
|
|
22241
|
+
for (const memo of resolvedMemos) {
|
|
22242
|
+
const created = new Date(memo.createdAt).getTime();
|
|
22243
|
+
const updated = new Date(memo.updatedAt).getTime();
|
|
22244
|
+
if (!isNaN(created) && !isNaN(updated) && updated > created) {
|
|
22245
|
+
totalMs += updated - created;
|
|
22246
|
+
validCount++;
|
|
22247
|
+
}
|
|
22248
|
+
}
|
|
22249
|
+
if (validCount > 0) {
|
|
22250
|
+
avgResolutionTime = totalMs / validCount;
|
|
22251
|
+
}
|
|
22252
|
+
}
|
|
22253
|
+
return {
|
|
22254
|
+
totalMemos: memos.length,
|
|
22255
|
+
byStatus,
|
|
22256
|
+
byType,
|
|
22257
|
+
resolutionRate,
|
|
22258
|
+
totalImpls: impls.length,
|
|
22259
|
+
implsByStatus,
|
|
22260
|
+
appliedCount,
|
|
22261
|
+
revertedCount,
|
|
22262
|
+
failedCount,
|
|
22263
|
+
totalGates: gates.length,
|
|
22264
|
+
gatesByStatus,
|
|
22265
|
+
totalArtifacts: artifacts.length,
|
|
22266
|
+
linkedFiles: fileSet.size,
|
|
22267
|
+
totalDependencies: dependencies.length,
|
|
22268
|
+
blockingChains,
|
|
22269
|
+
totalCheckpoints: checkpoints.length,
|
|
22270
|
+
lastCheckpoint,
|
|
22271
|
+
avgResolutionTime
|
|
22272
|
+
};
|
|
22273
|
+
}
|
|
22274
|
+
|
|
21998
22275
|
// src/tools.ts
|
|
21999
22276
|
function computeLineHash(line) {
|
|
22000
22277
|
let hash = 5381;
|
|
@@ -22215,7 +22492,11 @@ function registerTools(server2) {
|
|
|
22215
22492
|
const reviewedSections = getSectionsWithAnnotations(markdown);
|
|
22216
22493
|
const gates = evaluateAllGates(parts.gates, parts.memos);
|
|
22217
22494
|
const open = parts.memos.filter((m) => m.status === "open").length;
|
|
22218
|
-
const
|
|
22495
|
+
const inProgress = parts.memos.filter((m) => m.status === "in_progress").length;
|
|
22496
|
+
const answered = parts.memos.filter((m) => m.status === "answered").length;
|
|
22497
|
+
const done = parts.memos.filter((m) => m.status === "done").length;
|
|
22498
|
+
const failed = parts.memos.filter((m) => m.status === "failed").length;
|
|
22499
|
+
const wontfix = parts.memos.filter((m) => m.status === "wontfix").length;
|
|
22219
22500
|
const blocked = gates.filter((g) => g.status === "blocked").length;
|
|
22220
22501
|
const structure = {
|
|
22221
22502
|
version: "0.4.0",
|
|
@@ -22230,20 +22511,35 @@ function registerTools(server2) {
|
|
|
22230
22511
|
reviewed: reviewedSections,
|
|
22231
22512
|
uncovered: allSections.filter((s) => !reviewedSections.includes(s))
|
|
22232
22513
|
},
|
|
22514
|
+
impls: parts.impls,
|
|
22515
|
+
artifacts: parts.artifacts,
|
|
22516
|
+
dependencies: parts.dependencies,
|
|
22233
22517
|
summary: {
|
|
22234
22518
|
total: parts.memos.length,
|
|
22235
22519
|
open,
|
|
22520
|
+
inProgress,
|
|
22521
|
+
answered,
|
|
22236
22522
|
done,
|
|
22523
|
+
failed,
|
|
22524
|
+
wontfix,
|
|
22237
22525
|
blocked,
|
|
22238
22526
|
fixes: parts.memos.filter((m) => m.type === "fix").length,
|
|
22239
22527
|
questions: parts.memos.filter((m) => m.type === "question").length,
|
|
22240
22528
|
highlights: parts.memos.filter((m) => m.type === "highlight").length
|
|
22241
22529
|
}
|
|
22242
22530
|
};
|
|
22531
|
+
const metrics = computeMetrics(
|
|
22532
|
+
parts.memos,
|
|
22533
|
+
parts.impls,
|
|
22534
|
+
gates,
|
|
22535
|
+
parts.checkpoints,
|
|
22536
|
+
parts.artifacts,
|
|
22537
|
+
parts.dependencies
|
|
22538
|
+
);
|
|
22243
22539
|
return {
|
|
22244
22540
|
content: [{
|
|
22245
22541
|
type: "text",
|
|
22246
|
-
text: JSON.stringify(structure, null, 2)
|
|
22542
|
+
text: JSON.stringify({ ...structure, metrics }, null, 2)
|
|
22247
22543
|
}]
|
|
22248
22544
|
};
|
|
22249
22545
|
} catch (err) {
|
|
@@ -22466,7 +22762,7 @@ function registerTools(server2) {
|
|
|
22466
22762
|
{
|
|
22467
22763
|
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
22468
22764
|
memoId: external_exports.string().describe("The memo ID to update"),
|
|
22469
|
-
status: external_exports.enum(["open", "answered", "wontfix"]).describe("New status"),
|
|
22765
|
+
status: external_exports.enum(["open", "in_progress", "answered", "done", "failed", "wontfix"]).describe("New status"),
|
|
22470
22766
|
owner: external_exports.enum(["human", "agent", "tool"]).optional().describe("Optionally change the owner")
|
|
22471
22767
|
},
|
|
22472
22768
|
async ({ file, memoId, status, owner }) => {
|
|
@@ -22657,6 +22953,445 @@ function registerTools(server2) {
|
|
|
22657
22953
|
}
|
|
22658
22954
|
}
|
|
22659
22955
|
);
|
|
22956
|
+
server2.tool(
|
|
22957
|
+
"apply_memo",
|
|
22958
|
+
"Apply an implementation action to a memo. Supports text_replace (on current document), file_patch (apply patch to target file), and file_create (create a new file). Creates a snapshot before modification, records the implementation, and updates memo status to done.",
|
|
22959
|
+
{
|
|
22960
|
+
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
22961
|
+
memoId: external_exports.string().describe("The memo ID to apply implementation to"),
|
|
22962
|
+
action: external_exports.enum(["text_replace", "file_patch", "file_create"]).describe("Type of implementation action"),
|
|
22963
|
+
dryRun: external_exports.boolean().optional().default(false).describe("If true, return preview without writing"),
|
|
22964
|
+
oldText: external_exports.string().optional().describe("For text_replace: the text to find and replace"),
|
|
22965
|
+
newText: external_exports.string().optional().describe("For text_replace: the replacement text"),
|
|
22966
|
+
targetFile: external_exports.string().optional().describe("For file_patch/file_create: target file path"),
|
|
22967
|
+
patch: external_exports.string().optional().describe("For file_patch: the patch content"),
|
|
22968
|
+
content: external_exports.string().optional().describe("For file_create: the file content to write")
|
|
22969
|
+
},
|
|
22970
|
+
async ({ file, memoId, action, dryRun, oldText, newText, targetFile, patch, content: fileContent }) => {
|
|
22971
|
+
try {
|
|
22972
|
+
const markdown = readMarkdownFile(file);
|
|
22973
|
+
const parts = splitDocument(markdown);
|
|
22974
|
+
const memo = parts.memos.find((m) => m.id === memoId);
|
|
22975
|
+
if (!memo) {
|
|
22976
|
+
return {
|
|
22977
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Memo not found: ${memoId}` }) }],
|
|
22978
|
+
isError: true
|
|
22979
|
+
};
|
|
22980
|
+
}
|
|
22981
|
+
let operation;
|
|
22982
|
+
if (action === "text_replace") {
|
|
22983
|
+
if (!oldText || newText === void 0) {
|
|
22984
|
+
return {
|
|
22985
|
+
content: [{ type: "text", text: JSON.stringify({ error: "text_replace requires oldText and newText" }) }],
|
|
22986
|
+
isError: true
|
|
22987
|
+
};
|
|
22988
|
+
}
|
|
22989
|
+
operation = { type: "text_replace", file: "", before: oldText, after: newText };
|
|
22990
|
+
} else if (action === "file_patch") {
|
|
22991
|
+
if (!targetFile || !patch) {
|
|
22992
|
+
return {
|
|
22993
|
+
content: [{ type: "text", text: JSON.stringify({ error: "file_patch requires targetFile and patch" }) }],
|
|
22994
|
+
isError: true
|
|
22995
|
+
};
|
|
22996
|
+
}
|
|
22997
|
+
operation = { type: "file_patch", file: targetFile, patch };
|
|
22998
|
+
} else {
|
|
22999
|
+
if (!targetFile || fileContent === void 0) {
|
|
23000
|
+
return {
|
|
23001
|
+
content: [{ type: "text", text: JSON.stringify({ error: "file_create requires targetFile and content" }) }],
|
|
23002
|
+
isError: true
|
|
23003
|
+
};
|
|
23004
|
+
}
|
|
23005
|
+
operation = { type: "file_create", file: targetFile, content: fileContent };
|
|
23006
|
+
}
|
|
23007
|
+
const impl = {
|
|
23008
|
+
id: `impl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
23009
|
+
memoId,
|
|
23010
|
+
status: "applied",
|
|
23011
|
+
operations: [operation],
|
|
23012
|
+
summary: `${action} for ${memoId}`,
|
|
23013
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23014
|
+
};
|
|
23015
|
+
if (dryRun) {
|
|
23016
|
+
return {
|
|
23017
|
+
content: [{
|
|
23018
|
+
type: "text",
|
|
23019
|
+
text: JSON.stringify({ dryRun: true, impl, operation, memo: { id: memo.id, status: memo.status } }, null, 2)
|
|
23020
|
+
}]
|
|
23021
|
+
};
|
|
23022
|
+
}
|
|
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
|
+
writeSnapshot(file, markdown);
|
|
23034
|
+
if (action === "text_replace") {
|
|
23035
|
+
if (!parts.body.includes(oldText)) {
|
|
23036
|
+
return {
|
|
23037
|
+
content: [{ type: "text", text: JSON.stringify({ error: `oldText not found in document body` }) }],
|
|
23038
|
+
isError: true
|
|
23039
|
+
};
|
|
23040
|
+
}
|
|
23041
|
+
parts.body = parts.body.replace(oldText, newText);
|
|
23042
|
+
} else if (action === "file_patch") {
|
|
23043
|
+
writeMarkdownFile(targetFile, patch);
|
|
23044
|
+
} else if (action === "file_create") {
|
|
23045
|
+
writeMarkdownFile(targetFile, fileContent);
|
|
23046
|
+
}
|
|
23047
|
+
parts.impls.push(impl);
|
|
23048
|
+
memo.status = "done";
|
|
23049
|
+
memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23050
|
+
if (parts.gates.length > 0) {
|
|
23051
|
+
parts.gates = evaluateAllGates(parts.gates, parts.memos);
|
|
23052
|
+
}
|
|
23053
|
+
const resolvedCount = parts.memos.filter((m) => isResolved(m.status)).length;
|
|
23054
|
+
const openMemos = parts.memos.filter((m) => !isResolved(m.status));
|
|
23055
|
+
parts.cursor = {
|
|
23056
|
+
taskId: memoId,
|
|
23057
|
+
step: `${resolvedCount}/${parts.memos.length} resolved`,
|
|
23058
|
+
nextAction: openMemos.length === 0 ? "All annotations resolved \u2014 review complete" : `Resolve: ${openMemos.map((m) => m.id).slice(0, 3).join(", ")}${openMemos.length > 3 ? "..." : ""}`,
|
|
23059
|
+
lastSeenHash: generateBodyHash(parts.body),
|
|
23060
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23061
|
+
};
|
|
23062
|
+
const updated = mergeDocument(parts);
|
|
23063
|
+
writeMarkdownFile(file, updated);
|
|
23064
|
+
return {
|
|
23065
|
+
content: [{
|
|
23066
|
+
type: "text",
|
|
23067
|
+
text: JSON.stringify({ impl, memo: { id: memo.id, status: memo.status }, gatesUpdated: parts.gates.length }, null, 2)
|
|
23068
|
+
}]
|
|
23069
|
+
};
|
|
23070
|
+
} catch (err) {
|
|
23071
|
+
return {
|
|
23072
|
+
content: [{
|
|
23073
|
+
type: "text",
|
|
23074
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
23075
|
+
}],
|
|
23076
|
+
isError: true
|
|
23077
|
+
};
|
|
23078
|
+
}
|
|
23079
|
+
}
|
|
23080
|
+
);
|
|
23081
|
+
server2.tool(
|
|
23082
|
+
"link_artifacts",
|
|
23083
|
+
"Link file artifacts (source files, configs, etc.) to a memo. Creates a MemoArtifact record in the document.",
|
|
23084
|
+
{
|
|
23085
|
+
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
23086
|
+
memoId: external_exports.string().describe("The memo ID to link artifacts to"),
|
|
23087
|
+
files: external_exports.array(external_exports.string()).describe("Array of relative file paths to link")
|
|
23088
|
+
},
|
|
23089
|
+
async ({ file, memoId, files: artifactFiles }) => {
|
|
23090
|
+
try {
|
|
23091
|
+
const markdown = readMarkdownFile(file);
|
|
23092
|
+
const parts = splitDocument(markdown);
|
|
23093
|
+
const memo = parts.memos.find((m) => m.id === memoId);
|
|
23094
|
+
if (!memo) {
|
|
23095
|
+
return {
|
|
23096
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Memo not found: ${memoId}` }) }],
|
|
23097
|
+
isError: true
|
|
23098
|
+
};
|
|
23099
|
+
}
|
|
23100
|
+
const artifact = {
|
|
23101
|
+
id: `art_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
23102
|
+
memoId,
|
|
23103
|
+
files: artifactFiles,
|
|
23104
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23105
|
+
};
|
|
23106
|
+
parts.artifacts.push(artifact);
|
|
23107
|
+
const updated = mergeDocument(parts);
|
|
23108
|
+
writeMarkdownFile(file, updated);
|
|
23109
|
+
return {
|
|
23110
|
+
content: [{
|
|
23111
|
+
type: "text",
|
|
23112
|
+
text: JSON.stringify({ artifact }, null, 2)
|
|
23113
|
+
}]
|
|
23114
|
+
};
|
|
23115
|
+
} catch (err) {
|
|
23116
|
+
return {
|
|
23117
|
+
content: [{
|
|
23118
|
+
type: "text",
|
|
23119
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
23120
|
+
}],
|
|
23121
|
+
isError: true
|
|
23122
|
+
};
|
|
23123
|
+
}
|
|
23124
|
+
}
|
|
23125
|
+
);
|
|
23126
|
+
server2.tool(
|
|
23127
|
+
"update_memo_progress",
|
|
23128
|
+
"Update the progress of a memo with a status change and message. Writes progress to .md-feedback/progress.json and updates the memo status.",
|
|
23129
|
+
{
|
|
23130
|
+
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
23131
|
+
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"),
|
|
23133
|
+
message: external_exports.string().describe("Progress message describing what was done or what failed")
|
|
23134
|
+
},
|
|
23135
|
+
async ({ file, memoId, status, message }) => {
|
|
23136
|
+
try {
|
|
23137
|
+
const markdown = readMarkdownFile(file);
|
|
23138
|
+
const parts = splitDocument(markdown);
|
|
23139
|
+
const memo = parts.memos.find((m) => m.id === memoId);
|
|
23140
|
+
if (!memo) {
|
|
23141
|
+
return {
|
|
23142
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Memo not found: ${memoId}` }) }],
|
|
23143
|
+
isError: true
|
|
23144
|
+
};
|
|
23145
|
+
}
|
|
23146
|
+
memo.status = status;
|
|
23147
|
+
memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23148
|
+
const progressEntry = {
|
|
23149
|
+
memoId,
|
|
23150
|
+
status,
|
|
23151
|
+
message,
|
|
23152
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
23153
|
+
};
|
|
23154
|
+
appendProgress(file, progressEntry);
|
|
23155
|
+
if (parts.gates.length > 0) {
|
|
23156
|
+
parts.gates = evaluateAllGates(parts.gates, parts.memos);
|
|
23157
|
+
}
|
|
23158
|
+
const resolvedCount = parts.memos.filter((m) => isResolved(m.status)).length;
|
|
23159
|
+
const openMemos = parts.memos.filter((m) => !isResolved(m.status));
|
|
23160
|
+
parts.cursor = {
|
|
23161
|
+
taskId: memoId,
|
|
23162
|
+
step: `${resolvedCount}/${parts.memos.length} resolved`,
|
|
23163
|
+
nextAction: openMemos.length === 0 ? "All annotations resolved \u2014 review complete" : `Resolve: ${openMemos.map((m) => m.id).slice(0, 3).join(", ")}${openMemos.length > 3 ? "..." : ""}`,
|
|
23164
|
+
lastSeenHash: generateBodyHash(parts.body),
|
|
23165
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23166
|
+
};
|
|
23167
|
+
const updated = mergeDocument(parts);
|
|
23168
|
+
writeMarkdownFile(file, updated);
|
|
23169
|
+
return {
|
|
23170
|
+
content: [{
|
|
23171
|
+
type: "text",
|
|
23172
|
+
text: JSON.stringify({ memo: { id: memo.id, status: memo.status }, progressEntry, gatesUpdated: parts.gates.length }, null, 2)
|
|
23173
|
+
}]
|
|
23174
|
+
};
|
|
23175
|
+
} catch (err) {
|
|
23176
|
+
return {
|
|
23177
|
+
content: [{
|
|
23178
|
+
type: "text",
|
|
23179
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
23180
|
+
}],
|
|
23181
|
+
isError: true
|
|
23182
|
+
};
|
|
23183
|
+
}
|
|
23184
|
+
}
|
|
23185
|
+
);
|
|
23186
|
+
server2.tool(
|
|
23187
|
+
"rollback_memo",
|
|
23188
|
+
"Rollback the latest implementation for a memo. Reverses text_replace operations (swaps before/after), marks the impl as reverted, and sets the memo status back to open.",
|
|
23189
|
+
{
|
|
23190
|
+
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
23191
|
+
memoId: external_exports.string().describe("The memo ID to rollback")
|
|
23192
|
+
},
|
|
23193
|
+
async ({ file, memoId }) => {
|
|
23194
|
+
try {
|
|
23195
|
+
const markdown = readMarkdownFile(file);
|
|
23196
|
+
const parts = splitDocument(markdown);
|
|
23197
|
+
const memo = parts.memos.find((m) => m.id === memoId);
|
|
23198
|
+
if (!memo) {
|
|
23199
|
+
return {
|
|
23200
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Memo not found: ${memoId}` }) }],
|
|
23201
|
+
isError: true
|
|
23202
|
+
};
|
|
23203
|
+
}
|
|
23204
|
+
const memoImpls = parts.impls.filter((imp) => imp.memoId === memoId && imp.status === "applied");
|
|
23205
|
+
if (memoImpls.length === 0) {
|
|
23206
|
+
return {
|
|
23207
|
+
content: [{ type: "text", text: JSON.stringify({ error: `No applied implementation found for memo: ${memoId}` }) }],
|
|
23208
|
+
isError: true
|
|
23209
|
+
};
|
|
23210
|
+
}
|
|
23211
|
+
const latestImpl = memoImpls[memoImpls.length - 1];
|
|
23212
|
+
for (const op of latestImpl.operations) {
|
|
23213
|
+
if (op.type === "text_replace") {
|
|
23214
|
+
if (parts.body.includes(op.after)) {
|
|
23215
|
+
parts.body = parts.body.replace(op.after, op.before);
|
|
23216
|
+
}
|
|
23217
|
+
}
|
|
23218
|
+
}
|
|
23219
|
+
latestImpl.status = "reverted";
|
|
23220
|
+
memo.status = "open";
|
|
23221
|
+
memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23222
|
+
if (parts.gates.length > 0) {
|
|
23223
|
+
parts.gates = evaluateAllGates(parts.gates, parts.memos);
|
|
23224
|
+
}
|
|
23225
|
+
const resolvedCount = parts.memos.filter((m) => isResolved(m.status)).length;
|
|
23226
|
+
const openMemos = parts.memos.filter((m) => !isResolved(m.status));
|
|
23227
|
+
parts.cursor = {
|
|
23228
|
+
taskId: memoId,
|
|
23229
|
+
step: `${resolvedCount}/${parts.memos.length} resolved`,
|
|
23230
|
+
nextAction: openMemos.length === 0 ? "All annotations resolved \u2014 review complete" : `Resolve: ${openMemos.map((m) => m.id).slice(0, 3).join(", ")}${openMemos.length > 3 ? "..." : ""}`,
|
|
23231
|
+
lastSeenHash: generateBodyHash(parts.body),
|
|
23232
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23233
|
+
};
|
|
23234
|
+
const updated = mergeDocument(parts);
|
|
23235
|
+
writeMarkdownFile(file, updated);
|
|
23236
|
+
return {
|
|
23237
|
+
content: [{
|
|
23238
|
+
type: "text",
|
|
23239
|
+
text: JSON.stringify({
|
|
23240
|
+
rolledBack: latestImpl.id,
|
|
23241
|
+
memo: { id: memo.id, status: memo.status },
|
|
23242
|
+
gatesUpdated: parts.gates.length
|
|
23243
|
+
}, null, 2)
|
|
23244
|
+
}]
|
|
23245
|
+
};
|
|
23246
|
+
} catch (err) {
|
|
23247
|
+
return {
|
|
23248
|
+
content: [{
|
|
23249
|
+
type: "text",
|
|
23250
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
23251
|
+
}],
|
|
23252
|
+
isError: true
|
|
23253
|
+
};
|
|
23254
|
+
}
|
|
23255
|
+
}
|
|
23256
|
+
);
|
|
23257
|
+
server2.tool(
|
|
23258
|
+
"batch_apply",
|
|
23259
|
+
"Apply multiple implementation operations in a single transaction. Parses the document once, applies all operations sequentially, then writes once. Each operation follows the same format as apply_memo.",
|
|
23260
|
+
{
|
|
23261
|
+
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
23262
|
+
operations: external_exports.array(external_exports.object({
|
|
23263
|
+
memoId: external_exports.string().describe("The memo ID to apply implementation to"),
|
|
23264
|
+
action: external_exports.enum(["text_replace", "file_patch", "file_create"]).describe("Type of implementation action"),
|
|
23265
|
+
oldText: external_exports.string().optional().describe("For text_replace: the text to find and replace"),
|
|
23266
|
+
newText: external_exports.string().optional().describe("For text_replace: the replacement text"),
|
|
23267
|
+
targetFile: external_exports.string().optional().describe("For file_patch/file_create: target file path"),
|
|
23268
|
+
patch: external_exports.string().optional().describe("For file_patch: the patch content"),
|
|
23269
|
+
content: external_exports.string().optional().describe("For file_create: the file content to write")
|
|
23270
|
+
})).describe("Array of operations to apply")
|
|
23271
|
+
},
|
|
23272
|
+
async ({ file, operations }) => {
|
|
23273
|
+
try {
|
|
23274
|
+
const markdown = readMarkdownFile(file);
|
|
23275
|
+
const parts = splitDocument(markdown);
|
|
23276
|
+
writeSnapshot(file, markdown);
|
|
23277
|
+
const results = [];
|
|
23278
|
+
const safety = createFileSafety();
|
|
23279
|
+
for (const op of operations) {
|
|
23280
|
+
const memo = parts.memos.find((m) => m.id === op.memoId);
|
|
23281
|
+
if (!memo) {
|
|
23282
|
+
results.push({ memoId: op.memoId, implId: "", status: `error: memo not found` });
|
|
23283
|
+
continue;
|
|
23284
|
+
}
|
|
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
|
+
let operation;
|
|
23293
|
+
if (op.action === "text_replace") {
|
|
23294
|
+
if (!op.oldText || op.newText === void 0) {
|
|
23295
|
+
results.push({ memoId: op.memoId, implId: "", status: "error: text_replace requires oldText and newText" });
|
|
23296
|
+
continue;
|
|
23297
|
+
}
|
|
23298
|
+
operation = { type: "text_replace", file: "", before: op.oldText, after: op.newText };
|
|
23299
|
+
if (!parts.body.includes(op.oldText)) {
|
|
23300
|
+
results.push({ memoId: op.memoId, implId: "", status: "error: oldText not found in body" });
|
|
23301
|
+
continue;
|
|
23302
|
+
}
|
|
23303
|
+
parts.body = parts.body.replace(op.oldText, op.newText);
|
|
23304
|
+
} else if (op.action === "file_patch") {
|
|
23305
|
+
if (!op.targetFile || !op.patch) {
|
|
23306
|
+
results.push({ memoId: op.memoId, implId: "", status: "error: file_patch requires targetFile and patch" });
|
|
23307
|
+
continue;
|
|
23308
|
+
}
|
|
23309
|
+
operation = { type: "file_patch", file: op.targetFile, patch: op.patch };
|
|
23310
|
+
writeMarkdownFile(op.targetFile, op.patch);
|
|
23311
|
+
} else {
|
|
23312
|
+
if (!op.targetFile || op.content === void 0) {
|
|
23313
|
+
results.push({ memoId: op.memoId, implId: "", status: "error: file_create requires targetFile and content" });
|
|
23314
|
+
continue;
|
|
23315
|
+
}
|
|
23316
|
+
operation = { type: "file_create", file: op.targetFile, content: op.content };
|
|
23317
|
+
writeMarkdownFile(op.targetFile, op.content);
|
|
23318
|
+
}
|
|
23319
|
+
const impl = {
|
|
23320
|
+
id: `impl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
23321
|
+
memoId: op.memoId,
|
|
23322
|
+
status: "applied",
|
|
23323
|
+
operations: [operation],
|
|
23324
|
+
summary: `${op.action} for ${op.memoId}`,
|
|
23325
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23326
|
+
};
|
|
23327
|
+
parts.impls.push(impl);
|
|
23328
|
+
memo.status = "done";
|
|
23329
|
+
memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23330
|
+
results.push({ memoId: op.memoId, implId: impl.id, status: "applied" });
|
|
23331
|
+
}
|
|
23332
|
+
if (parts.gates.length > 0) {
|
|
23333
|
+
parts.gates = evaluateAllGates(parts.gates, parts.memos);
|
|
23334
|
+
}
|
|
23335
|
+
const resolvedCount = parts.memos.filter((m) => isResolved(m.status)).length;
|
|
23336
|
+
const openMemos = parts.memos.filter((m) => !isResolved(m.status));
|
|
23337
|
+
parts.cursor = {
|
|
23338
|
+
taskId: operations[0]?.memoId || "",
|
|
23339
|
+
step: `${resolvedCount}/${parts.memos.length} resolved`,
|
|
23340
|
+
nextAction: openMemos.length === 0 ? "All annotations resolved \u2014 review complete" : `Resolve: ${openMemos.map((m) => m.id).slice(0, 3).join(", ")}${openMemos.length > 3 ? "..." : ""}`,
|
|
23341
|
+
lastSeenHash: generateBodyHash(parts.body),
|
|
23342
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23343
|
+
};
|
|
23344
|
+
writeTransaction(file, { type: "batch_apply", results, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23345
|
+
const updated = mergeDocument(parts);
|
|
23346
|
+
writeMarkdownFile(file, updated);
|
|
23347
|
+
return {
|
|
23348
|
+
content: [{
|
|
23349
|
+
type: "text",
|
|
23350
|
+
text: JSON.stringify({ results, gatesUpdated: parts.gates.length, cursor: parts.cursor }, null, 2)
|
|
23351
|
+
}]
|
|
23352
|
+
};
|
|
23353
|
+
} catch (err) {
|
|
23354
|
+
return {
|
|
23355
|
+
content: [{
|
|
23356
|
+
type: "text",
|
|
23357
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
23358
|
+
}],
|
|
23359
|
+
isError: true
|
|
23360
|
+
};
|
|
23361
|
+
}
|
|
23362
|
+
}
|
|
23363
|
+
);
|
|
23364
|
+
server2.tool(
|
|
23365
|
+
"get_memo_changes",
|
|
23366
|
+
"Get the implementation history and progress for a memo. Returns all MemoImpl records and progress entries from .md-feedback/progress.json. If memoId is omitted, returns all changes.",
|
|
23367
|
+
{
|
|
23368
|
+
file: external_exports.string().describe("Path to the annotated markdown file"),
|
|
23369
|
+
memoId: external_exports.string().optional().describe("Optional memo ID to filter by \u2014 if omitted, returns all changes")
|
|
23370
|
+
},
|
|
23371
|
+
async ({ file, memoId }) => {
|
|
23372
|
+
try {
|
|
23373
|
+
const markdown = readMarkdownFile(file);
|
|
23374
|
+
const parts = splitDocument(markdown);
|
|
23375
|
+
const impls = memoId ? parts.impls.filter((imp) => imp.memoId === memoId) : parts.impls;
|
|
23376
|
+
const allProgress = readProgress(file);
|
|
23377
|
+
const progress = memoId ? allProgress.filter((p) => p.memoId === memoId) : allProgress;
|
|
23378
|
+
return {
|
|
23379
|
+
content: [{
|
|
23380
|
+
type: "text",
|
|
23381
|
+
text: JSON.stringify({ impls, progress }, null, 2)
|
|
23382
|
+
}]
|
|
23383
|
+
};
|
|
23384
|
+
} catch (err) {
|
|
23385
|
+
return {
|
|
23386
|
+
content: [{
|
|
23387
|
+
type: "text",
|
|
23388
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
|
23389
|
+
}],
|
|
23390
|
+
isError: true
|
|
23391
|
+
};
|
|
23392
|
+
}
|
|
23393
|
+
}
|
|
23394
|
+
);
|
|
22660
23395
|
}
|
|
22661
23396
|
|
|
22662
23397
|
// src/server.ts
|
|
@@ -22666,13 +23401,13 @@ function log(msg) {
|
|
|
22666
23401
|
}
|
|
22667
23402
|
var server = new McpServer({
|
|
22668
23403
|
name: "md-feedback",
|
|
22669
|
-
version: "1.0
|
|
23404
|
+
version: "1.1.0"
|
|
22670
23405
|
});
|
|
22671
23406
|
registerTools(server);
|
|
22672
23407
|
async function main() {
|
|
22673
23408
|
const transport = new StdioServerTransport();
|
|
22674
23409
|
await server.connect(transport);
|
|
22675
|
-
log(`v${"1.0
|
|
23410
|
+
log(`v${"1.1.0"} ready (stdio)`);
|
|
22676
23411
|
}
|
|
22677
23412
|
main().catch((err) => {
|
|
22678
23413
|
log(`fatal: ${err}`);
|
package/package.json
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "md-feedback",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "MCP server for markdown plan review — AI agents read annotations, mark tasks done, evaluate quality gates, and generate session handoffs.
|
|
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.1.0",
|
|
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
|
+
}
|