@imenam/mcp-feature-analyzer 1.0.2 → 1.0.4
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 +336 -295
- package/dist/shared/schemas/analysis.schema.d.ts +61 -0
- package/dist/shared/schemas/analysis.schema.js +26 -0
- package/dist/shared/schemas/api.schema.d.ts +13 -0
- package/dist/shared/schemas/api.schema.js +8 -0
- package/dist/shared/schemas/ipc.schema.d.ts +1 -1
- package/dist/shared/schemas/ipc.schema.js +1 -0
- package/dist/src/core/agent-repo.d.ts +4 -0
- package/dist/src/core/agent-repo.js +172 -0
- package/dist/src/core/analysis-store.js +1 -0
- package/dist/src/core/gateway-exec.d.ts +27 -0
- package/dist/src/core/gateway-exec.js +115 -0
- package/dist/src/core/git.d.ts +27 -2
- package/dist/src/core/git.js +48 -34
- package/dist/src/core/ids.d.ts +1 -0
- package/dist/src/core/ids.js +1 -0
- package/dist/src/core/invariants.d.ts +3 -1
- package/dist/src/core/invariants.js +27 -1
- package/dist/src/core/locations.d.ts +22 -4
- package/dist/src/core/locations.js +88 -4
- package/dist/src/core/migrate.d.ts +2 -0
- package/dist/src/core/migrate.js +3 -1
- package/dist/src/core/refresh.d.ts +8 -2
- package/dist/src/core/refresh.js +19 -1
- package/dist/src/core/review-actions.d.ts +6 -1
- package/dist/src/core/review-actions.js +20 -0
- package/dist/src/core/role.d.ts +11 -0
- package/dist/src/core/role.js +9 -0
- package/dist/src/gui-worker.js +7 -1
- package/dist/src/index.js +139 -19
- package/dist/src/tools/add-explanations.d.ts +9 -0
- package/dist/src/tools/add-explanations.js +73 -0
- package/dist/src/tools/create-analysis.d.ts +3 -2
- package/dist/src/tools/create-analysis.js +5 -5
- package/dist/src/tools/delete-explanations.d.ts +4 -0
- package/dist/src/tools/delete-explanations.js +23 -0
- package/dist/src/tools/format.d.ts +3 -1
- package/dist/src/tools/format.js +5 -0
- package/dist/src/tools/get-analysis.js +6 -1
- package/dist/src/tools/get-diff.js +2 -1
- package/dist/src/tools/inputs.d.ts +14 -0
- package/dist/src/tools/inputs.js +20 -2
- package/dist/src/tools/refresh-analysis.d.ts +4 -3
- package/dist/src/tools/refresh-analysis.js +13 -12
- package/dist/src/tools/update-explanation.d.ts +7 -0
- package/dist/src/tools/update-explanation.js +57 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/index-BLyQW-EW.js +72 -0
- package/gui/dist/assets/index-CmSZOGwc.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/gui/dist/assets/index-Db7Hu3tM.js +0 -72
- package/gui/dist/assets/index-Du5eL78F.css +0 -1
|
@@ -4,6 +4,8 @@ export declare const FILE_STATUSES: readonly ["added", "modified", "deleted", "r
|
|
|
4
4
|
export declare const SEVERITIES: readonly ["critical", "major", "minor", "trivial"];
|
|
5
5
|
export declare const FINDING_KINDS: readonly ["issue", "requirement_gap"];
|
|
6
6
|
export declare const FINDING_STATUSES: readonly ["open", "ignored", "outdated"];
|
|
7
|
+
export declare const EXPLANATION_SIDES: readonly ["new", "old"];
|
|
8
|
+
export declare const EXPLANATION_STATUSES: readonly ["current", "outdated"];
|
|
7
9
|
export declare const DIAGRAM_KINDS: readonly ["flow", "layers", "mindmap"];
|
|
8
10
|
export declare const NODE_SHAPES: readonly ["box", "pill", "decision"];
|
|
9
11
|
export declare const NODE_STATUSES: readonly ["new", "modified", "impacted", "existing", "finding", "missing"];
|
|
@@ -66,6 +68,41 @@ export declare const FindingSchema: z.ZodObject<{
|
|
|
66
68
|
}>;
|
|
67
69
|
createdAt: z.ZodString;
|
|
68
70
|
}, z.core.$strip>;
|
|
71
|
+
/**
|
|
72
|
+
* Plage de lignes décrite par une explication, bornes incluses : côté `new` (numéros
|
|
73
|
+
* du côté « nouveau », code ajouté ou conservé) ou côté `old` (numéros du côté
|
|
74
|
+
* « ancien », code supprimé).
|
|
75
|
+
*/
|
|
76
|
+
export declare const ExplanationLocationSchema: z.ZodObject<{
|
|
77
|
+
path: z.ZodString;
|
|
78
|
+
side: z.ZodEnum<{
|
|
79
|
+
new: "new";
|
|
80
|
+
old: "old";
|
|
81
|
+
}>;
|
|
82
|
+
startLine: z.ZodNumber;
|
|
83
|
+
endLine: z.ZodNumber;
|
|
84
|
+
}, z.core.$strip>;
|
|
85
|
+
/** Description, par l'agent, de ce que fait un bloc de code et de la façon dont il le fait. */
|
|
86
|
+
export declare const ExplanationSchema: z.ZodObject<{
|
|
87
|
+
id: z.ZodString;
|
|
88
|
+
title: z.ZodString;
|
|
89
|
+
body: z.ZodString;
|
|
90
|
+
location: z.ZodObject<{
|
|
91
|
+
path: z.ZodString;
|
|
92
|
+
side: z.ZodEnum<{
|
|
93
|
+
new: "new";
|
|
94
|
+
old: "old";
|
|
95
|
+
}>;
|
|
96
|
+
startLine: z.ZodNumber;
|
|
97
|
+
endLine: z.ZodNumber;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
anchorText: z.ZodString;
|
|
100
|
+
status: z.ZodEnum<{
|
|
101
|
+
current: "current";
|
|
102
|
+
outdated: "outdated";
|
|
103
|
+
}>;
|
|
104
|
+
createdAt: z.ZodString;
|
|
105
|
+
}, z.core.$strip>;
|
|
69
106
|
/** Emplacement d'une remarque : une ligne du côté « nouveau », ou le fichier entier (`line` null). */
|
|
70
107
|
export declare const NoteLocationSchema: z.ZodObject<{
|
|
71
108
|
path: z.ZodString;
|
|
@@ -247,6 +284,26 @@ export declare const AnalysisSchema: z.ZodObject<{
|
|
|
247
284
|
}>;
|
|
248
285
|
createdAt: z.ZodString;
|
|
249
286
|
}, z.core.$strip>>;
|
|
287
|
+
explanations: z.ZodArray<z.ZodObject<{
|
|
288
|
+
id: z.ZodString;
|
|
289
|
+
title: z.ZodString;
|
|
290
|
+
body: z.ZodString;
|
|
291
|
+
location: z.ZodObject<{
|
|
292
|
+
path: z.ZodString;
|
|
293
|
+
side: z.ZodEnum<{
|
|
294
|
+
new: "new";
|
|
295
|
+
old: "old";
|
|
296
|
+
}>;
|
|
297
|
+
startLine: z.ZodNumber;
|
|
298
|
+
endLine: z.ZodNumber;
|
|
299
|
+
}, z.core.$strip>;
|
|
300
|
+
anchorText: z.ZodString;
|
|
301
|
+
status: z.ZodEnum<{
|
|
302
|
+
current: "current";
|
|
303
|
+
outdated: "outdated";
|
|
304
|
+
}>;
|
|
305
|
+
createdAt: z.ZodString;
|
|
306
|
+
}, z.core.$strip>>;
|
|
250
307
|
notes: z.ZodArray<z.ZodObject<{
|
|
251
308
|
id: z.ZodString;
|
|
252
309
|
location: z.ZodNullable<z.ZodObject<{
|
|
@@ -323,6 +380,8 @@ export type FileStatus = (typeof FILE_STATUSES)[number];
|
|
|
323
380
|
export type Severity = (typeof SEVERITIES)[number];
|
|
324
381
|
export type FindingKind = (typeof FINDING_KINDS)[number];
|
|
325
382
|
export type FindingStatus = (typeof FINDING_STATUSES)[number];
|
|
383
|
+
export type ExplanationSide = (typeof EXPLANATION_SIDES)[number];
|
|
384
|
+
export type ExplanationStatus = (typeof EXPLANATION_STATUSES)[number];
|
|
326
385
|
export type DiagramKind = (typeof DIAGRAM_KINDS)[number];
|
|
327
386
|
export type NodeShape = (typeof NODE_SHAPES)[number];
|
|
328
387
|
export type NodeStatus = (typeof NODE_STATUSES)[number];
|
|
@@ -332,6 +391,8 @@ export type Editor = (typeof EDITORS)[number];
|
|
|
332
391
|
export type FileEntry = z.infer<typeof FileEntrySchema>;
|
|
333
392
|
export type FindingLocation = z.infer<typeof FindingLocationSchema>;
|
|
334
393
|
export type Finding = z.infer<typeof FindingSchema>;
|
|
394
|
+
export type ExplanationLocation = z.infer<typeof ExplanationLocationSchema>;
|
|
395
|
+
export type Explanation = z.infer<typeof ExplanationSchema>;
|
|
335
396
|
export type NoteLocation = z.infer<typeof NoteLocationSchema>;
|
|
336
397
|
export type Note = z.infer<typeof NoteSchema>;
|
|
337
398
|
export type DiagramNodeLocation = z.infer<typeof DiagramNodeLocationSchema>;
|
|
@@ -4,6 +4,8 @@ export const FILE_STATUSES = ["added", "modified", "deleted", "renamed"];
|
|
|
4
4
|
export const SEVERITIES = ["critical", "major", "minor", "trivial"];
|
|
5
5
|
export const FINDING_KINDS = ["issue", "requirement_gap"];
|
|
6
6
|
export const FINDING_STATUSES = ["open", "ignored", "outdated"];
|
|
7
|
+
export const EXPLANATION_SIDES = ["new", "old"];
|
|
8
|
+
export const EXPLANATION_STATUSES = ["current", "outdated"];
|
|
7
9
|
export const DIAGRAM_KINDS = ["flow", "layers", "mindmap"];
|
|
8
10
|
export const NODE_SHAPES = ["box", "pill", "decision"];
|
|
9
11
|
export const NODE_STATUSES = ["new", "modified", "impacted", "existing", "finding", "missing"];
|
|
@@ -55,6 +57,29 @@ export const FindingSchema = z.object({
|
|
|
55
57
|
status: z.enum(FINDING_STATUSES),
|
|
56
58
|
createdAt: IsoDateSchema,
|
|
57
59
|
});
|
|
60
|
+
/**
|
|
61
|
+
* Plage de lignes décrite par une explication, bornes incluses : côté `new` (numéros
|
|
62
|
+
* du côté « nouveau », code ajouté ou conservé) ou côté `old` (numéros du côté
|
|
63
|
+
* « ancien », code supprimé).
|
|
64
|
+
*/
|
|
65
|
+
export const ExplanationLocationSchema = z.object({
|
|
66
|
+
path: z.string().min(1),
|
|
67
|
+
side: z.enum(EXPLANATION_SIDES),
|
|
68
|
+
startLine: LineSchema,
|
|
69
|
+
endLine: LineSchema,
|
|
70
|
+
});
|
|
71
|
+
/** Description, par l'agent, de ce que fait un bloc de code et de la façon dont il le fait. */
|
|
72
|
+
export const ExplanationSchema = z.object({
|
|
73
|
+
id: ItemIdSchema,
|
|
74
|
+
title: z.string().min(1),
|
|
75
|
+
body: z.string().min(1),
|
|
76
|
+
location: ExplanationLocationSchema,
|
|
77
|
+
/** Texte exact des lignes décrites au moment où l'explication a été ancrée. */
|
|
78
|
+
anchorText: z.string(),
|
|
79
|
+
/** `outdated` quand les lignes décrites ont changé depuis, au recalcul du snapshot. */
|
|
80
|
+
status: z.enum(EXPLANATION_STATUSES),
|
|
81
|
+
createdAt: IsoDateSchema,
|
|
82
|
+
});
|
|
58
83
|
/** Emplacement d'une remarque : une ligne du côté « nouveau », ou le fichier entier (`line` null). */
|
|
59
84
|
export const NoteLocationSchema = z.object({
|
|
60
85
|
path: z.string().min(1),
|
|
@@ -140,6 +165,7 @@ export const AnalysisSchema = z.object({
|
|
|
140
165
|
summary: z.array(z.string().min(1)),
|
|
141
166
|
files: z.array(FileEntrySchema),
|
|
142
167
|
findings: z.array(FindingSchema),
|
|
168
|
+
explanations: z.array(ExplanationSchema),
|
|
143
169
|
notes: z.array(NoteSchema),
|
|
144
170
|
diagrams: z.array(DiagramSchema),
|
|
145
171
|
review: ReviewSchema,
|
|
@@ -19,6 +19,10 @@ export declare const AddNoteBodySchema: z.ZodObject<{
|
|
|
19
19
|
text: z.ZodString;
|
|
20
20
|
baseRevision: z.ZodNumber;
|
|
21
21
|
}, z.core.$strict>;
|
|
22
|
+
export declare const UpdateNoteBodySchema: z.ZodObject<{
|
|
23
|
+
text: z.ZodString;
|
|
24
|
+
baseRevision: z.ZodNumber;
|
|
25
|
+
}, z.core.$strict>;
|
|
22
26
|
export declare const DeleteNoteBodySchema: z.ZodObject<{
|
|
23
27
|
baseRevision: z.ZodNumber;
|
|
24
28
|
}, z.core.$strict>;
|
|
@@ -35,6 +39,7 @@ export declare const SubmitReviewBodySchema: z.ZodObject<{
|
|
|
35
39
|
export type SetFileReviewedBody = z.infer<typeof SetFileReviewedBodySchema>;
|
|
36
40
|
export type SetFindingStatusBody = z.infer<typeof SetFindingStatusBodySchema>;
|
|
37
41
|
export type AddNoteBody = z.infer<typeof AddNoteBodySchema>;
|
|
42
|
+
export type UpdateNoteBody = z.infer<typeof UpdateNoteBodySchema>;
|
|
38
43
|
export type DeleteNoteBody = z.infer<typeof DeleteNoteBodySchema>;
|
|
39
44
|
export type SubmitReviewBody = z.infer<typeof SubmitReviewBodySchema>;
|
|
40
45
|
/** Données IPC des requêtes de la GUI qui visent une analyse. */
|
|
@@ -70,6 +75,14 @@ export declare const AddNoteRequestSchema: z.ZodObject<{
|
|
|
70
75
|
baseRevision: z.ZodNumber;
|
|
71
76
|
}, z.core.$strict>;
|
|
72
77
|
}, z.core.$strict>;
|
|
78
|
+
export declare const UpdateNoteRequestSchema: z.ZodObject<{
|
|
79
|
+
analysisId: z.ZodString;
|
|
80
|
+
noteId: z.ZodString;
|
|
81
|
+
body: z.ZodObject<{
|
|
82
|
+
text: z.ZodString;
|
|
83
|
+
baseRevision: z.ZodNumber;
|
|
84
|
+
}, z.core.$strict>;
|
|
85
|
+
}, z.core.$strict>;
|
|
73
86
|
export declare const DeleteNoteRequestSchema: z.ZodObject<{
|
|
74
87
|
analysisId: z.ZodString;
|
|
75
88
|
noteId: z.ZodString;
|
|
@@ -22,6 +22,10 @@ export const AddNoteBodySchema = z.strictObject({
|
|
|
22
22
|
text: z.string().trim().min(1, "the note text is empty"),
|
|
23
23
|
baseRevision: BaseRevisionSchema,
|
|
24
24
|
});
|
|
25
|
+
export const UpdateNoteBodySchema = z.strictObject({
|
|
26
|
+
text: z.string().trim().min(1, "the note text is empty"),
|
|
27
|
+
baseRevision: BaseRevisionSchema,
|
|
28
|
+
});
|
|
25
29
|
export const DeleteNoteBodySchema = z.strictObject({
|
|
26
30
|
baseRevision: BaseRevisionSchema,
|
|
27
31
|
});
|
|
@@ -39,6 +43,10 @@ export const SetFindingStatusRequestSchema = AnalysisRequestSchema.extend({
|
|
|
39
43
|
body: SetFindingStatusBodySchema,
|
|
40
44
|
});
|
|
41
45
|
export const AddNoteRequestSchema = AnalysisRequestSchema.extend({ body: AddNoteBodySchema });
|
|
46
|
+
export const UpdateNoteRequestSchema = AnalysisRequestSchema.extend({
|
|
47
|
+
noteId: z.string().min(1),
|
|
48
|
+
body: UpdateNoteBodySchema,
|
|
49
|
+
});
|
|
42
50
|
export const DeleteNoteRequestSchema = AnalysisRequestSchema.extend({
|
|
43
51
|
noteId: z.string().min(1),
|
|
44
52
|
body: DeleteNoteBodySchema,
|
|
@@ -25,5 +25,5 @@ export type IPCMessage = z.infer<typeof IPCMessageSchema>;
|
|
|
25
25
|
export declare const PUSHED_EVENT_TYPES: readonly ["INITIAL_STATE", "ANALYSES_UPDATED", "ANALYSIS_UPDATED"];
|
|
26
26
|
export type PushedEventType = (typeof PUSHED_EVENT_TYPES)[number];
|
|
27
27
|
/** Requêtes du GUI worker au maître ; chacune reçoit une réponse de même `correlationId`. */
|
|
28
|
-
export declare const GUI_REQUEST_TYPES: readonly ["GET_CONFIG", "LIST_ANALYSES", "GET_ANALYSIS", "GET_DIFF", "SET_FILE_REVIEWED", "SET_FINDING_STATUS", "ADD_NOTE", "DELETE_NOTE", "SUBMIT_REVIEW"];
|
|
28
|
+
export declare const GUI_REQUEST_TYPES: readonly ["GET_CONFIG", "LIST_ANALYSES", "GET_ANALYSIS", "GET_DIFF", "SET_FILE_REVIEWED", "SET_FINDING_STATUS", "ADD_NOTE", "UPDATE_NOTE", "DELETE_NOTE", "SUBMIT_REVIEW"];
|
|
29
29
|
export type GuiRequestType = (typeof GUI_REQUEST_TYPES)[number];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RepoAccess } from "./git.js";
|
|
2
|
+
import { type Run } from "./gateway-exec.js";
|
|
3
|
+
/** Accès au dépôt `repoPath` de la machine de l'agent, par les commandes que `run` y fait exécuter. */
|
|
4
|
+
export declare function agentRepo(repoPath: string, run: Run): RepoAccess;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accès au dépôt placé sur la machine de l'agent, quand ce serveur tourne derrière
|
|
3
|
+
* mcp-http-gateway : chaque commande est exécutée par le relais agent (voir
|
|
4
|
+
* gateway-exec.ts), dans `repoPath`, et sa sortie revient dans le rappel de l'outil.
|
|
5
|
+
*
|
|
6
|
+
* - git lit le dépôt ; `node` lit les fichiers de la copie de travail.
|
|
7
|
+
* - La sortie d'une commande est limitée à 1 Mio par le relais : le diff est demandé
|
|
8
|
+
* fichier par fichier, et un contenu tronqué est rendu `null`.
|
|
9
|
+
* - Les commandes d'une même demande sont groupées par paquets de STEPS_PER_ROUND, pour
|
|
10
|
+
* borner la taille de chaque rappel.
|
|
11
|
+
*/
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { toPosixPath } from "./diff-parser.js";
|
|
14
|
+
import { GitError } from "./errors.js";
|
|
15
|
+
import { DIFF_ARGS, assertRefShape } from "./git.js";
|
|
16
|
+
import { RELAY_MAX_OUTPUT_BYTES, isTruncated } from "./gateway-exec.js";
|
|
17
|
+
/** Nombre maximal de commandes par demande d'exécution. */
|
|
18
|
+
const STEPS_PER_ROUND = 25;
|
|
19
|
+
/** Variable d'environnement qui porte le chemin du fichier à lire. */
|
|
20
|
+
const FILE_ENV = "FEATURE_ANALYZER_FILE";
|
|
21
|
+
/** Écrit sur stdout le contenu du fichier FILE_ENV, ou la cible POSIX d'un lien symbolique. */
|
|
22
|
+
const READ_FILE_SCRIPT = [
|
|
23
|
+
'const fs = require("fs");',
|
|
24
|
+
`const file = process.env.${FILE_ENV};`,
|
|
25
|
+
"process.stdout.write(fs.lstatSync(file).isSymbolicLink()",
|
|
26
|
+
' ? fs.readlinkSync(file).split(String.fromCharCode(92)).join("/")',
|
|
27
|
+
" : fs.readFileSync(file));",
|
|
28
|
+
].join("\n");
|
|
29
|
+
const MIB = `${RELAY_MAX_OUTPUT_BYTES / (1024 * 1024)} MiB`;
|
|
30
|
+
function detail(result) {
|
|
31
|
+
return (result.error ?? (result.stderr.trim() || result.stdout.trim())) || "no output";
|
|
32
|
+
}
|
|
33
|
+
/** Accès au dépôt `repoPath` de la machine de l'agent, par les commandes que `run` y fait exécuter. */
|
|
34
|
+
export function agentRepo(repoPath, run) {
|
|
35
|
+
function gitStep(args) {
|
|
36
|
+
return {
|
|
37
|
+
command: "git",
|
|
38
|
+
args: ["-c", "core.quotepath=false", ...args],
|
|
39
|
+
cwd: repoPath,
|
|
40
|
+
env: { GIT_LITERAL_PATHSPECS: "1", GIT_TERMINAL_PROMPT: "0" },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function readFileStep(filePath) {
|
|
44
|
+
return {
|
|
45
|
+
command: "node",
|
|
46
|
+
args: ["--input-type=commonjs", "-e", READ_FILE_SCRIPT],
|
|
47
|
+
cwd: repoPath,
|
|
48
|
+
env: { [FILE_ENV]: filePath },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Échec de lancement d'une commande : dossier absent ou exécutable introuvable. */
|
|
52
|
+
function launchError(step, result) {
|
|
53
|
+
return new GitError(`Could not run ${step.command} in "${repoPath}" on the agent's machine (${result.error}). ` +
|
|
54
|
+
`Check that repo_path exists on that machine and that ${step.command} is on its PATH.`);
|
|
55
|
+
}
|
|
56
|
+
/** Exécute une seule commande et rend son résultat ; un échec de lancement lève une erreur. */
|
|
57
|
+
async function runOne(step) {
|
|
58
|
+
const [result] = await run([step]);
|
|
59
|
+
if (!result)
|
|
60
|
+
throw new GitError(`The agent relay returned no result for ${step.command} ${step.args.join(" ")}.`);
|
|
61
|
+
if (result.error !== undefined)
|
|
62
|
+
throw launchError(step, result);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/** Exécute une commande git qui doit réussir, avec une sortie complète. */
|
|
66
|
+
async function runGit(args) {
|
|
67
|
+
const result = await runOne(gitStep(args));
|
|
68
|
+
if (result.code !== 0)
|
|
69
|
+
throw new GitError(`git ${args.join(" ")} failed in ${repoPath} on the agent's machine: ${detail(result)}`);
|
|
70
|
+
if (isTruncated(result.stdout)) {
|
|
71
|
+
throw new GitError(`The output of git ${args.join(" ")} exceeds ${MIB}, the output limit of the agent relay.`);
|
|
72
|
+
}
|
|
73
|
+
return result.stdout;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Exécute une commande par élément, par paquets. Le relais s'arrête à la première
|
|
77
|
+
* commande en échec : `onFailure` rend l'erreur à lever pour cet élément.
|
|
78
|
+
*/
|
|
79
|
+
async function runEach(items, toStep, onFailure) {
|
|
80
|
+
const all = [];
|
|
81
|
+
for (let start = 0; start < items.length; start += STEPS_PER_ROUND) {
|
|
82
|
+
const chunk = items.slice(start, start + STEPS_PER_ROUND);
|
|
83
|
+
const steps = chunk.map(toStep);
|
|
84
|
+
const results = await run(steps);
|
|
85
|
+
chunk.forEach((item, index) => {
|
|
86
|
+
const result = results[index];
|
|
87
|
+
if (!result)
|
|
88
|
+
throw new GitError(`The agent relay returned no result for ${steps[index].command} ${steps[index].args.join(" ")}.`);
|
|
89
|
+
if (result.error !== undefined)
|
|
90
|
+
throw launchError(steps[index], result);
|
|
91
|
+
if (result.code !== 0)
|
|
92
|
+
throw onFailure(item, result);
|
|
93
|
+
all.push(result);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return all;
|
|
97
|
+
}
|
|
98
|
+
/** Contenu rendu par le relais, ou null s'il a été tronqué. */
|
|
99
|
+
function contentOf(result) {
|
|
100
|
+
return isTruncated(result.stdout) ? null : Buffer.from(result.stdout, "utf-8");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
async assertRepoRoot() {
|
|
104
|
+
if (!path.posix.isAbsolute(repoPath) && !path.win32.isAbsolute(repoPath)) {
|
|
105
|
+
throw new GitError(`repo_path must be an absolute path on the agent's machine, got "${repoPath}".`);
|
|
106
|
+
}
|
|
107
|
+
const args = ["rev-parse", "--show-toplevel", "--show-cdup"];
|
|
108
|
+
const result = await runOne(gitStep(args));
|
|
109
|
+
if (result.code !== 0) {
|
|
110
|
+
if (/not a git repository/i.test(result.stderr)) {
|
|
111
|
+
throw new GitError(`repo_path "${repoPath}" is not inside a git repository.`);
|
|
112
|
+
}
|
|
113
|
+
throw new GitError(`git ${args.join(" ")} failed in ${repoPath} on the agent's machine: ${detail(result)}`);
|
|
114
|
+
}
|
|
115
|
+
const [topLevel, cdup] = result.stdout.split("\n");
|
|
116
|
+
if (cdup.trim() !== "") {
|
|
117
|
+
throw new GitError(`repo_path "${repoPath}" is not the root of its git repository. Use the repository root: ${topLevel.trim()}`);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
async resolveCommit(ref, role) {
|
|
121
|
+
assertRefShape(ref, role);
|
|
122
|
+
const result = await runOne(gitStep(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]));
|
|
123
|
+
if (result.code !== 0) {
|
|
124
|
+
throw new GitError(`Unknown ${role} ref "${ref}" in ${repoPath}. Use a branch name, a tag or a commit sha that exists in this repository.`);
|
|
125
|
+
}
|
|
126
|
+
return result.stdout.trim();
|
|
127
|
+
},
|
|
128
|
+
async diff(range) {
|
|
129
|
+
const listing = await runGit([
|
|
130
|
+
"diff",
|
|
131
|
+
"--no-color",
|
|
132
|
+
"--no-ext-diff",
|
|
133
|
+
"--no-relative",
|
|
134
|
+
"--find-renames",
|
|
135
|
+
"--name-status",
|
|
136
|
+
"-z",
|
|
137
|
+
...range,
|
|
138
|
+
"--",
|
|
139
|
+
]);
|
|
140
|
+
const tokens = listing.split("\0");
|
|
141
|
+
const files = [];
|
|
142
|
+
for (let index = 0; index < tokens.length && tokens[index] !== "";) {
|
|
143
|
+
const pathCount = /^[RC]/.test(tokens[index]) ? 2 : 1;
|
|
144
|
+
files.push(tokens.slice(index + 1, index + 1 + pathCount));
|
|
145
|
+
index += 1 + pathCount;
|
|
146
|
+
}
|
|
147
|
+
const results = await runEach(files, (paths) => gitStep([...DIFF_ARGS, ...range, "--", ...paths]), (paths, result) => new GitError(`git diff of "${paths.join('" -> "')}" failed on the agent's machine: ${detail(result)}`));
|
|
148
|
+
return results
|
|
149
|
+
.map((result, index) => {
|
|
150
|
+
if (isTruncated(result.stdout)) {
|
|
151
|
+
throw new GitError(`The diff of "${files[index].at(-1)}" exceeds ${MIB}, the output limit of the agent relay: this change cannot be analysed through the gateway.`);
|
|
152
|
+
}
|
|
153
|
+
return result.stdout;
|
|
154
|
+
})
|
|
155
|
+
.join("");
|
|
156
|
+
},
|
|
157
|
+
async readCommitFiles(commit, paths) {
|
|
158
|
+
const results = await runEach(paths, (filePath) => gitStep(["cat-file", "blob", `${commit}:${filePath}`]), (filePath) => new GitError(`"${filePath}" is in the diff but is not a file of commit ${commit} (submodule or unreadable entry).`));
|
|
159
|
+
return new Map(paths.map((filePath, index) => [filePath, contentOf(results[index])]));
|
|
160
|
+
},
|
|
161
|
+
async untrackedFiles() {
|
|
162
|
+
return (await runGit(["ls-files", "--others", "--exclude-standard", "-z"]))
|
|
163
|
+
.split("\0")
|
|
164
|
+
.filter((entry) => entry !== "")
|
|
165
|
+
.map(toPosixPath);
|
|
166
|
+
},
|
|
167
|
+
async readWorkingFiles(paths) {
|
|
168
|
+
const results = await runEach(paths, readFileStep, (filePath, result) => new GitError(`Could not read "${filePath}" in ${repoPath} on the agent's machine: ${detail(result)}`));
|
|
169
|
+
return new Map(paths.map((filePath, index) => [filePath, contentOf(results[index])]));
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolResult } from "../tools/types.js";
|
|
2
|
+
export declare const EXEC_ARG = "_gateway_exec";
|
|
3
|
+
export declare const EXEC_META = "gateway/exec";
|
|
4
|
+
export declare const EXEC_VERSION = 1;
|
|
5
|
+
/** Taille maximale de la sortie d'une étape conservée par le relais agent. */
|
|
6
|
+
export declare const RELAY_MAX_OUTPUT_BYTES: number;
|
|
7
|
+
export interface ExecStep {
|
|
8
|
+
command: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
cwd?: string;
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export interface StepResult {
|
|
14
|
+
code: number | null;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
export type Run = (steps: ExecStep[]) => Promise<StepResult[]>;
|
|
20
|
+
/** Vrai si le relais agent a tronqué cette sortie. */
|
|
21
|
+
export declare function isTruncated(output: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Point d'entrée d'un outil qui s'exécute sur la machine de l'agent. Au premier appel,
|
|
24
|
+
* démarre `tool` ; aux rappels du relais agent, reprend la fonction là où elle attend.
|
|
25
|
+
* `exec` est la valeur de l'argument `_gateway_exec` de l'appel.
|
|
26
|
+
*/
|
|
27
|
+
export declare function handleAgentExec(exec: unknown, tool: (run: Run) => Promise<ToolResult>): Promise<ToolResult>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exécution de commandes sur la machine de l'agent, par le relais agent de
|
|
3
|
+
* mcp-http-gateway (`mcp-http-gateway --mcp`).
|
|
4
|
+
*
|
|
5
|
+
* Convention, entre le relais agent et un MCP placé derrière le gateway :
|
|
6
|
+
*
|
|
7
|
+
* - Quand la route de ce MCP figure dans sa liste blanche (GATEWAY_EXEC_ROUTES), le
|
|
8
|
+
* relais agent ajoute à chaque appel d'outil l'argument réservé `_gateway_exec` :
|
|
9
|
+
* `{ version, cwd, gatewayToken }`. `cwd` est son dossier de lancement.
|
|
10
|
+
* - Le MCP peut répondre par une demande d'exécution au lieu d'un résultat final :
|
|
11
|
+
* `_meta["gateway/exec"] = { id, steps }`. Chaque étape est `{ command, args, cwd, env }`,
|
|
12
|
+
* lancée sans shell ; `cwd` est résolu par rapport au dossier de lancement du relais.
|
|
13
|
+
* - Le relais agent exécute les étapes dans l'ordre et s'arrête à la première qui
|
|
14
|
+
* échoue. Il rappelle alors le même outil avec
|
|
15
|
+
* `_gateway_exec.continuation = { id, results }`, un résultat par étape exécutée.
|
|
16
|
+
* La sortie standard de chaque étape est tronquée au-delà de 1 Mio.
|
|
17
|
+
*
|
|
18
|
+
* Un outil s'écrit comme une fonction asynchrone qui appelle `run(steps)` : chaque
|
|
19
|
+
* appel suspend la fonction jusqu'au rappel du relais agent qui porte les résultats.
|
|
20
|
+
*/
|
|
21
|
+
import { randomUUID } from "crypto";
|
|
22
|
+
export const EXEC_ARG = "_gateway_exec";
|
|
23
|
+
export const EXEC_META = "gateway/exec";
|
|
24
|
+
export const EXEC_VERSION = 1;
|
|
25
|
+
/** Taille maximale de la sortie d'une étape conservée par le relais agent. */
|
|
26
|
+
export const RELAY_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
27
|
+
/** Mention ajoutée par le relais agent à la fin d'une sortie tronquée. */
|
|
28
|
+
const TRUNCATION_MARK = /\n\[sortie tronquée à \d+ octets\]$/;
|
|
29
|
+
/** Délai laissé au relais agent pour rendre les résultats d'une demande. */
|
|
30
|
+
const CONTINUATION_TTL_MS = 30 * 60 * 1000;
|
|
31
|
+
/** Vrai si le relais agent a tronqué cette sortie. */
|
|
32
|
+
export function isTruncated(output) {
|
|
33
|
+
return TRUNCATION_MARK.test(output);
|
|
34
|
+
}
|
|
35
|
+
const flows = new Map();
|
|
36
|
+
function nextEvent(flow) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
flow.emit = resolve;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function toToolResult(flow, event) {
|
|
42
|
+
if (event.kind === "done") {
|
|
43
|
+
flows.delete(flow.id);
|
|
44
|
+
return event.result;
|
|
45
|
+
}
|
|
46
|
+
if (event.kind === "failed") {
|
|
47
|
+
flows.delete(flow.id);
|
|
48
|
+
throw event.error;
|
|
49
|
+
}
|
|
50
|
+
flow.steps = event.steps;
|
|
51
|
+
flow.timer = setTimeout(() => {
|
|
52
|
+
flows.delete(flow.id);
|
|
53
|
+
flow.abort?.(new Error("Timed out waiting for the agent relay to return the command results."));
|
|
54
|
+
}, CONTINUATION_TTL_MS);
|
|
55
|
+
flow.timer.unref();
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: "Command execution request for the agent's machine, to be handled by the agent relay." }],
|
|
58
|
+
_meta: { [EXEC_META]: { id: flow.id, steps: event.steps } },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function assertAnnounced(raw) {
|
|
62
|
+
if (!raw || typeof raw !== "object") {
|
|
63
|
+
const route = process.env.MCP_SERVER_ROUTE?.trim() || "the route of this MCP";
|
|
64
|
+
throw new Error("This tool runs git on the agent's machine, through the agent relay (mcp-http-gateway --mcp), " +
|
|
65
|
+
`and the relay did not offer command execution: add ${route} to GATEWAY_EXEC_ROUTES in the "env" block ` +
|
|
66
|
+
"of the relay configuration, with a version of mcp-http-gateway that supports it.");
|
|
67
|
+
}
|
|
68
|
+
if (raw.version !== EXEC_VERSION) {
|
|
69
|
+
throw new Error(`Unsupported agent relay execution version: ${raw.version} (expected ${EXEC_VERSION}).`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function parseResults(raw, steps) {
|
|
73
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > steps.length) {
|
|
74
|
+
throw new Error("Invalid command results from the agent relay: one result is expected per step run.");
|
|
75
|
+
}
|
|
76
|
+
return raw.map((r) => ({
|
|
77
|
+
code: typeof r?.code === "number" ? r.code : null,
|
|
78
|
+
stdout: typeof r?.stdout === "string" ? r.stdout : "",
|
|
79
|
+
stderr: typeof r?.stderr === "string" ? r.stderr : "",
|
|
80
|
+
...(typeof r?.error === "string" ? { error: r.error } : {}),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Point d'entrée d'un outil qui s'exécute sur la machine de l'agent. Au premier appel,
|
|
85
|
+
* démarre `tool` ; aux rappels du relais agent, reprend la fonction là où elle attend.
|
|
86
|
+
* `exec` est la valeur de l'argument `_gateway_exec` de l'appel.
|
|
87
|
+
*/
|
|
88
|
+
export async function handleAgentExec(exec, tool) {
|
|
89
|
+
assertAnnounced(exec);
|
|
90
|
+
const continuation = exec.continuation;
|
|
91
|
+
if (continuation) {
|
|
92
|
+
const flow = flows.get(continuation.id);
|
|
93
|
+
if (!flow || !flow.resume || !flow.steps) {
|
|
94
|
+
throw new Error("Unknown or expired execution continuation (the MCP server may have restarted): call the tool again.");
|
|
95
|
+
}
|
|
96
|
+
const results = parseResults(continuation.results, flow.steps);
|
|
97
|
+
clearTimeout(flow.timer);
|
|
98
|
+
const resume = flow.resume;
|
|
99
|
+
flow.resume = undefined;
|
|
100
|
+
flow.abort = undefined;
|
|
101
|
+
const event = nextEvent(flow);
|
|
102
|
+
resume(results);
|
|
103
|
+
return toToolResult(flow, await event);
|
|
104
|
+
}
|
|
105
|
+
const flow = { id: randomUUID(), emit: () => { } };
|
|
106
|
+
flows.set(flow.id, flow);
|
|
107
|
+
const event = nextEvent(flow);
|
|
108
|
+
const run = (steps) => new Promise((resolve, reject) => {
|
|
109
|
+
flow.resume = resolve;
|
|
110
|
+
flow.abort = reject;
|
|
111
|
+
flow.emit({ kind: "exec", steps });
|
|
112
|
+
});
|
|
113
|
+
tool(run).then((result) => flow.emit({ kind: "done", result }), (error) => flow.emit({ kind: "failed", error }));
|
|
114
|
+
return toToolResult(flow, await event);
|
|
115
|
+
}
|
package/dist/src/core/git.d.ts
CHANGED
|
@@ -26,10 +26,35 @@ export interface ComputedSnapshot {
|
|
|
26
26
|
diff: FileDiff[];
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Accès au dépôt sur la machine qui le porte : git et copie de travail.
|
|
30
|
+
* Un contenu `null` dépasse ce que l'accès sait transférer.
|
|
31
|
+
*/
|
|
32
|
+
export interface RepoAccess {
|
|
33
|
+
/** Vérifie que le chemin du dépôt désigne la racine d'un dépôt git. */
|
|
34
|
+
assertRepoRoot(): Promise<void>;
|
|
35
|
+
/** Résout une ref en sha de commit ; erreur explicite si elle est inconnue. */
|
|
36
|
+
resolveCommit(ref: string, role: string): Promise<string>;
|
|
37
|
+
/** Sortie de `git diff` (DIFF_ARGS) pour `range`, par ex. `["<base>...<head>"]` ou `["HEAD"]`. */
|
|
38
|
+
diff(range: string[]): Promise<string>;
|
|
39
|
+
/** Contenu des fichiers `paths` dans le commit `commit`. */
|
|
40
|
+
readCommitFiles(commit: string, paths: string[]): Promise<Map<string, Buffer | null>>;
|
|
41
|
+
/** Fichiers non suivis et non ignorés, chemins POSIX relatifs au dépôt. */
|
|
42
|
+
untrackedFiles(): Promise<string[]>;
|
|
43
|
+
/** Contenu des fichiers `paths` de la copie de travail ; un lien symbolique donne sa cible. */
|
|
44
|
+
readWorkingFiles(paths: string[]): Promise<Map<string, Buffer | null>>;
|
|
45
|
+
}
|
|
46
|
+
export declare const DIFF_ARGS: string[];
|
|
47
|
+
/** Vrai si le contenu contient un octet nul dans ses 8000 premiers octets (même règle que git). */
|
|
48
|
+
export declare function looksBinary(content: Buffer): boolean;
|
|
49
|
+
/** Accès direct au dépôt, sur la machine de ce serveur. */
|
|
50
|
+
export declare function localRepo(repoPath: string): RepoAccess;
|
|
51
|
+
/** Vérifie une ref fournie par l'agent avant de la passer à git. */
|
|
52
|
+
export declare function assertRefShape(ref: string, role: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Calcule le snapshot d'une analyse avec git, par `repo` (accès local par défaut).
|
|
30
55
|
* - `branch` : diff `base...head` (head vaut `HEAD` par défaut), contenu lu dans le commit de tête ;
|
|
31
56
|
* - `working_tree` : diff de HEAD vers la copie de travail, index compris, fichiers non suivis
|
|
32
57
|
* présentés comme ajoutés, contenu lu sur disque.
|
|
33
58
|
* Les fichiers sont triés par chemin ; `reviewed` vaut false partout.
|
|
34
59
|
*/
|
|
35
|
-
export declare function computeSnapshot(request: SnapshotRequest): Promise<ComputedSnapshot>;
|
|
60
|
+
export declare function computeSnapshot(request: SnapshotRequest, repo?: RepoAccess): Promise<ComputedSnapshot>;
|