@musashishao/folderforge 1.2.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 +181 -0
- package/dist/adapters/child-mcp/client.js +114 -0
- package/dist/adapters/child-mcp/registry.js +66 -0
- package/dist/audit/audit-log.js +45 -0
- package/dist/audit/event-types.js +1 -0
- package/dist/core/config.js +211 -0
- package/dist/core/container.js +51 -0
- package/dist/core/errors.js +37 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/types.js +4 -0
- package/dist/dashboard/server.js +191 -0
- package/dist/lsp/protocol.js +116 -0
- package/dist/main.js +190 -0
- package/dist/managers/db-manager.js +161 -0
- package/dist/managers/lsp-manager.js +269 -0
- package/dist/managers/process-manager.js +140 -0
- package/dist/policy/approvals.js +143 -0
- package/dist/policy/command-policy.js +99 -0
- package/dist/policy/glob-match.js +61 -0
- package/dist/policy/path-policy.js +73 -0
- package/dist/policy/policy-engine.js +156 -0
- package/dist/policy/rate-limiter.js +96 -0
- package/dist/policy/risk.js +112 -0
- package/dist/policy/secret-policy.js +132 -0
- package/dist/server/mcp-server.js +144 -0
- package/dist/server/transports/http.js +133 -0
- package/dist/server/transports/stdio.js +14 -0
- package/dist/tools/adapter-tools.js +62 -0
- package/dist/tools/browser-tools.js +76 -0
- package/dist/tools/build-tools.js +78 -0
- package/dist/tools/code-tools.js +250 -0
- package/dist/tools/coverage-tools.js +135 -0
- package/dist/tools/db-tools.js +130 -0
- package/dist/tools/diff-util.js +45 -0
- package/dist/tools/error-parser.js +57 -0
- package/dist/tools/file-tools.js +319 -0
- package/dist/tools/format-tools.js +118 -0
- package/dist/tools/git-tools.js +371 -0
- package/dist/tools/index.js +63 -0
- package/dist/tools/memory-tools.js +54 -0
- package/dist/tools/output-schemas.js +100 -0
- package/dist/tools/pagination.js +92 -0
- package/dist/tools/pkg-tools.js +260 -0
- package/dist/tools/process-tools.js +128 -0
- package/dist/tools/registry.js +194 -0
- package/dist/tools/schema-lock.js +152 -0
- package/dist/tools/search-tools.js +176 -0
- package/dist/tools/security-tools.js +147 -0
- package/dist/tools/terminal-tools.js +57 -0
- package/dist/tools/workspace-tools.js +186 -0
- package/dist/workspace/memory-store.js +67 -0
- package/dist/workspace/onboarding.js +46 -0
- package/dist/workspace/project-detector.js +95 -0
- package/dist/workspace/workspace-manager.js +106 -0
- package/docs/adapters.md +76 -0
- package/docs/architecture.md +66 -0
- package/docs/roadmap.md +172 -0
- package/docs/security.md +94 -0
- package/docs/tools.md +129 -0
- package/examples/claude-desktop.json +18 -0
- package/examples/codex.toml +18 -0
- package/examples/config.basic.yaml +37 -0
- package/examples/config.full.yaml +120 -0
- package/package.json +74 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { simpleGit } from 'simple-git';
|
|
2
|
+
import { defineTool } from './registry.js';
|
|
3
|
+
import { GIT_STATUS_OUTPUT_SCHEMA } from './output-schemas.js';
|
|
4
|
+
function git(ctx) {
|
|
5
|
+
return simpleGit({ baseDir: ctx.projectRoot });
|
|
6
|
+
}
|
|
7
|
+
export function gitTools() {
|
|
8
|
+
return [
|
|
9
|
+
defineTool({
|
|
10
|
+
name: 'git_status',
|
|
11
|
+
description: 'Show git branch and changed/staged/unstaged files.',
|
|
12
|
+
group: 'git',
|
|
13
|
+
mutates: false,
|
|
14
|
+
inputSchema: { type: 'object', properties: {} },
|
|
15
|
+
outputSchema: GIT_STATUS_OUTPUT_SCHEMA,
|
|
16
|
+
handler: async (_a, ctx) => {
|
|
17
|
+
const s = await git(ctx).status();
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
data: {
|
|
21
|
+
branch: s.current,
|
|
22
|
+
ahead: s.ahead,
|
|
23
|
+
behind: s.behind,
|
|
24
|
+
staged: s.staged,
|
|
25
|
+
modified: s.modified,
|
|
26
|
+
not_added: s.not_added,
|
|
27
|
+
deleted: s.deleted,
|
|
28
|
+
conflicted: s.conflicted,
|
|
29
|
+
clean: s.isClean(),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
defineTool({
|
|
35
|
+
name: 'git_diff',
|
|
36
|
+
description: 'Show diff for the working tree, staged changes, or a specific file.',
|
|
37
|
+
group: 'git',
|
|
38
|
+
mutates: false,
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: { staged: { type: 'boolean' }, file: { type: 'string' } },
|
|
42
|
+
},
|
|
43
|
+
handler: async (args, ctx) => {
|
|
44
|
+
const opts = [];
|
|
45
|
+
if (args.staged)
|
|
46
|
+
opts.push('--cached');
|
|
47
|
+
if (args.file)
|
|
48
|
+
opts.push('--', String(args.file));
|
|
49
|
+
const diff = await git(ctx).diff(opts);
|
|
50
|
+
return { ok: true, data: { diff: ctx.container.policy.secret.redact(diff) } };
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
defineTool({
|
|
54
|
+
name: 'git_log',
|
|
55
|
+
description: 'Show recent commits.',
|
|
56
|
+
group: 'git',
|
|
57
|
+
mutates: false,
|
|
58
|
+
inputSchema: { type: 'object', properties: { limit: { type: 'number' } } },
|
|
59
|
+
handler: async (args, ctx) => {
|
|
60
|
+
const log = await git(ctx).log({ maxCount: Number(args.limit ?? 20) });
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
data: {
|
|
64
|
+
commits: log.all.map((c) => ({ hash: c.hash.slice(0, 8), date: c.date, message: c.message, author: c.author_name })),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
defineTool({
|
|
70
|
+
name: 'git_branch',
|
|
71
|
+
description: 'List branches or create a new one.',
|
|
72
|
+
group: 'git',
|
|
73
|
+
mutates: false,
|
|
74
|
+
inputSchema: { type: 'object', properties: { create: { type: 'string' } } },
|
|
75
|
+
handler: async (args, ctx) => {
|
|
76
|
+
if (args.create) {
|
|
77
|
+
await git(ctx).checkoutLocalBranch(String(args.create));
|
|
78
|
+
return { ok: true, data: { created: String(args.create) } };
|
|
79
|
+
}
|
|
80
|
+
const b = await git(ctx).branchLocal();
|
|
81
|
+
return { ok: true, data: { current: b.current, branches: b.all } };
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
defineTool({
|
|
85
|
+
name: 'git_checkout',
|
|
86
|
+
description: 'Switch to an existing branch.',
|
|
87
|
+
group: 'git',
|
|
88
|
+
mutates: true,
|
|
89
|
+
inputSchema: { type: 'object', properties: { branch: { type: 'string' } }, required: ['branch'] },
|
|
90
|
+
handler: async (args, ctx) => {
|
|
91
|
+
await git(ctx).checkout(String(args.branch));
|
|
92
|
+
return { ok: true, data: { branch: String(args.branch) } };
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
defineTool({
|
|
96
|
+
name: 'git_add',
|
|
97
|
+
description: 'Stage files for commit. Requires approval in safe mode.',
|
|
98
|
+
group: 'git',
|
|
99
|
+
mutates: true,
|
|
100
|
+
inputSchema: { type: 'object', properties: { files: { type: 'array', items: { type: 'string' } } }, required: ['files'] },
|
|
101
|
+
handler: async (args, ctx) => {
|
|
102
|
+
await git(ctx).add(args.files);
|
|
103
|
+
return { ok: true, data: { staged: args.files } };
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
defineTool({
|
|
107
|
+
name: 'git_commit',
|
|
108
|
+
description: 'Commit staged files. Requires approval.',
|
|
109
|
+
group: 'git',
|
|
110
|
+
mutates: true,
|
|
111
|
+
inputSchema: { type: 'object', properties: { message: { type: 'string' } }, required: ['message'] },
|
|
112
|
+
handler: async (args, ctx) => {
|
|
113
|
+
const res = await git(ctx).commit(String(args.message));
|
|
114
|
+
return { ok: true, data: { commit: res.commit, summary: res.summary } };
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
defineTool({
|
|
118
|
+
name: 'git_push',
|
|
119
|
+
description: 'Push commits. CRITICAL; denied unless danger mode + approval. Force push disabled.',
|
|
120
|
+
group: 'git',
|
|
121
|
+
mutates: true,
|
|
122
|
+
inputSchema: { type: 'object', properties: { remote: { type: 'string' }, branch: { type: 'string' } } },
|
|
123
|
+
handler: async (args, ctx) => {
|
|
124
|
+
const remote = String(args.remote ?? 'origin');
|
|
125
|
+
const branch = args.branch ? String(args.branch) : undefined;
|
|
126
|
+
// P8 - elicitation: pushing is irreversible from the local side
|
|
127
|
+
// (publishes commits to a shared remote). When the client supports
|
|
128
|
+
// interactive input, confirm the exact remote/branch before pushing.
|
|
129
|
+
// Clients without the capability see `elicitInput === undefined` and
|
|
130
|
+
// the push proceeds (policy/approval already gate it upstream).
|
|
131
|
+
if (ctx.control?.elicitInput) {
|
|
132
|
+
const status = await git(ctx).status();
|
|
133
|
+
const target = branch ?? status.current ?? 'current branch';
|
|
134
|
+
const ahead = status.ahead ?? 0;
|
|
135
|
+
const res = await ctx.control.elicitInput({
|
|
136
|
+
message: `Push ${ahead} commit(s) to ${remote}/${target}? This publishes them to the shared remote.`,
|
|
137
|
+
requestedSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
confirm: {
|
|
141
|
+
type: 'boolean',
|
|
142
|
+
description: 'Confirm the push.',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ['confirm'],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
if (res.action !== 'accept' || res.content?.confirm !== true) {
|
|
149
|
+
return { ok: false, error: `git_push cancelled by user (${res.action}).` };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// P4 - progress: report a tick before and after the network call so a
|
|
153
|
+
// client that sent a progressToken sees the long push advance.
|
|
154
|
+
await ctx.control?.reportProgress?.(0, 1, `Pushing to ${remote}...`);
|
|
155
|
+
await git(ctx).push(remote, branch);
|
|
156
|
+
await ctx.control?.reportProgress?.(1, 1, 'Push complete.');
|
|
157
|
+
return { ok: true, data: { pushed: true, remote, branch: branch ?? null } };
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
defineTool({
|
|
161
|
+
name: 'git_reset',
|
|
162
|
+
description: 'Reset staged changes (soft/mixed only). CRITICAL; hard reset disabled.',
|
|
163
|
+
group: 'git',
|
|
164
|
+
mutates: true,
|
|
165
|
+
inputSchema: { type: 'object', properties: { mode: { type: 'string', enum: ['soft', 'mixed'] } } },
|
|
166
|
+
handler: async (args, ctx) => {
|
|
167
|
+
const mode = String(args.mode ?? 'mixed');
|
|
168
|
+
if (mode === 'hard' && !ctx.config.git.allowResetHard) {
|
|
169
|
+
return { ok: false, error: 'Hard reset is disabled by configuration.' };
|
|
170
|
+
}
|
|
171
|
+
// P8 - elicitation: when the connected client supports it, confirm this
|
|
172
|
+
// destructive reset interactively before touching the index. Clients
|
|
173
|
+
// without the capability see `elicitInput === undefined` and the reset
|
|
174
|
+
// proceeds non-interactively (policy/approval already gate it upstream).
|
|
175
|
+
if (ctx.control?.elicitInput) {
|
|
176
|
+
const status = await git(ctx).status();
|
|
177
|
+
const res = await ctx.control.elicitInput({
|
|
178
|
+
message: `Reset (--${mode}) will unstage ${status.staged.length} file(s) on branch ${status.current}. Continue?`,
|
|
179
|
+
requestedSchema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
confirm: {
|
|
183
|
+
type: 'boolean',
|
|
184
|
+
description: 'Confirm the reset.',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ['confirm'],
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
if (res.action !== 'accept' || res.content?.confirm !== true) {
|
|
191
|
+
return { ok: false, error: `git_reset cancelled by user (${res.action}).` };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await git(ctx).reset([`--${mode}`]);
|
|
195
|
+
return { ok: true, data: { reset: mode } };
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
defineTool({
|
|
199
|
+
name: 'git_fetch',
|
|
200
|
+
description: 'Fetch updates from a remote without touching the working tree. Updates ' +
|
|
201
|
+
'remote-tracking refs only; safe and non-destructive.',
|
|
202
|
+
group: 'git',
|
|
203
|
+
mutates: true,
|
|
204
|
+
annotations: { openWorldHint: true },
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
remote: { type: 'string', description: 'Remote name (default origin).' },
|
|
209
|
+
branch: { type: 'string', description: 'Specific branch to fetch (default all).' },
|
|
210
|
+
prune: { type: 'boolean', description: 'Prune deleted remote-tracking branches.' },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
handler: async (args, ctx) => {
|
|
214
|
+
const remote = String(args.remote ?? 'origin');
|
|
215
|
+
const branch = args.branch ? String(args.branch) : undefined;
|
|
216
|
+
const opts = args.prune ? ['--prune'] : [];
|
|
217
|
+
await ctx.control?.reportProgress?.(0, 1, `Fetching from ${remote}...`);
|
|
218
|
+
const res = await git(ctx).fetch(remote, branch, opts);
|
|
219
|
+
await ctx.control?.reportProgress?.(1, 1, 'Fetch complete.');
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
data: {
|
|
223
|
+
remote,
|
|
224
|
+
branch: branch ?? null,
|
|
225
|
+
updated: res.updated ?? [],
|
|
226
|
+
deleted: res.deleted ?? [],
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
defineTool({
|
|
232
|
+
name: 'git_pull',
|
|
233
|
+
description: 'Pull and integrate changes from a remote into the current branch. HIGH ' +
|
|
234
|
+
'risk: may rewrite working-tree files and produce merge conflicts. ' +
|
|
235
|
+
'Confirms interactively when the client supports elicitation.',
|
|
236
|
+
group: 'git',
|
|
237
|
+
mutates: true,
|
|
238
|
+
annotations: { openWorldHint: true },
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
remote: { type: 'string', description: 'Remote name (default origin).' },
|
|
243
|
+
branch: { type: 'string', description: 'Branch to pull (default current upstream).' },
|
|
244
|
+
rebase: { type: 'boolean', description: 'Use --rebase instead of merge.' },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
handler: async (args, ctx) => {
|
|
248
|
+
const remote = String(args.remote ?? 'origin');
|
|
249
|
+
const branch = args.branch ? String(args.branch) : undefined;
|
|
250
|
+
const rebase = args.rebase === true;
|
|
251
|
+
// P8 - elicitation: a pull can overwrite local files and create
|
|
252
|
+
// conflicts, so confirm before integrating when the client supports it.
|
|
253
|
+
// Clients without the capability proceed (policy/approval gate upstream).
|
|
254
|
+
if (ctx.control?.elicitInput) {
|
|
255
|
+
const status = await git(ctx).status();
|
|
256
|
+
const target = branch ?? status.tracking ?? status.current ?? 'upstream';
|
|
257
|
+
const dirty = !status.isClean();
|
|
258
|
+
const res = await ctx.control.elicitInput({
|
|
259
|
+
message: `Pull from ${remote}/${target} into ${status.current}` +
|
|
260
|
+
`${rebase ? ' (rebase)' : ''}? ` +
|
|
261
|
+
`${dirty ? 'Your working tree has uncommitted changes. ' : ''}` +
|
|
262
|
+
'This may modify local files.',
|
|
263
|
+
requestedSchema: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {
|
|
266
|
+
confirm: { type: 'boolean', description: 'Confirm the pull.' },
|
|
267
|
+
},
|
|
268
|
+
required: ['confirm'],
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
if (res.action !== 'accept' || res.content?.confirm !== true) {
|
|
272
|
+
return { ok: false, error: `git_pull cancelled by user (${res.action}).` };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const opts = {};
|
|
276
|
+
if (rebase)
|
|
277
|
+
opts['--rebase'] = null;
|
|
278
|
+
await ctx.control?.reportProgress?.(0, 1, `Pulling from ${remote}...`);
|
|
279
|
+
const summary = await git(ctx).pull(remote, branch, opts);
|
|
280
|
+
await ctx.control?.reportProgress?.(1, 1, 'Pull complete.');
|
|
281
|
+
return {
|
|
282
|
+
ok: true,
|
|
283
|
+
data: {
|
|
284
|
+
remote,
|
|
285
|
+
branch: branch ?? null,
|
|
286
|
+
rebase,
|
|
287
|
+
changes: summary.summary?.changes ?? 0,
|
|
288
|
+
insertions: summary.summary?.insertions ?? 0,
|
|
289
|
+
deletions: summary.summary?.deletions ?? 0,
|
|
290
|
+
files: summary.files ?? [],
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
defineTool({
|
|
296
|
+
name: 'git_stash',
|
|
297
|
+
description: 'Save, restore, list, or drop stashed changes. op: push (default) | pop | ' +
|
|
298
|
+
'apply | list | drop. Hard data loss is avoided (no stash clear).',
|
|
299
|
+
group: 'git',
|
|
300
|
+
mutates: true,
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
op: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
enum: ['push', 'pop', 'apply', 'list', 'drop'],
|
|
307
|
+
description: 'Stash operation (default push).',
|
|
308
|
+
},
|
|
309
|
+
message: { type: 'string', description: 'Optional label when op=push.' },
|
|
310
|
+
index: { type: 'number', description: 'Stash index for pop/apply/drop (default 0).' },
|
|
311
|
+
includeUntracked: { type: 'boolean', description: 'Include untracked files when op=push.' },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
handler: async (args, ctx) => {
|
|
315
|
+
const op = String(args.op ?? 'push');
|
|
316
|
+
const g = git(ctx);
|
|
317
|
+
if (op === 'list') {
|
|
318
|
+
const list = await g.stashList();
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
data: {
|
|
322
|
+
op,
|
|
323
|
+
count: list.total,
|
|
324
|
+
entries: list.all.map((e) => ({ hash: e.hash.slice(0, 8), message: e.message })),
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (op === 'push') {
|
|
329
|
+
const opts = ['push'];
|
|
330
|
+
if (args.includeUntracked)
|
|
331
|
+
opts.push('--include-untracked');
|
|
332
|
+
if (args.message)
|
|
333
|
+
opts.push('-m', String(args.message));
|
|
334
|
+
const out = await g.stash(opts);
|
|
335
|
+
return { ok: true, data: { op, result: out.trim() } };
|
|
336
|
+
}
|
|
337
|
+
if (op === 'pop' || op === 'apply' || op === 'drop') {
|
|
338
|
+
const index = Number(args.index ?? 0);
|
|
339
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
340
|
+
return { ok: false, error: `Invalid stash index: ${args.index}` };
|
|
341
|
+
}
|
|
342
|
+
const out = await g.stash([op, `stash@{${index}}`]);
|
|
343
|
+
return { ok: true, data: { op, index, result: out.trim() } };
|
|
344
|
+
}
|
|
345
|
+
return { ok: false, error: `Unknown stash op: ${op}` };
|
|
346
|
+
},
|
|
347
|
+
}),
|
|
348
|
+
defineTool({
|
|
349
|
+
name: 'git_show',
|
|
350
|
+
description: 'Show a commit by ref (read-only).',
|
|
351
|
+
group: 'git',
|
|
352
|
+
mutates: false,
|
|
353
|
+
inputSchema: { type: 'object', properties: { ref: { type: 'string' } }, required: ['ref'] },
|
|
354
|
+
handler: async (args, ctx) => {
|
|
355
|
+
const out = await git(ctx).show([String(args.ref)]);
|
|
356
|
+
return { ok: true, data: { content: ctx.container.policy.secret.redact(out) } };
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
defineTool({
|
|
360
|
+
name: 'git_blame',
|
|
361
|
+
description: 'Show blame for a file (read-only).',
|
|
362
|
+
group: 'git',
|
|
363
|
+
mutates: false,
|
|
364
|
+
inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
|
|
365
|
+
handler: async (args, ctx) => {
|
|
366
|
+
const out = await git(ctx).raw(['blame', String(args.file)]);
|
|
367
|
+
return { ok: true, data: { blame: out.slice(0, 50000) } };
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
];
|
|
371
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ToolRegistry } from './registry.js';
|
|
2
|
+
import { workspaceTools } from './workspace-tools.js';
|
|
3
|
+
import { fileTools } from './file-tools.js';
|
|
4
|
+
import { searchTools } from './search-tools.js';
|
|
5
|
+
import { terminalTools } from './terminal-tools.js';
|
|
6
|
+
import { processTools } from './process-tools.js';
|
|
7
|
+
import { gitTools } from './git-tools.js';
|
|
8
|
+
import { buildTools } from './build-tools.js';
|
|
9
|
+
import { memoryTools } from './memory-tools.js';
|
|
10
|
+
import { securityTools } from './security-tools.js';
|
|
11
|
+
import { codeTools } from './code-tools.js';
|
|
12
|
+
import { browserTools } from './browser-tools.js';
|
|
13
|
+
import { dbTools } from './db-tools.js';
|
|
14
|
+
import { pkgTools } from './pkg-tools.js';
|
|
15
|
+
import { formatTools } from './format-tools.js';
|
|
16
|
+
import { coverageTools } from './coverage-tools.js';
|
|
17
|
+
import { buildAdapterTools } from './adapter-tools.js';
|
|
18
|
+
/**
|
|
19
|
+
* Build the full tool registry with every group registered.
|
|
20
|
+
*/
|
|
21
|
+
export function buildRegistry(container) {
|
|
22
|
+
const registry = new ToolRegistry(container);
|
|
23
|
+
registry.registerAll([
|
|
24
|
+
...workspaceTools(),
|
|
25
|
+
...fileTools(),
|
|
26
|
+
...searchTools(),
|
|
27
|
+
...terminalTools(),
|
|
28
|
+
...processTools(),
|
|
29
|
+
...gitTools(),
|
|
30
|
+
...buildTools(),
|
|
31
|
+
...memoryTools(),
|
|
32
|
+
...securityTools(),
|
|
33
|
+
...codeTools(),
|
|
34
|
+
...browserTools(),
|
|
35
|
+
...dbTools(),
|
|
36
|
+
...pkgTools(),
|
|
37
|
+
...formatTools(),
|
|
38
|
+
...coverageTools(),
|
|
39
|
+
]);
|
|
40
|
+
// Expose the registry on the container so routing tools (workspace_route)
|
|
41
|
+
// can switch the active tool subset at runtime.
|
|
42
|
+
container.registry = registry;
|
|
43
|
+
return registry;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Discover and register tools exposed by enabled child MCP adapters (Serena,
|
|
47
|
+
* Playwright, ...). Each child tool is namespaced (e.g. `serena__find_symbol`)
|
|
48
|
+
* and routed through the normal policy + audit pipeline. Safe to call once after
|
|
49
|
+
* {@link buildRegistry}; a no-op when no adapters are enabled.
|
|
50
|
+
*/
|
|
51
|
+
export async function registerAdapterTools(container, registry) {
|
|
52
|
+
const adapterTools = await buildAdapterTools(container);
|
|
53
|
+
registry.registerAll(adapterTools);
|
|
54
|
+
return adapterTools.length;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Curated tool subsets for task-based routing (section 9 of the spec).
|
|
58
|
+
*/
|
|
59
|
+
export const TASK_PRESETS = {
|
|
60
|
+
explore: ['workspace_status', 'search_text', 'search_files', 'code_find_symbol', 'code_symbols_overview', 'file_read'],
|
|
61
|
+
run_ui: ['process_start', 'process_read', 'browser_open', 'browser_snapshot', 'browser_console', 'browser_network'],
|
|
62
|
+
fix_tests: ['run_test', 'code_diagnostics', 'file_patch', 'file_edit_block', 'shell_exec', 'git_diff'],
|
|
63
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineTool } from './registry.js';
|
|
2
|
+
export function memoryTools() {
|
|
3
|
+
return [
|
|
4
|
+
defineTool({
|
|
5
|
+
name: 'memory_list',
|
|
6
|
+
description: 'List project memory files.',
|
|
7
|
+
group: 'memory',
|
|
8
|
+
mutates: false,
|
|
9
|
+
inputSchema: { type: 'object', properties: {} },
|
|
10
|
+
handler: async (_a, ctx) => ({ ok: true, data: { memories: ctx.container.workspace.getMemory().list() } }),
|
|
11
|
+
}),
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'memory_read',
|
|
14
|
+
description: 'Read a project memory file by name.',
|
|
15
|
+
group: 'memory',
|
|
16
|
+
mutates: false,
|
|
17
|
+
inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
18
|
+
handler: async (args, ctx) => ({
|
|
19
|
+
ok: true,
|
|
20
|
+
data: { content: ctx.container.workspace.getMemory().read(String(args.name)) },
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
defineTool({
|
|
24
|
+
name: 'memory_write',
|
|
25
|
+
description: 'Create or overwrite a project memory file.',
|
|
26
|
+
group: 'memory',
|
|
27
|
+
mutates: true,
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: { name: { type: 'string' }, content: { type: 'string' } },
|
|
31
|
+
required: ['name', 'content'],
|
|
32
|
+
},
|
|
33
|
+
handler: async (args, ctx) => {
|
|
34
|
+
const path = ctx.container.workspace.getMemory().write(String(args.name), String(args.content));
|
|
35
|
+
return { ok: true, data: { path } };
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
defineTool({
|
|
39
|
+
name: 'memory_update',
|
|
40
|
+
description: 'Append content to an existing project memory file.',
|
|
41
|
+
group: 'memory',
|
|
42
|
+
mutates: true,
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: { name: { type: 'string' }, append: { type: 'string' } },
|
|
46
|
+
required: ['name', 'append'],
|
|
47
|
+
},
|
|
48
|
+
handler: async (args, ctx) => {
|
|
49
|
+
const path = ctx.container.workspace.getMemory().update(String(args.name), String(args.append));
|
|
50
|
+
return { ok: true, data: { path } };
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable JSON-Schema fragments for tool `outputSchema` declarations
|
|
3
|
+
* (roadmap Q1 - typed structured output). These describe the shape of the
|
|
4
|
+
* `data` payload a tool returns on success so MCP clients can validate the
|
|
5
|
+
* `structuredContent` field advertised by the server.
|
|
6
|
+
*
|
|
7
|
+
* Keeping them centralized means the schema and the handler stay close and a
|
|
8
|
+
* single source documents the structured contract for the four "high-value"
|
|
9
|
+
* tools called out in the roadmap: run_test, code_diagnostics, git_status,
|
|
10
|
+
* db_query_readonly.
|
|
11
|
+
*/
|
|
12
|
+
const errorItem = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
file: { type: 'string' },
|
|
16
|
+
line: { type: 'integer' },
|
|
17
|
+
column: { type: 'integer' },
|
|
18
|
+
severity: { type: 'string', enum: ['error', 'warning', 'info'] },
|
|
19
|
+
message: { type: 'string' },
|
|
20
|
+
code: { type: 'string' },
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
/** Output of run_test / run_lint / run_typecheck / run_build (runScript). */
|
|
24
|
+
export const RUN_SCRIPT_OUTPUT_SCHEMA = {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
command: { type: 'string', description: 'The resolved command that was executed.' },
|
|
28
|
+
exitCode: { type: ['integer', 'null'], description: 'Process exit code (0 = success).' },
|
|
29
|
+
stdout: { type: 'string' },
|
|
30
|
+
stderr: { type: 'string' },
|
|
31
|
+
errors: {
|
|
32
|
+
type: 'array',
|
|
33
|
+
description: 'Structured failures parsed from the output.',
|
|
34
|
+
items: errorItem,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['command', 'exitCode', 'errors'],
|
|
38
|
+
};
|
|
39
|
+
/** Output of code_diagnostics. */
|
|
40
|
+
export const DIAGNOSTICS_OUTPUT_SCHEMA = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
diagnostics: { type: 'array', items: errorItem },
|
|
44
|
+
count: { type: 'integer' },
|
|
45
|
+
source: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Backend that produced the diagnostics (e.g. "lsp" or "regex").',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ['diagnostics'],
|
|
51
|
+
};
|
|
52
|
+
/** Output of git_status. */
|
|
53
|
+
export const GIT_STATUS_OUTPUT_SCHEMA = {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
branch: { type: ['string', 'null'] },
|
|
57
|
+
ahead: { type: 'integer' },
|
|
58
|
+
behind: { type: 'integer' },
|
|
59
|
+
clean: { type: 'boolean' },
|
|
60
|
+
staged: { type: 'array', items: { type: 'string' } },
|
|
61
|
+
modified: { type: 'array', items: { type: 'string' } },
|
|
62
|
+
not_added: { type: 'array', items: { type: 'string' } },
|
|
63
|
+
deleted: { type: 'array', items: { type: 'string' } },
|
|
64
|
+
conflicted: { type: 'array', items: { type: 'string' } },
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
/** Output of db_query_readonly. */
|
|
68
|
+
export const DB_QUERY_OUTPUT_SCHEMA = {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
rows: {
|
|
72
|
+
type: 'array',
|
|
73
|
+
description: 'Result rows; each row is a column->value object.',
|
|
74
|
+
items: { type: 'object', additionalProperties: true },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ['rows'],
|
|
78
|
+
};
|
|
79
|
+
/** Output of run_coverage. */
|
|
80
|
+
export const COVERAGE_OUTPUT_SCHEMA = {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
command: { type: 'string' },
|
|
84
|
+
exitCode: { type: ['integer', 'null'] },
|
|
85
|
+
summary: {
|
|
86
|
+
type: ['object', 'null'],
|
|
87
|
+
description: 'Coverage percentages (lines/branches/functions/statements).',
|
|
88
|
+
properties: {
|
|
89
|
+
lines: { type: 'number' },
|
|
90
|
+
branches: { type: 'number' },
|
|
91
|
+
functions: { type: 'number' },
|
|
92
|
+
statements: { type: 'number' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
errors: { type: 'array', items: errorItem },
|
|
96
|
+
stdout: { type: 'string' },
|
|
97
|
+
stderr: { type: 'string' },
|
|
98
|
+
},
|
|
99
|
+
required: ['command', 'exitCode'],
|
|
100
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pagination & truncation helpers (roadmap Q3 - token efficiency).
|
|
3
|
+
*
|
|
4
|
+
* Large read-style tool outputs (search hits, logs, file bodies) can blow an
|
|
5
|
+
* agent's context window. These helpers give every reading tool a consistent
|
|
6
|
+
* `offset` / `limit` / `maxBytes` contract and a uniform truncation envelope so
|
|
7
|
+
* the agent always knows whether more data is available and how to fetch it.
|
|
8
|
+
*
|
|
9
|
+
* Conventions:
|
|
10
|
+
* - `offset` is 0-based; `limit` caps the number of returned items.
|
|
11
|
+
* - `maxBytes` caps the UTF-8 byte size of a returned string body.
|
|
12
|
+
* - When output is cut short, the tool sets `truncated: true` and returns a
|
|
13
|
+
* `nextOffset` (for item lists) so the caller can page forward.
|
|
14
|
+
*/
|
|
15
|
+
/** Default and ceiling page sizes; tools may override the default. */
|
|
16
|
+
export const DEFAULT_LIMIT = 100;
|
|
17
|
+
export const MAX_LIMIT = 1000;
|
|
18
|
+
/** Coerce a raw arg into a non-negative integer, or fall back. */
|
|
19
|
+
export function toInt(value, fallback) {
|
|
20
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
21
|
+
if (!Number.isFinite(n) || n < 0)
|
|
22
|
+
return fallback;
|
|
23
|
+
return Math.floor(n);
|
|
24
|
+
}
|
|
25
|
+
/** Read and normalize pagination params from a raw args object. */
|
|
26
|
+
export function readPageParams(args, defaultLimit = DEFAULT_LIMIT) {
|
|
27
|
+
const offset = toInt(args.offset, 0);
|
|
28
|
+
const limit = Math.min(toInt(args.limit, defaultLimit) || defaultLimit, MAX_LIMIT);
|
|
29
|
+
const maxBytes = args.maxBytes === undefined ? undefined : toInt(args.maxBytes, 0) || undefined;
|
|
30
|
+
return maxBytes === undefined ? { offset, limit } : { offset, limit, maxBytes };
|
|
31
|
+
}
|
|
32
|
+
/** Slice an array into a {@link Page} envelope. */
|
|
33
|
+
export function paginate(all, offset, limit) {
|
|
34
|
+
const total = all.length;
|
|
35
|
+
const start = Math.min(offset, total);
|
|
36
|
+
const end = Math.min(start + limit, total);
|
|
37
|
+
const items = all.slice(start, end);
|
|
38
|
+
const truncated = end < total;
|
|
39
|
+
return {
|
|
40
|
+
items,
|
|
41
|
+
total,
|
|
42
|
+
offset: start,
|
|
43
|
+
count: items.length,
|
|
44
|
+
truncated,
|
|
45
|
+
nextOffset: truncated ? end : null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Truncate a string to at most `maxBytes` UTF-8 bytes without splitting a
|
|
50
|
+
* multi-byte character. Returns the original string untouched when it fits or
|
|
51
|
+
* when `maxBytes` is undefined.
|
|
52
|
+
*/
|
|
53
|
+
export function truncateBytes(text, maxBytes) {
|
|
54
|
+
const buf = Buffer.from(text, 'utf8');
|
|
55
|
+
const totalBytes = buf.length;
|
|
56
|
+
if (maxBytes === undefined || totalBytes <= maxBytes) {
|
|
57
|
+
return { text, truncated: false, totalBytes, returnedBytes: totalBytes };
|
|
58
|
+
}
|
|
59
|
+
// Step back to a UTF-8 character boundary (continuation bytes are 0b10xxxxxx).
|
|
60
|
+
let end = maxBytes;
|
|
61
|
+
while (end > 0 && ((buf[end] ?? 0) & 0xc0) === 0x80)
|
|
62
|
+
end--;
|
|
63
|
+
const slice = buf.subarray(0, end);
|
|
64
|
+
return {
|
|
65
|
+
text: slice.toString('utf8'),
|
|
66
|
+
truncated: true,
|
|
67
|
+
totalBytes,
|
|
68
|
+
returnedBytes: slice.length,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* JSON-Schema fragment for the shared pagination inputs. Spread into a tool's
|
|
73
|
+
* `inputSchema.properties` so every reading tool documents the same contract.
|
|
74
|
+
*/
|
|
75
|
+
export const PAGE_INPUT_SCHEMA = {
|
|
76
|
+
offset: {
|
|
77
|
+
type: 'integer',
|
|
78
|
+
minimum: 0,
|
|
79
|
+
description: '0-based index of the first item to return (default 0).',
|
|
80
|
+
},
|
|
81
|
+
limit: {
|
|
82
|
+
type: 'integer',
|
|
83
|
+
minimum: 1,
|
|
84
|
+
maximum: MAX_LIMIT,
|
|
85
|
+
description: `Maximum number of items to return (default ${DEFAULT_LIMIT}).`,
|
|
86
|
+
},
|
|
87
|
+
maxBytes: {
|
|
88
|
+
type: 'integer',
|
|
89
|
+
minimum: 1,
|
|
90
|
+
description: 'Cap the UTF-8 byte size of large string output.',
|
|
91
|
+
},
|
|
92
|
+
};
|