@rethunk/mcp-multi-root-git 1.0.0 → 2.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/AGENTS.md +37 -31
- package/HUMANS.md +8 -0
- package/README.md +3 -8
- package/dist/repo-paths.js +1 -1
- package/dist/server/batch-commit-tool.js +139 -0
- package/dist/server/git-diff-summary-tool.js +319 -0
- package/dist/server/git-inventory-tool.js +32 -84
- package/dist/server/git-log-tool.js +276 -0
- package/dist/server/git-parity-tool.js +2 -5
- package/dist/server/git-status-tool.js +11 -10
- package/dist/server/git.js +6 -27
- package/dist/server/inventory.js +59 -82
- package/dist/server/json.js +2 -9
- package/dist/server/list-presets-tool.js +1 -1
- package/dist/server/presets.js +10 -9
- package/dist/server/roots.js +12 -26
- package/dist/server/schemas.js +4 -16
- package/dist/server/test-harness.js +64 -0
- package/dist/server/tools.js +6 -0
- package/docs/mcp-tools.md +173 -12
- package/package.json +1 -1
package/dist/server/inventory.js
CHANGED
|
@@ -6,48 +6,52 @@ export function validateRepoPath(rel, gitTop) {
|
|
|
6
6
|
return { abs, underTop: assertRelativePathUnderTop(rel, abs, gitTop) };
|
|
7
7
|
}
|
|
8
8
|
export function makeSkipEntry(label, abs, upstreamMode, skipReason) {
|
|
9
|
-
return {
|
|
10
|
-
label,
|
|
11
|
-
path: abs,
|
|
12
|
-
branchStatus: "",
|
|
13
|
-
shortStatus: "",
|
|
14
|
-
detached: false,
|
|
15
|
-
headAbbrev: "",
|
|
16
|
-
upstreamMode,
|
|
17
|
-
upstreamRef: null,
|
|
18
|
-
ahead: null,
|
|
19
|
-
behind: null,
|
|
20
|
-
upstreamNote: "",
|
|
21
|
-
skipReason,
|
|
22
|
-
};
|
|
9
|
+
return { label, path: abs, upstreamMode, skipReason };
|
|
23
10
|
}
|
|
24
11
|
export function buildInventorySectionMarkdown(e) {
|
|
12
|
+
const header = `## ${e.label} — ${e.path}`;
|
|
25
13
|
if (e.skipReason) {
|
|
26
|
-
return [
|
|
27
|
-
}
|
|
28
|
-
const lines = [];
|
|
29
|
-
lines.push(e.branchStatus);
|
|
30
|
-
lines.push("");
|
|
31
|
-
lines.push("short:");
|
|
32
|
-
lines.push(e.shortStatus || "(clean)");
|
|
33
|
-
lines.push("");
|
|
34
|
-
if (e.detached) {
|
|
35
|
-
lines.push("branch: (detached HEAD)");
|
|
36
|
-
lines.push("");
|
|
14
|
+
return ["", header, e.skipReason];
|
|
37
15
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
lines.push(
|
|
16
|
+
const lines = [e.branchStatus || "(clean)"];
|
|
17
|
+
if (e.detached)
|
|
18
|
+
lines.push("detached HEAD");
|
|
19
|
+
if (e.ahead !== undefined && e.behind !== undefined && e.upstreamRef) {
|
|
20
|
+
lines.push(`${e.upstreamRef}: ahead ${e.ahead}, behind ${e.behind}`);
|
|
41
21
|
}
|
|
42
|
-
else {
|
|
22
|
+
else if (e.upstreamNote) {
|
|
43
23
|
lines.push(`upstream: ${e.upstreamNote}`);
|
|
44
24
|
}
|
|
45
|
-
|
|
25
|
+
const single = lines.length === 1 ? lines[0] : undefined;
|
|
26
|
+
if (single !== undefined && !single.includes("\n")) {
|
|
27
|
+
return ["", header, single];
|
|
28
|
+
}
|
|
29
|
+
return ["", header, "```text", lines.join("\n"), "```"];
|
|
46
30
|
}
|
|
47
|
-
function upstreamNoteFor(ref,
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
function upstreamNoteFor(ref, hasCounts) {
|
|
32
|
+
return hasCounts ? `tracking ${ref}` : `upstream ${ref} (counts unreadable)`;
|
|
33
|
+
}
|
|
34
|
+
function buildEntry(params) {
|
|
35
|
+
const out = {
|
|
36
|
+
label: params.label,
|
|
37
|
+
path: params.absPath,
|
|
38
|
+
upstreamMode: params.upstreamMode,
|
|
39
|
+
};
|
|
40
|
+
if (params.branchStatus)
|
|
41
|
+
out.branchStatus = params.branchStatus;
|
|
42
|
+
if (params.detached)
|
|
43
|
+
out.detached = true;
|
|
44
|
+
if (params.headAbbrev)
|
|
45
|
+
out.headAbbrev = params.headAbbrev;
|
|
46
|
+
if (params.upstreamRef !== null)
|
|
47
|
+
out.upstreamRef = params.upstreamRef;
|
|
48
|
+
if (params.ahead !== null)
|
|
49
|
+
out.ahead = params.ahead;
|
|
50
|
+
if (params.behind !== null)
|
|
51
|
+
out.behind = params.behind;
|
|
52
|
+
if (params.upstreamNote)
|
|
53
|
+
out.upstreamNote = params.upstreamNote;
|
|
54
|
+
return out;
|
|
51
55
|
}
|
|
52
56
|
export async function collectInventoryEntry(label, absPath, fixedRemote, fixedBranch) {
|
|
53
57
|
const [snap, headR] = await Promise.all([
|
|
@@ -55,79 +59,52 @@ export async function collectInventoryEntry(label, absPath, fixedRemote, fixedBr
|
|
|
55
59
|
spawnGitAsync(absPath, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
56
60
|
]);
|
|
57
61
|
const branchStatus = snap.branchLine;
|
|
58
|
-
const shortStatus = snap.shortLine;
|
|
59
62
|
const headAbbrev = headR.ok ? headR.stdout.trim() : "";
|
|
60
63
|
const detached = !headR.ok || headAbbrev === "HEAD" || headAbbrev.endsWith("/HEAD");
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const verify = await spawnGitAsync(absPath, ["rev-parse", "--verify", `${remote}/${branch}`]);
|
|
64
|
+
const base = { label, absPath, branchStatus, detached, headAbbrev };
|
|
65
|
+
if (fixedRemote !== undefined && fixedBranch !== undefined) {
|
|
66
|
+
const ref = `${fixedRemote}/${fixedBranch}`;
|
|
67
|
+
const verify = await spawnGitAsync(absPath, ["rev-parse", "--verify", ref]);
|
|
66
68
|
if (!verify.ok) {
|
|
67
|
-
return {
|
|
68
|
-
|
|
69
|
-
path: absPath,
|
|
70
|
-
branchStatus,
|
|
71
|
-
shortStatus,
|
|
72
|
-
detached,
|
|
73
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
69
|
+
return buildEntry({
|
|
70
|
+
...base,
|
|
74
71
|
upstreamMode: "fixed",
|
|
75
|
-
upstreamRef:
|
|
72
|
+
upstreamRef: ref,
|
|
76
73
|
ahead: null,
|
|
77
74
|
behind: null,
|
|
78
|
-
upstreamNote: `(no local ref ${
|
|
79
|
-
};
|
|
75
|
+
upstreamNote: `(no local ref ${ref} or unreadable)`,
|
|
76
|
+
});
|
|
80
77
|
}
|
|
81
|
-
const ref = `${remote}/${branch}`;
|
|
82
78
|
const { ahead, behind } = await fetchAheadBehind(absPath, ref);
|
|
83
|
-
return {
|
|
84
|
-
|
|
85
|
-
path: absPath,
|
|
86
|
-
branchStatus,
|
|
87
|
-
shortStatus,
|
|
88
|
-
detached,
|
|
89
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
79
|
+
return buildEntry({
|
|
80
|
+
...base,
|
|
90
81
|
upstreamMode: "fixed",
|
|
91
82
|
upstreamRef: ref,
|
|
92
83
|
ahead,
|
|
93
84
|
behind,
|
|
94
|
-
upstreamNote: upstreamNoteFor(ref, ahead
|
|
95
|
-
};
|
|
85
|
+
upstreamNote: upstreamNoteFor(ref, ahead != null && behind != null),
|
|
86
|
+
});
|
|
96
87
|
}
|
|
97
88
|
const upVerify = await spawnGitAsync(absPath, ["rev-parse", "--verify", "@{u}"]);
|
|
98
89
|
if (!upVerify.ok) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
note = "detached HEAD — no upstream";
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
label,
|
|
105
|
-
path: absPath,
|
|
106
|
-
branchStatus,
|
|
107
|
-
shortStatus,
|
|
108
|
-
detached,
|
|
109
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
90
|
+
return buildEntry({
|
|
91
|
+
...base,
|
|
110
92
|
upstreamMode: "auto",
|
|
111
93
|
upstreamRef: null,
|
|
112
94
|
ahead: null,
|
|
113
95
|
behind: null,
|
|
114
|
-
upstreamNote:
|
|
115
|
-
};
|
|
96
|
+
upstreamNote: detached ? "detached HEAD — no upstream" : "no upstream configured",
|
|
97
|
+
});
|
|
116
98
|
}
|
|
117
99
|
const abbrevR = await spawnGitAsync(absPath, ["rev-parse", "--abbrev-ref", "@{u}"]);
|
|
118
100
|
const upstreamRef = abbrevR.ok ? abbrevR.stdout.trim() : "@{u}";
|
|
119
101
|
const { ahead, behind } = await fetchAheadBehind(absPath, "@{u}");
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
path: absPath,
|
|
123
|
-
branchStatus,
|
|
124
|
-
shortStatus,
|
|
125
|
-
detached,
|
|
126
|
-
headAbbrev: headAbbrev || "(unknown)",
|
|
102
|
+
return buildEntry({
|
|
103
|
+
...base,
|
|
127
104
|
upstreamMode: "auto",
|
|
128
105
|
upstreamRef,
|
|
129
106
|
ahead,
|
|
130
107
|
behind,
|
|
131
|
-
upstreamNote: upstreamNoteFor(upstreamRef, ahead
|
|
132
|
-
};
|
|
108
|
+
upstreamNote: upstreamNoteFor(upstreamRef, ahead != null && behind != null),
|
|
109
|
+
});
|
|
133
110
|
}
|
package/dist/server/json.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
export function readPackageVersion() {
|
|
4
|
+
function readPackageVersion() {
|
|
6
5
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
6
|
const pkgPath = join(here, "..", "..", "package.json");
|
|
8
7
|
try {
|
|
@@ -23,13 +22,7 @@ export function readMcpServerVersion() {
|
|
|
23
22
|
return "0.0.0";
|
|
24
23
|
}
|
|
25
24
|
export function jsonRespond(body) {
|
|
26
|
-
return JSON.stringify(
|
|
27
|
-
...body,
|
|
28
|
-
rethunkGitMcp: {
|
|
29
|
-
jsonFormatVersion: MCP_JSON_FORMAT_VERSION,
|
|
30
|
-
packageVersion: readPackageVersion(),
|
|
31
|
-
},
|
|
32
|
-
}, null, 2);
|
|
25
|
+
return JSON.stringify(body);
|
|
33
26
|
}
|
|
34
27
|
/** Spread into an object literal only when `cond` is true; otherwise `{}`. */
|
|
35
28
|
export function spreadWhen(cond, fields) {
|
|
@@ -7,7 +7,7 @@ import { WorkspacePickSchema } from "./schemas.js";
|
|
|
7
7
|
export function registerListPresetsTool(server) {
|
|
8
8
|
server.addTool({
|
|
9
9
|
name: "list_presets",
|
|
10
|
-
description: "List
|
|
10
|
+
description: "List presets from .rethunk/git-mcp-presets.json. See docs/mcp-tools.md.",
|
|
11
11
|
parameters: WorkspacePickSchema.pick({
|
|
12
12
|
workspaceRoot: true,
|
|
13
13
|
rootIndex: true,
|
package/dist/server/presets.js
CHANGED
|
@@ -24,7 +24,7 @@ export const PRESET_FILE_PATH = ".rethunk/git-mcp-presets.json";
|
|
|
24
24
|
* - Wrapped: `{ "schemaVersion": "1", "presets": { "name": { ... } } }`
|
|
25
25
|
* - Legacy: `{ "name": { ... }, ... }` with optional top-level `schemaVersion` / `$schema` (editor hints).
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
function splitPresetFileRaw(raw) {
|
|
28
28
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
29
29
|
throw new Error("invalid_root");
|
|
30
30
|
}
|
|
@@ -96,24 +96,25 @@ export function presetLoadErrorPayload(gitTop, fail) {
|
|
|
96
96
|
}
|
|
97
97
|
return { error: "preset_file_invalid", presetFile };
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
function getPresetEntry(gitTop, presetName) {
|
|
100
100
|
const loaded = loadPresetsFromGitTop(gitTop);
|
|
101
101
|
if (!loaded.ok) {
|
|
102
102
|
if (loaded.reason === "missing") {
|
|
103
103
|
return {
|
|
104
|
+
ok: false,
|
|
104
105
|
error: {
|
|
105
106
|
error: "preset_not_found",
|
|
106
107
|
preset: presetName,
|
|
107
108
|
presetFile: join(gitTop, PRESET_FILE_PATH),
|
|
108
|
-
message: "Preset file missing",
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
-
return { error: presetLoadErrorPayload(gitTop, loaded) };
|
|
112
|
+
return { ok: false, error: presetLoadErrorPayload(gitTop, loaded) };
|
|
113
113
|
}
|
|
114
114
|
const entry = loaded.data[presetName];
|
|
115
115
|
if (!entry) {
|
|
116
116
|
return {
|
|
117
|
+
ok: false,
|
|
117
118
|
error: {
|
|
118
119
|
error: "preset_not_found",
|
|
119
120
|
preset: presetName,
|
|
@@ -121,9 +122,9 @@ export function getPresetEntry(gitTop, presetName) {
|
|
|
121
122
|
},
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
|
-
return { entry, presetSchemaVersion: loaded.schemaVersion };
|
|
125
|
+
return { ok: true, entry, presetSchemaVersion: loaded.schemaVersion };
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
+
function mergeNestedRoots(preset, inline) {
|
|
127
128
|
const a = preset ?? [];
|
|
128
129
|
const b = inline ?? [];
|
|
129
130
|
if (a.length === 0 && b.length === 0)
|
|
@@ -138,7 +139,7 @@ export function mergeNestedRoots(preset, inline) {
|
|
|
138
139
|
}
|
|
139
140
|
return out;
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
+
function mergePairs(preset, inline) {
|
|
142
143
|
const a = preset ?? [];
|
|
143
144
|
const b = inline ?? [];
|
|
144
145
|
if (a.length === 0 && b.length === 0)
|
|
@@ -147,7 +148,7 @@ export function mergePairs(preset, inline) {
|
|
|
147
148
|
}
|
|
148
149
|
export function applyPresetNestedRoots(gitTop, presetName, presetMerge, inlineNestedRoots) {
|
|
149
150
|
const got = getPresetEntry(gitTop, presetName);
|
|
150
|
-
if (
|
|
151
|
+
if (!got.ok)
|
|
151
152
|
return { ok: false, error: got.error };
|
|
152
153
|
const fromPreset = got.entry.nestedRoots;
|
|
153
154
|
let nestedRoots = inlineNestedRoots;
|
|
@@ -161,7 +162,7 @@ export function applyPresetNestedRoots(gitTop, presetName, presetMerge, inlineNe
|
|
|
161
162
|
}
|
|
162
163
|
export function applyPresetParityPairs(gitTop, presetName, presetMerge, inlinePairs) {
|
|
163
164
|
const got = getPresetEntry(gitTop, presetName);
|
|
164
|
-
if (
|
|
165
|
+
if (!got.ok)
|
|
165
166
|
return { ok: false, error: got.error };
|
|
166
167
|
const fromPreset = got.entry.parityPairs;
|
|
167
168
|
let pairs = inlinePairs;
|
package/dist/server/roots.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { basename,
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { gateGit, gitTopLevel } from "./git.js";
|
|
4
4
|
import { loadPresetsFromGitTop, presetLoadErrorPayload } from "./presets.js";
|
|
5
|
-
|
|
5
|
+
function uriToPath(uri) {
|
|
6
6
|
if (!uri.startsWith("file://"))
|
|
7
7
|
return null;
|
|
8
8
|
try {
|
|
@@ -12,7 +12,7 @@ export function uriToPath(uri) {
|
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
function listFileRoots(server) {
|
|
16
16
|
const sessions = server.sessions;
|
|
17
17
|
const roots = sessions[0]?.roots ?? [];
|
|
18
18
|
const paths = [];
|
|
@@ -24,40 +24,29 @@ export function listFileRoots(server) {
|
|
|
24
24
|
return paths;
|
|
25
25
|
}
|
|
26
26
|
/** Basename or trailing path segment; compares using normalized slashes so Windows backslashes match. */
|
|
27
|
-
|
|
27
|
+
function pathMatchesWorkspaceRootHint(rootPath, hint) {
|
|
28
28
|
const h = hint.trim();
|
|
29
29
|
if (!h)
|
|
30
30
|
return true;
|
|
31
|
-
if (basename(rootPath) === h)
|
|
32
|
-
return true;
|
|
33
31
|
const absRoot = resolve(rootPath);
|
|
34
|
-
if (absRoot === h)
|
|
35
|
-
return true;
|
|
36
|
-
try {
|
|
37
|
-
if (isAbsolute(h) && resolve(h) === absRoot)
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
/* invalid absolute hint */
|
|
42
|
-
}
|
|
43
32
|
const normRoot = absRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
44
33
|
const normHint = h.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\/+/, "");
|
|
45
34
|
if (!normHint)
|
|
46
35
|
return true;
|
|
47
36
|
if (normRoot === normHint)
|
|
48
37
|
return true;
|
|
49
|
-
|
|
38
|
+
if (normRoot.endsWith(`/${normHint}`))
|
|
39
|
+
return true;
|
|
40
|
+
return basename(rootPath) === h;
|
|
50
41
|
}
|
|
51
|
-
|
|
42
|
+
function resolveWorkspaceRoots(server, args) {
|
|
52
43
|
if (args.workspaceRoot?.trim()) {
|
|
53
44
|
return { ok: true, roots: [resolve(args.workspaceRoot.trim())] };
|
|
54
45
|
}
|
|
55
46
|
const fileRoots = listFileRoots(server);
|
|
47
|
+
const fallback = { ok: true, roots: [process.cwd()] };
|
|
56
48
|
if (args.allWorkspaceRoots) {
|
|
57
|
-
|
|
58
|
-
return { ok: true, roots: [process.cwd()] };
|
|
59
|
-
}
|
|
60
|
-
return { ok: true, roots: fileRoots };
|
|
49
|
+
return fileRoots.length === 0 ? fallback : { ok: true, roots: fileRoots };
|
|
61
50
|
}
|
|
62
51
|
if (args.rootIndex != null) {
|
|
63
52
|
const r = fileRoots[args.rootIndex];
|
|
@@ -74,16 +63,13 @@ export function resolveWorkspaceRoots(server, args) {
|
|
|
74
63
|
return { ok: true, roots: [r] };
|
|
75
64
|
}
|
|
76
65
|
const primary = fileRoots[0];
|
|
77
|
-
|
|
78
|
-
return { ok: true, roots: [primary] };
|
|
79
|
-
}
|
|
80
|
-
return { ok: true, roots: [process.cwd()] };
|
|
66
|
+
return primary !== undefined ? { ok: true, roots: [primary] } : fallback;
|
|
81
67
|
}
|
|
82
68
|
/**
|
|
83
69
|
* When a preset name is requested and multiple MCP roots exist, pick the first root
|
|
84
70
|
* whose git toplevel loads a preset file containing that name.
|
|
85
71
|
*/
|
|
86
|
-
|
|
72
|
+
function resolveRootsForPreset(server, args, presetName) {
|
|
87
73
|
if (args.workspaceRoot?.trim() || args.allWorkspaceRoots || args.rootIndex != null) {
|
|
88
74
|
return resolveWorkspaceRoots(server, args);
|
|
89
75
|
}
|
package/dist/server/schemas.js
CHANGED
|
@@ -2,21 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { MAX_INVENTORY_ROOTS_DEFAULT } from "./inventory.js";
|
|
3
3
|
const FormatSchema = z.enum(["markdown", "json"]).optional().default("markdown");
|
|
4
4
|
export const WorkspacePickSchema = z.object({
|
|
5
|
-
workspaceRoot: z
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
rootIndex: z
|
|
10
|
-
.number()
|
|
11
|
-
.int()
|
|
12
|
-
.min(0)
|
|
13
|
-
.optional()
|
|
14
|
-
.describe("Use the Nth file:// MCP root (0-based) when multiple workspace roots exist."),
|
|
15
|
-
allWorkspaceRoots: z
|
|
16
|
-
.boolean()
|
|
17
|
-
.optional()
|
|
18
|
-
.default(false)
|
|
19
|
-
.describe("Run against every file:// MCP root and aggregate results."),
|
|
20
|
-
format: FormatSchema.describe('Return "markdown" (default) or structured "json".'),
|
|
5
|
+
workspaceRoot: z.string().optional().describe("Highest-priority override."),
|
|
6
|
+
rootIndex: z.number().int().min(0).optional(),
|
|
7
|
+
allWorkspaceRoots: z.boolean().optional().default(false),
|
|
8
|
+
format: FormatSchema,
|
|
21
9
|
});
|
|
22
10
|
export { MAX_INVENTORY_ROOTS_DEFAULT };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight test harness for MCP tool execute handlers.
|
|
3
|
+
*
|
|
4
|
+
* FastMCP does not expose a way to inject a custom transport, so the full
|
|
5
|
+
* MCP client/server stack cannot be wired up in tests without stdio or HTTP.
|
|
6
|
+
* Instead, we use a duck-typed fake server that satisfies the FastMCP interface
|
|
7
|
+
* just enough for tool registration: it has `sessions` (empty — tools use
|
|
8
|
+
* `workspaceRoot` arg which bypasses session root detection) and `addTool`
|
|
9
|
+
* which captures the tool definition so we can call `execute` directly.
|
|
10
|
+
*
|
|
11
|
+
* Context passed to execute is a no-op stub — none of the current tools
|
|
12
|
+
* use the context object (logging, progress, etc.).
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* const tool = captureTool(registerBatchCommitTool);
|
|
16
|
+
* const result = await tool({ workspaceRoot: dir, commits: [...] });
|
|
17
|
+
* // result is string (markdown) or JSON-parseable string
|
|
18
|
+
*/
|
|
19
|
+
// Stub context — no tool currently uses context
|
|
20
|
+
const STUB_CONTEXT = {
|
|
21
|
+
log: {
|
|
22
|
+
debug: () => undefined,
|
|
23
|
+
error: () => undefined,
|
|
24
|
+
info: () => undefined,
|
|
25
|
+
warn: () => undefined,
|
|
26
|
+
},
|
|
27
|
+
reportProgress: async () => undefined,
|
|
28
|
+
session: undefined,
|
|
29
|
+
};
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Fake server
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
function makeFakeServer() {
|
|
34
|
+
const tools = [];
|
|
35
|
+
const server = {
|
|
36
|
+
sessions: [],
|
|
37
|
+
addTool(tool) {
|
|
38
|
+
tools.push({ name: tool.name, execute: tool.execute });
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return { server, tools };
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Public API
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Register one tool and return a caller that invokes its execute handler.
|
|
48
|
+
* The returned function accepts tool args (always include `workspaceRoot`)
|
|
49
|
+
* and returns the raw result as a string.
|
|
50
|
+
*/
|
|
51
|
+
export function captureTool(register, toolName) {
|
|
52
|
+
const { server, tools } = makeFakeServer();
|
|
53
|
+
register(server);
|
|
54
|
+
const pick = toolName ? tools.find((t) => t.name === toolName) : tools[0];
|
|
55
|
+
if (!pick) {
|
|
56
|
+
throw new Error(`captureTool: no tool captured${toolName ? ` named "${toolName}"` : ""}. Did you forget to call register?`);
|
|
57
|
+
}
|
|
58
|
+
return async (args) => {
|
|
59
|
+
const result = await pick.execute(args, STUB_CONTEXT);
|
|
60
|
+
if (typeof result === "string")
|
|
61
|
+
return result;
|
|
62
|
+
return JSON.stringify(result);
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/server/tools.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { registerBatchCommitTool } from "./batch-commit-tool.js";
|
|
2
|
+
import { registerGitDiffSummaryTool } from "./git-diff-summary-tool.js";
|
|
1
3
|
import { registerGitInventoryTool } from "./git-inventory-tool.js";
|
|
4
|
+
import { registerGitLogTool } from "./git-log-tool.js";
|
|
2
5
|
import { registerGitParityTool } from "./git-parity-tool.js";
|
|
3
6
|
import { registerGitStatusTool } from "./git-status-tool.js";
|
|
4
7
|
import { registerListPresetsTool } from "./list-presets-tool.js";
|
|
@@ -8,5 +11,8 @@ export function registerRethunkGitTools(server) {
|
|
|
8
11
|
registerGitInventoryTool(server);
|
|
9
12
|
registerGitParityTool(server);
|
|
10
13
|
registerListPresetsTool(server);
|
|
14
|
+
registerBatchCommitTool(server);
|
|
15
|
+
registerGitDiffSummaryTool(server);
|
|
16
|
+
registerGitLogTool(server);
|
|
11
17
|
registerPresetsResource(server);
|
|
12
18
|
}
|