fapony 0.7.0 → 0.7.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 +144 -156
- package/package.json +5 -6
- package/skill/define-convention/SKILL.md +3 -3
- package/skill/lookup-before-edit/SKILL.md +2 -1
- package/skill/move-to-done/SKILL.md +18 -22
- package/skill/plan-with-pony/SKILL.md +4 -4
- package/skill/review-pony/SKILL.md +10 -12
- package/src/adapters/cli.ts +11 -52
- package/src/adapters/hooks/index.ts +0 -67
- package/src/adapters/hooks/mv-guard.ts +2 -2
- package/src/analyze/discover.ts +1 -1
- package/src/analyze/index.ts +0 -1
- package/src/commands.ts +8 -39
- package/src/core/config.ts +0 -8
- package/src/core/fapony-dir.ts +71 -0
- package/src/core/hook-helpers.ts +2 -11
- package/src/debt/load.ts +3 -5
- package/src/debt/promotion.ts +2 -2
- package/src/debt/scan.ts +1 -1
- package/src/digest/collect.ts +6 -7
- package/src/fael.ts +139 -0
- package/src/hook.ts +1 -81
- package/src/init.ts +30 -90
- package/src/install/antigravity.ts +8 -99
- package/src/install/claude.ts +66 -200
- package/src/install/codex.ts +6 -170
- package/src/install/detect.ts +4 -14
- package/src/install/opencode.ts +50 -486
- package/src/install/types.ts +1 -39
- package/src/install/zcode.ts +5 -91
- package/src/install.ts +9 -35
- package/src/memory.ts +10 -69
- package/src/plan/index.ts +34 -0
- package/src/plan/next.ts +181 -0
- package/src/plan/store.ts +41 -0
- package/src/{mem/commands/plan.ts → plan/sweep.ts} +66 -46
- package/src/seed/plan-seed.ts +10 -9
- package/src/setup.ts +4 -22
- package/src/update.ts +1 -1
- package/templates/PLAN.md +1 -1
- package/src/adapters/hooks/bug-markers.ts +0 -60
- package/src/adapters/hooks/context-data.ts +0 -211
- package/src/adapters/hooks/edit-hint.ts +0 -215
- package/src/adapters/hooks/read-hint.ts +0 -400
- package/src/adapters/hooks/session-start.ts +0 -112
- package/src/adapters/hooks/stop.ts +0 -768
- package/src/adapters/mcp/tools/index.ts +0 -149
- package/src/adapters/mcp/tools/mem.ts +0 -258
- package/src/adapters/mcp/transport.ts +0 -147
- package/src/analyze/cache.ts +0 -162
- package/src/core/mem-log.ts +0 -394
- package/src/init-mem.ts +0 -143
- package/src/install/cursor.ts +0 -167
- package/src/install/utils.ts +0 -29
- package/src/mem/commands/read.ts +0 -792
- package/src/mem/commands/rotate.ts +0 -59
- package/src/mem/commands/where.ts +0 -56
- package/src/mem/commands/write.ts +0 -284
- package/src/mem/engine.ts +0 -329
- package/src/mem/index.ts +0 -201
- package/src/mem/key-registry.ts +0 -116
- package/src/mem/render.ts +0 -66
- package/src/mem/selectors.ts +0 -227
- package/src/mem/store.ts +0 -356
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
// src/mcp/tools/index.ts — barrel + TOOLS array
|
|
2
|
-
//
|
|
3
|
-
// Every schema here is paid as input tokens in *every* session of *every*
|
|
4
|
-
// client that connects, whether or not the tool is called. Adding one is
|
|
5
|
-
// buying attention with a standing charge; earning it back means the tool
|
|
6
|
-
// saves more than it costs (see CLAUDE.md "Spend tokens smart"). Keep
|
|
7
|
-
// descriptions imperative — say what to send, not why it matters.
|
|
8
|
-
|
|
9
|
-
export { toolMemAdd, toolMemClose, toolMemFind } from "./mem.js";
|
|
10
|
-
|
|
11
|
-
// --- Tool definitions ---
|
|
12
|
-
|
|
13
|
-
export const TOOLS = [
|
|
14
|
-
{
|
|
15
|
-
name: "mem_find",
|
|
16
|
-
description:
|
|
17
|
-
"Search the project's mem log (.fapony/.memory/log*.jsonl — decisions, " +
|
|
18
|
-
"bugs, notes, and bookkeeping kinds alike; NO default kind filter). " +
|
|
19
|
-
"Read-only. Answer 'what was ever decided about this file?' in one call " +
|
|
20
|
-
"BEFORE editing: pass files[] (repo-relative). Matches the row's stored " +
|
|
21
|
-
"files[], falling back to a substring of text/spec/ref for rows written " +
|
|
22
|
-
"without it — a row that names the file nowhere cannot be found. " +
|
|
23
|
-
"memDir shows which log dir was resolved (walked up from the given " +
|
|
24
|
-
"worktree — in a monorepo pass the app directory to read its log). " +
|
|
25
|
-
"Returns {rows, total, filesFound, " +
|
|
26
|
-
"skipped, memDir}: memDir:null = no mem at all, not 'nothing matched'. " +
|
|
27
|
-
"A key query that misses adds knownKeys (every key in the log).",
|
|
28
|
-
inputSchema: {
|
|
29
|
-
type: "object" as const,
|
|
30
|
-
properties: {
|
|
31
|
-
worktree: {
|
|
32
|
-
type: "string",
|
|
33
|
-
description:
|
|
34
|
-
"Absolute path (git rev-parse --show-toplevel) — required; " +
|
|
35
|
-
"scope of the mem log to read",
|
|
36
|
-
},
|
|
37
|
-
files: {
|
|
38
|
-
type: "array",
|
|
39
|
-
items: { type: "string" },
|
|
40
|
-
description:
|
|
41
|
-
"Repo-relative paths — matched against the row's files[], else its text",
|
|
42
|
-
},
|
|
43
|
-
text: {
|
|
44
|
-
type: "string",
|
|
45
|
-
description: "Substring, case-insensitive",
|
|
46
|
-
},
|
|
47
|
-
kind: {
|
|
48
|
-
type: "array",
|
|
49
|
-
items: { type: "string" },
|
|
50
|
-
description:
|
|
51
|
-
"Filter by kind (decision/note/bug/close/…). Omit = every kind — " +
|
|
52
|
-
"no default filter",
|
|
53
|
-
},
|
|
54
|
-
since: {
|
|
55
|
-
type: "string",
|
|
56
|
-
description: "ISO date — only rows at or after this time",
|
|
57
|
-
},
|
|
58
|
-
limit: {
|
|
59
|
-
type: "number",
|
|
60
|
-
description:
|
|
61
|
-
"Max rows returned (default 20) — total still counts all matches",
|
|
62
|
-
},
|
|
63
|
-
open: {
|
|
64
|
-
type: "boolean",
|
|
65
|
-
description:
|
|
66
|
-
"true = unresolved work only: drops close/claim/release/synced rows and work rows already closed. Omit = every row. With kind:[bug] answers 'what bugs remain?'",
|
|
67
|
-
},
|
|
68
|
-
key: {
|
|
69
|
-
type: "string",
|
|
70
|
-
description:
|
|
71
|
-
"Exact problem-identity match — miss returns knownKeys (every key in the log). No pattern: a wrong-pattern query must reach the server",
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
required: ["worktree"],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: "mem_add",
|
|
79
|
-
description:
|
|
80
|
-
"Append a mem row (decision/bug/note/next/hold) with files[]. files is " +
|
|
81
|
-
"required — a row that does not name the file is unfindable when you " +
|
|
82
|
-
"touch that file. Returns the row's id, kind, files, and timestamp.",
|
|
83
|
-
inputSchema: {
|
|
84
|
-
type: "object" as const,
|
|
85
|
-
properties: {
|
|
86
|
-
worktree: {
|
|
87
|
-
type: "string",
|
|
88
|
-
description:
|
|
89
|
-
"Absolute path (git rev-parse --show-toplevel) — required",
|
|
90
|
-
},
|
|
91
|
-
kind: {
|
|
92
|
-
type: "string",
|
|
93
|
-
enum: ["next", "bug", "decision", "note", "hold"],
|
|
94
|
-
description:
|
|
95
|
-
"Row kind: decision=locked choice, bug=broken thing, note=state " +
|
|
96
|
-
"next session needs, next=pending work, hold=blocked on spec",
|
|
97
|
-
},
|
|
98
|
-
text: {
|
|
99
|
-
type: "string",
|
|
100
|
-
description:
|
|
101
|
-
"Standalone text — read months later, no conversation context",
|
|
102
|
-
},
|
|
103
|
-
files: {
|
|
104
|
-
type: "array",
|
|
105
|
-
items: { type: "string" },
|
|
106
|
-
description:
|
|
107
|
-
"Repo-relative paths this row is about — required, non-empty",
|
|
108
|
-
},
|
|
109
|
-
spec: {
|
|
110
|
-
type: "string",
|
|
111
|
-
description: "Optional spec/plan .md path",
|
|
112
|
-
},
|
|
113
|
-
key: {
|
|
114
|
-
type: "string",
|
|
115
|
-
pattern: "^[a-z0-9-]{3,40}(:[a-z0-9-]{1,40})?$",
|
|
116
|
-
description:
|
|
117
|
-
"Problem identity (not kind, not files) — same problem, same key; domain:sub (e.g. auth:login) when the repo declares domains",
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
required: ["worktree", "kind", "text", "files"],
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: "mem_close",
|
|
125
|
-
description:
|
|
126
|
-
"Close a mem row by id with a tombstone message (what was done). " +
|
|
127
|
-
"The write half of closing what mem_find shows as open — id must exist. " +
|
|
128
|
-
"Returns the ref and timestamp.",
|
|
129
|
-
inputSchema: {
|
|
130
|
-
type: "object" as const,
|
|
131
|
-
properties: {
|
|
132
|
-
worktree: {
|
|
133
|
-
type: "string",
|
|
134
|
-
description:
|
|
135
|
-
"Absolute path (git rev-parse --show-toplevel) — required",
|
|
136
|
-
},
|
|
137
|
-
id: {
|
|
138
|
-
type: "string",
|
|
139
|
-
description: "The row id to close",
|
|
140
|
-
},
|
|
141
|
-
text: {
|
|
142
|
-
type: "string",
|
|
143
|
-
description: "Tombstone message — what was done (commit sha counts)",
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
required: ["worktree", "id", "text"],
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
];
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
// src/mcp/tools/mem.ts — mem_find (read) + mem_add (write)
|
|
2
|
-
//
|
|
3
|
-
// "writable ≠ readable back" — the write side was CLI-only until now; what was
|
|
4
|
-
// missing is letting an agent append a mem row mid-session without spawning a
|
|
5
|
-
// shell. files[] is required on the write side: a row that does not name the
|
|
6
|
-
// file is unfindable when you touch that file (fill rate 26% CLI-flag vs 88%
|
|
7
|
-
// MCP-required — schema wins over prose every time, see CLAUDE.md rule 9).
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
type EngineAddResult,
|
|
11
|
-
type EngineCloseResult,
|
|
12
|
-
engineAdd,
|
|
13
|
-
engineClose,
|
|
14
|
-
engineFind,
|
|
15
|
-
} from "../../../mem/engine.js";
|
|
16
|
-
import { loadKeyRegistry } from "../../../mem/key-registry.js";
|
|
17
|
-
import { initStore, KINDS, type WorkKind } from "../../../mem/store.js";
|
|
18
|
-
import { type MemRow, readMemLog, resolveMemDir } from "../../../memory.js";
|
|
19
|
-
import { errorResult, jsonResult, type ToolResult } from "../types.js";
|
|
20
|
-
|
|
21
|
-
export interface MemFindResult {
|
|
22
|
-
rows: MemRow[];
|
|
23
|
-
total: number;
|
|
24
|
-
filesFound: number;
|
|
25
|
-
skipped: number;
|
|
26
|
-
memDir: string | null;
|
|
27
|
-
/** Present only when a key query missed — every distinct key in the log. */
|
|
28
|
-
knownKeys?: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function memFind(args: {
|
|
32
|
-
worktree: string;
|
|
33
|
-
files?: string[];
|
|
34
|
-
text?: string;
|
|
35
|
-
kind?: string[];
|
|
36
|
-
since?: string;
|
|
37
|
-
limit?: number;
|
|
38
|
-
open?: boolean;
|
|
39
|
-
key?: string;
|
|
40
|
-
}): MemFindResult {
|
|
41
|
-
// Query logic lives in the shared engine (src/mem/engine.ts) — this wrapper
|
|
42
|
-
// owns only the read (readMemLog sees live + rotated archives via its loose
|
|
43
|
-
// log*.jsonl regex) and the result shape. No kind default here by contract:
|
|
44
|
-
// omitting kind returns every kind (locked by test). CLI cmdFind calls the
|
|
45
|
-
// same engine with its own bookkeeping exclude.
|
|
46
|
-
const read = readMemLog(args.worktree);
|
|
47
|
-
const found = engineFind(read.rows, {
|
|
48
|
-
text: args.text,
|
|
49
|
-
files: args.files,
|
|
50
|
-
kind: args.kind,
|
|
51
|
-
sinceIso: args.since,
|
|
52
|
-
key: args.key,
|
|
53
|
-
limit: args.limit,
|
|
54
|
-
open: args.open,
|
|
55
|
-
});
|
|
56
|
-
return {
|
|
57
|
-
rows: found.rows,
|
|
58
|
-
total: found.total,
|
|
59
|
-
filesFound: read.filesFound,
|
|
60
|
-
skipped: read.skipped,
|
|
61
|
-
// memDir separates "no mem at all" from "nothing matched" (spec §3) and
|
|
62
|
-
// makes the monorepo single-log limit visible (spec §5.1).
|
|
63
|
-
memDir: resolveMemDir(args.worktree),
|
|
64
|
-
...(found.knownKeys ? { knownKeys: found.knownKeys } : {}),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function toolMemFind(args: Record<string, unknown>): ToolResult {
|
|
69
|
-
const worktree =
|
|
70
|
-
typeof args.worktree === "string" ? args.worktree.trim() : "";
|
|
71
|
-
if (!worktree) {
|
|
72
|
-
return errorResult(
|
|
73
|
-
"worktree is required and must be an absolute path " +
|
|
74
|
-
"(git rev-parse --show-toplevel) — an empty result without it would be " +
|
|
75
|
-
"misread as 'no history'",
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
if (!worktree.startsWith("/")) {
|
|
79
|
-
return errorResult(
|
|
80
|
-
`worktree must be an absolute path, got: ${worktree} — a bare name scopes to nothing`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
const files = Array.isArray(args.files)
|
|
84
|
-
? args.files.filter(
|
|
85
|
-
(f): f is string => typeof f === "string" && f.length > 0,
|
|
86
|
-
)
|
|
87
|
-
: undefined;
|
|
88
|
-
const kind = Array.isArray(args.kind)
|
|
89
|
-
? args.kind.filter(
|
|
90
|
-
(k): k is string => typeof k === "string" && k.length > 0,
|
|
91
|
-
)
|
|
92
|
-
: undefined;
|
|
93
|
-
const text = typeof args.text === "string" ? args.text : undefined;
|
|
94
|
-
const since = typeof args.since === "string" ? args.since : undefined;
|
|
95
|
-
if (since !== undefined && Number.isNaN(Date.parse(since))) {
|
|
96
|
-
return errorResult(
|
|
97
|
-
`since must be an ISO date (e.g. 2026-09-19T00:00:00.000Z), got: "${since}" — CLI accepts <N>d/YYYY-MM-DD, MCP takes ISO only`,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
101
|
-
const open = typeof args.open === "boolean" ? args.open : undefined;
|
|
102
|
-
// Shape gate like mem_add — a non-string key must not coerce to undefined
|
|
103
|
-
// (silent drop = reject-without-saying). Pattern is NOT checked here: find
|
|
104
|
-
// answers a wrong-pattern key with knownKeys, not a reject (SPEC fail example).
|
|
105
|
-
// A bare domain doubles as a prefix — key:"auth" catches every auth:*.
|
|
106
|
-
if (args.key !== undefined && typeof args.key !== "string") {
|
|
107
|
-
return errorResult(
|
|
108
|
-
'key must be a string matching [a-z0-9-]{3,40}(:[a-z0-9-]{1,40})? — e.g. "fix-stop-dedupe" or "auth:login"',
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
const key = typeof args.key === "string" ? args.key : undefined;
|
|
112
|
-
|
|
113
|
-
return jsonResult(
|
|
114
|
-
memFind({ worktree, files, text, kind, since, limit, open, key }),
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// --- mem_add ---
|
|
119
|
-
//
|
|
120
|
-
// Domain rules live in the shared engine (src/mem/engine.ts) — this wrapper
|
|
121
|
-
// owns only store init for its worktree. CLI cmdAdd calls the same engine.
|
|
122
|
-
|
|
123
|
-
export type MemAddResult = EngineAddResult;
|
|
124
|
-
|
|
125
|
-
export function memAdd(args: {
|
|
126
|
-
worktree: string;
|
|
127
|
-
kind: string;
|
|
128
|
-
text: string;
|
|
129
|
-
files: string[];
|
|
130
|
-
spec?: string;
|
|
131
|
-
key?: string;
|
|
132
|
-
}): MemAddResult {
|
|
133
|
-
initStore(args.worktree);
|
|
134
|
-
// Registry beside the mem log; absent = free-form keys (never reject).
|
|
135
|
-
// Loaded per call, not at MCP start (initialize budget ≤ ~100ms).
|
|
136
|
-
const reg = loadKeyRegistry(args.worktree);
|
|
137
|
-
return engineAdd({
|
|
138
|
-
kind: args.kind,
|
|
139
|
-
text: args.text,
|
|
140
|
-
files: args.files,
|
|
141
|
-
spec: args.spec,
|
|
142
|
-
key: args.key,
|
|
143
|
-
knownDomains: reg.path ? reg.domains : null,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function toolMemAdd(args: Record<string, unknown>): ToolResult {
|
|
148
|
-
const worktree =
|
|
149
|
-
typeof args.worktree === "string" ? args.worktree.trim() : "";
|
|
150
|
-
if (!worktree) {
|
|
151
|
-
return errorResult(
|
|
152
|
-
"worktree is required and must be an absolute path " +
|
|
153
|
-
"(git rev-parse --show-toplevel)",
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
if (!worktree.startsWith("/")) {
|
|
157
|
-
return errorResult(`worktree must be an absolute path, got: ${worktree}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const kind = typeof args.kind === "string" ? args.kind.trim() : "";
|
|
161
|
-
if (!kind) {
|
|
162
|
-
return errorResult(`kind is required — one of ${KINDS.join("|")}`);
|
|
163
|
-
}
|
|
164
|
-
if (!KINDS.includes(kind as WorkKind)) {
|
|
165
|
-
return errorResult(
|
|
166
|
-
`kind must be one of ${KINDS.join("|")} — got "${kind}"`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
171
|
-
if (!text) {
|
|
172
|
-
return errorResult("text is required and must not be empty");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const files = Array.isArray(args.files)
|
|
176
|
-
? args.files
|
|
177
|
-
.filter((f): f is string => typeof f === "string" && f.length > 0)
|
|
178
|
-
.map((f) => f.trim().replace(/^\.\//, ""))
|
|
179
|
-
: [];
|
|
180
|
-
if (files.length === 0) {
|
|
181
|
-
return errorResult(
|
|
182
|
-
"files is required and must contain at least one repo-relative path",
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const spec =
|
|
187
|
-
typeof args.spec === "string" && args.spec.trim().endsWith(".md")
|
|
188
|
-
? args.spec.trim()
|
|
189
|
-
: undefined;
|
|
190
|
-
|
|
191
|
-
// Shape gate here, pattern check in engineAdd — a non-string key must not
|
|
192
|
-
// slip through as undefined (silent drop = reject-without-saying, SPEC §Validation).
|
|
193
|
-
if (args.key !== undefined && typeof args.key !== "string") {
|
|
194
|
-
return errorResult(
|
|
195
|
-
'key must be a string matching [a-z0-9-]{3,40}(:[a-z0-9-]{3,40})? — e.g. "fix-stop-dedupe" or "auth:login"',
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
const key = typeof args.key === "string" ? args.key : undefined;
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
const result = memAdd({ worktree, kind, text, files, spec, key });
|
|
202
|
-
return jsonResult(result);
|
|
203
|
-
} catch (e) {
|
|
204
|
-
return errorResult(e instanceof Error ? e.message : String(e));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// --- mem_close ---
|
|
209
|
-
//
|
|
210
|
-
// A separate tool on purpose, not kind:"close" inside mem_add: a close row
|
|
211
|
-
// carries {ref, text} with no files[] and no spec, while mem_add requires a
|
|
212
|
-
// non-empty files[] (and a spec for hold) — folding them into one schema
|
|
213
|
-
// would make required fields depend on the value of another field, the shape
|
|
214
|
-
// models call wrong most often. Mirrors CLI `mem close <id> "<msg>"`
|
|
215
|
-
// (commands/write.ts cmdClose): the id must exist; the tombstone voids the
|
|
216
|
-
// claim by itself.
|
|
217
|
-
|
|
218
|
-
export type MemCloseResult = EngineCloseResult;
|
|
219
|
-
|
|
220
|
-
export function memClose(args: {
|
|
221
|
-
worktree: string;
|
|
222
|
-
id: string;
|
|
223
|
-
text: string;
|
|
224
|
-
}): MemCloseResult {
|
|
225
|
-
initStore(args.worktree);
|
|
226
|
-
return engineClose({ id: args.id, text: args.text });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function toolMemClose(args: Record<string, unknown>): ToolResult {
|
|
230
|
-
const worktree =
|
|
231
|
-
typeof args.worktree === "string" ? args.worktree.trim() : "";
|
|
232
|
-
if (!worktree) {
|
|
233
|
-
return errorResult(
|
|
234
|
-
"worktree is required and must be an absolute path " +
|
|
235
|
-
"(git rev-parse --show-toplevel)",
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
if (!worktree.startsWith("/")) {
|
|
239
|
-
return errorResult(`worktree must be an absolute path, got: ${worktree}`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
243
|
-
if (!id) {
|
|
244
|
-
return errorResult("id is required — the row id to close");
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
248
|
-
if (!text) {
|
|
249
|
-
return errorResult("text is required and must not be empty");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
const result = memClose({ worktree, id, text });
|
|
254
|
-
return jsonResult(result);
|
|
255
|
-
} catch (e) {
|
|
256
|
-
return errorResult(e instanceof Error ? e.message : String(e));
|
|
257
|
-
}
|
|
258
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// src/mcp/transport.ts — JSON-RPC dispatch + stdio entry point
|
|
2
|
-
|
|
3
|
-
import { createInterface } from "node:readline";
|
|
4
|
-
import { getServerSha } from "./primitives.js";
|
|
5
|
-
import { TOOLS, toolMemAdd, toolMemClose, toolMemFind } from "./tools/index.js";
|
|
6
|
-
import { errorResult, type ToolResult } from "./types.js";
|
|
7
|
-
|
|
8
|
-
// --- Server instructions ---
|
|
9
|
-
//
|
|
10
|
-
// MCP's initialize response carries an `instructions` string that clients
|
|
11
|
-
// inject into the model's context. This is the vendor-neutral place for the
|
|
12
|
-
// habit fapony depends on — a user should never have to paste rules
|
|
13
|
-
// into their own CLAUDE.md (or AGENTS.md, or a hook) to make the tools work,
|
|
14
|
-
// and a rule pasted there would only cover one client anyway.
|
|
15
|
-
//
|
|
16
|
-
// Kept short on purpose: this text is spent on every session of every user.
|
|
17
|
-
// Both habits degrade silently — an agent that ignores them still gets
|
|
18
|
-
// correct answers from every tool, just a thinner history.
|
|
19
|
-
|
|
20
|
-
const SERVER_INSTRUCTIONS = `fapony records decisions, bugs, and notes about this project so the next session (or the next agent) knows what happened and what to watch out for.
|
|
21
|
-
|
|
22
|
-
When you finish a unit of work, record a mem row: fapony mem add <decision|bug|note> "what happened" --files <files> <path/to/PLAN.md>. files[] is required — a row without it is unfindable when you touch that file next session. Name a known problem with --key <domain:sub> and recall every row for it via mem_find key.
|
|
23
|
-
|
|
24
|
-
worktree must be the absolute path (git rev-parse --show-toplevel): every query scopes by it, so a bare name or none files the row where nothing reads it, and nothing errors to say so. Write the note standalone — it is read months later with no access to this conversation.
|
|
25
|
-
|
|
26
|
-
Skip it and every tool still answers correctly, on a thinner history.`;
|
|
27
|
-
|
|
28
|
-
// --- MCP protocol constants ---
|
|
29
|
-
|
|
30
|
-
const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
31
|
-
const SERVER_NAME = "fapony-handcheck";
|
|
32
|
-
const SERVER_VERSION = "0.3.0";
|
|
33
|
-
|
|
34
|
-
// --- JSON-RPC dispatch ---
|
|
35
|
-
|
|
36
|
-
export function dispatch(
|
|
37
|
-
method: string,
|
|
38
|
-
params: unknown,
|
|
39
|
-
): object | ToolResult | null {
|
|
40
|
-
switch (method) {
|
|
41
|
-
case "initialize":
|
|
42
|
-
return {
|
|
43
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
44
|
-
capabilities: { tools: {} },
|
|
45
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
46
|
-
instructions: SERVER_INSTRUCTIONS,
|
|
47
|
-
};
|
|
48
|
-
case "notifications/initialized":
|
|
49
|
-
return null; // no response needed
|
|
50
|
-
case "tools/list":
|
|
51
|
-
return { tools: TOOLS };
|
|
52
|
-
case "tools/call":
|
|
53
|
-
return dispatchToolCall(
|
|
54
|
-
params as { name: string; arguments?: Record<string, unknown> },
|
|
55
|
-
);
|
|
56
|
-
default:
|
|
57
|
-
return {
|
|
58
|
-
code: -32601,
|
|
59
|
-
message: `method not found: ${method}`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function dispatchToolCall(params: {
|
|
65
|
-
name: string;
|
|
66
|
-
arguments?: Record<string, unknown>;
|
|
67
|
-
}): ToolResult {
|
|
68
|
-
const args = params.arguments ?? {};
|
|
69
|
-
let result: ToolResult;
|
|
70
|
-
switch (params.name) {
|
|
71
|
-
case "mem_find":
|
|
72
|
-
result = toolMemFind(args);
|
|
73
|
-
break;
|
|
74
|
-
case "mem_add":
|
|
75
|
-
result = toolMemAdd(args);
|
|
76
|
-
break;
|
|
77
|
-
case "mem_close":
|
|
78
|
-
result = toolMemClose(args);
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
81
|
-
return errorResult(`unknown tool: ${params.name}`);
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// --- Entry point ---
|
|
87
|
-
|
|
88
|
-
export function cmdMcp(): void {
|
|
89
|
-
getServerSha(); // cache while the process is still fresh, not on first report call
|
|
90
|
-
const rl = createInterface({ input: process.stdin });
|
|
91
|
-
|
|
92
|
-
rl.on("line", (line) => {
|
|
93
|
-
const trimmed = line.trim();
|
|
94
|
-
if (!trimmed) return;
|
|
95
|
-
|
|
96
|
-
let msg: { id?: number; method: string; params?: unknown };
|
|
97
|
-
try {
|
|
98
|
-
msg = JSON.parse(trimmed);
|
|
99
|
-
} catch {
|
|
100
|
-
// Invalid JSON — send error
|
|
101
|
-
const resp = {
|
|
102
|
-
jsonrpc: "2.0",
|
|
103
|
-
id: null,
|
|
104
|
-
error: { code: -32700, message: "Parse error" },
|
|
105
|
-
};
|
|
106
|
-
process.stdout.write(`${JSON.stringify(resp)}\n`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const result = dispatch(msg.method, msg.params ?? {});
|
|
111
|
-
|
|
112
|
-
// notifications don't get a response
|
|
113
|
-
if (result === null) return;
|
|
114
|
-
|
|
115
|
-
const resp: Record<string, unknown> = {
|
|
116
|
-
jsonrpc: "2.0",
|
|
117
|
-
id: msg.id ?? null,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
result &&
|
|
122
|
-
typeof result === "object" &&
|
|
123
|
-
"content" in result &&
|
|
124
|
-
"isError" in result
|
|
125
|
-
) {
|
|
126
|
-
// Tool result
|
|
127
|
-
resp.result = result;
|
|
128
|
-
} else if (
|
|
129
|
-
result &&
|
|
130
|
-
typeof result === "object" &&
|
|
131
|
-
"code" in result &&
|
|
132
|
-
"message" in result
|
|
133
|
-
) {
|
|
134
|
-
// Error response
|
|
135
|
-
resp.error = result;
|
|
136
|
-
} else {
|
|
137
|
-
// Normal result
|
|
138
|
-
resp.result = result;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
process.stdout.write(`${JSON.stringify(resp)}\n`);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
rl.on("close", () => {
|
|
145
|
-
process.exit(0);
|
|
146
|
-
});
|
|
147
|
-
}
|