@arii/boomtick-mcp 0.3.5 → 0.3.7
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/.mcp/schemas/github.issue_update.json +5 -1
- package/.mcp/schemas/github.search_open_prs.json +8 -1
- package/CHANGELOG.md +18 -0
- package/actions/ai-review/action.yml +2 -2
- package/actions/audit/action.yml +1 -1
- package/actions/ci-validate/action.yml +1 -1
- package/actions/impact-analysis/action.yml +106 -36
- package/actions/lint-typecheck/action.yml +2 -2
- package/actions/setup/action.yml +1 -1
- package/actions/test-build/action.yml +1 -1
- package/dist/lib/td-cli.js +27 -0
- package/dist/tools/contract.js +1085 -151
- package/dist/tools/github.get_pr_diff.js +1 -1
- package/dist/tools/repo.read_agent_context.js +10 -14
- package/dist/tools/repo.read_agent_context.test.js +10 -0
- package/package.json +4 -3
- package/scripts/sync-contracts.ts +216 -6
- package/src/lib/td-cli.ts +35 -0
- package/src/tools/contract.ts +1844 -878
- package/src/tools/github.get_pr_diff.ts +1 -1
- package/src/tools/repo.read_agent_context.test.ts +11 -0
- package/src/tools/repo.read_agent_context.ts +10 -14
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import path from "path";
|
|
4
2
|
import { config } from "../config.js";
|
|
3
|
+
import { loadStandaloneAgentContext } from "../lib/td-cli.js";
|
|
5
4
|
export const ReadAgentContextInputSchema = z.object({});
|
|
6
5
|
export async function readAgentContextHandler(_args) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
catch (e) {
|
|
17
|
-
console.error("Failed to read agent context:", e.message);
|
|
18
|
-
throw new Error(`Failed to read agent context from ${contextPath}. Ensure it exists by running 'td-cli context-warm'.`);
|
|
6
|
+
const context = loadStandaloneAgentContext(config.repoPath);
|
|
7
|
+
const result = {
|
|
8
|
+
status: "success",
|
|
9
|
+
...context
|
|
10
|
+
};
|
|
11
|
+
delete result.submodules;
|
|
12
|
+
if (result.config) {
|
|
13
|
+
delete result.config.submodules;
|
|
19
14
|
}
|
|
15
|
+
return result;
|
|
20
16
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { loadStandaloneAgentContext } from "../lib/td-cli.js";
|
|
3
|
+
describe("Standalone Agent Context Loader", () => {
|
|
4
|
+
it("should load agent context without submodule references", () => {
|
|
5
|
+
const context = loadStandaloneAgentContext(process.cwd());
|
|
6
|
+
expect(context).toHaveProperty("packageName");
|
|
7
|
+
expect(context).toHaveProperty("gitCommit");
|
|
8
|
+
expect(context).not.toHaveProperty("submodules");
|
|
9
|
+
});
|
|
10
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arii/boomtick-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Boomtick MCP Guide: PR Rescue + Merge Conflict Agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.17.15",
|
|
29
29
|
"json-schema-to-zod": "^2.8.1",
|
|
30
|
-
"tsx": "^4.22.4",
|
|
31
30
|
"shx": "^0.4.0",
|
|
31
|
+
"tsx": "^4.22.4",
|
|
32
32
|
"typescript": "^5.7.3",
|
|
33
|
-
"vitest": "^4.1.9"
|
|
33
|
+
"vitest": "^4.1.9",
|
|
34
|
+
"yaml": "^2.9.0"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
|
36
37
|
"build": "tsx scripts/sync-contracts.ts && tsc && shx cp src/tools/ddgs_search.py dist/tools/ && tsx scripts/sync-mcp-schemas.ts",
|
|
@@ -1,10 +1,226 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
interface InterfaceField {
|
|
6
|
+
name: string;
|
|
7
|
+
isOptional: boolean;
|
|
8
|
+
tsType: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ParsedInterface {
|
|
12
|
+
name: string;
|
|
13
|
+
extendsFrom?: string;
|
|
14
|
+
fields: InterfaceField[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveLibPath(filename: string): string {
|
|
18
|
+
let p = path.join(process.cwd(), 'lib', filename);
|
|
19
|
+
if (!fs.existsSync(p)) {
|
|
20
|
+
p = path.join(process.cwd(), '../lib', filename);
|
|
21
|
+
}
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function camelToSnake(str: string): string {
|
|
26
|
+
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mapTsTypeToPython(tsType: string, typeMap: Map<string, string>): string {
|
|
30
|
+
tsType = tsType.trim();
|
|
31
|
+
if (tsType.includes('|')) {
|
|
32
|
+
const parts = tsType.split('|').map(p => p.trim());
|
|
33
|
+
const isAllLiterals = parts.every(p => (p.startsWith("'") && p.endsWith("'")) || (p.startsWith('"') && p.endsWith('"')));
|
|
34
|
+
if (isAllLiterals) {
|
|
35
|
+
return `Literal[${parts.map(p => p.replace(/"/g, "'")).join(', ')}]`;
|
|
36
|
+
}
|
|
37
|
+
const mappedParts = parts.map(p => mapTsTypeToPython(p, typeMap));
|
|
38
|
+
return `Union[${mappedParts.join(', ')}]`;
|
|
39
|
+
}
|
|
40
|
+
if (tsType.endsWith('[]')) {
|
|
41
|
+
const baseType = tsType.slice(0, -2);
|
|
42
|
+
const pyBase = mapTsTypeToPython(baseType, typeMap);
|
|
43
|
+
return `List[${pyBase}]`;
|
|
44
|
+
}
|
|
45
|
+
if (tsType.startsWith('{') && tsType.endsWith('}')) {
|
|
46
|
+
return 'Dict[str, Any]';
|
|
47
|
+
}
|
|
48
|
+
if (typeMap.has(tsType)) {
|
|
49
|
+
return tsType; // Just use the alias name
|
|
50
|
+
}
|
|
51
|
+
switch (tsType) {
|
|
52
|
+
case 'string': return 'str';
|
|
53
|
+
case 'boolean': return 'bool';
|
|
54
|
+
case 'number': return 'Union[int, float]';
|
|
55
|
+
case 'void': return 'None';
|
|
56
|
+
case 'any':
|
|
57
|
+
case 'unknown': return 'Any';
|
|
58
|
+
default: return tsType;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mapFieldTsTypeToPython(fieldName: string, tsType: string, typeMap: Map<string, string>): string {
|
|
63
|
+
const normType = tsType.trim();
|
|
64
|
+
if (normType === 'number') {
|
|
65
|
+
const lower = fieldName.toLowerCase();
|
|
66
|
+
if (lower.includes('tokens') || lower === 'token' || lower.includes('count') || lower.includes('line') || lower.includes('number') || lower.includes('char') || lower.includes('ms') || lower === 'id') {
|
|
67
|
+
return 'int';
|
|
68
|
+
}
|
|
69
|
+
if (lower.includes('cost') || lower.includes('percent') || lower.includes('diff')) {
|
|
70
|
+
return 'float';
|
|
71
|
+
}
|
|
72
|
+
return 'Union[int, float]';
|
|
73
|
+
}
|
|
74
|
+
return mapTsTypeToPython(tsType, typeMap);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseInterfaceFields(body: string): InterfaceField[] {
|
|
78
|
+
const fields: InterfaceField[] = [];
|
|
79
|
+
const lines = body.split('\n');
|
|
80
|
+
let i = 0;
|
|
81
|
+
while (i < lines.length) {
|
|
82
|
+
const line = lines[i].trim();
|
|
83
|
+
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
|
84
|
+
i++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const match = line.match(/^(\w+)(\??)\s*:\s*([^;]+);/);
|
|
88
|
+
if (match) {
|
|
89
|
+
const name = match[1];
|
|
90
|
+
const isOptional = match[2] === '?';
|
|
91
|
+
const tsType = match[3].trim();
|
|
92
|
+
fields.push({ name, isOptional, tsType });
|
|
93
|
+
}
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
return fields;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function generatePythonContracts() {
|
|
100
|
+
const codeReviewPath = resolveLibPath('codeReviewTypes.ts');
|
|
101
|
+
const visualReviewPath = resolveLibPath('visualReviewTypes.ts');
|
|
102
|
+
const aiLoggerPath = resolveLibPath('aiLogger.ts');
|
|
103
|
+
|
|
104
|
+
const typeMap = new Map<string, string>();
|
|
105
|
+
const parsedInterfaces: ParsedInterface[] = [];
|
|
106
|
+
|
|
107
|
+
for (const filePath of [codeReviewPath, visualReviewPath, aiLoggerPath]) {
|
|
108
|
+
if (!fs.existsSync(filePath)) continue;
|
|
109
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
110
|
+
|
|
111
|
+
// Match type aliases
|
|
112
|
+
const typeAliasRegex = /export\s+type\s+(\w+)\s*=\s*([^;]+);/g;
|
|
113
|
+
let match;
|
|
114
|
+
while ((match = typeAliasRegex.exec(content)) !== null) {
|
|
115
|
+
const name = match[1];
|
|
116
|
+
const tsType = match[2].trim();
|
|
117
|
+
const pyType = mapTsTypeToPython(tsType, typeMap);
|
|
118
|
+
typeMap.set(name, pyType);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Match interfaces
|
|
122
|
+
const interfaceRegex = /export\s+interface\s+(\w+)(?:\s+extends\s+(\w+))?\s*\{([\s\S]*?)\}/g;
|
|
123
|
+
while ((match = interfaceRegex.exec(content)) !== null) {
|
|
124
|
+
const name = match[1];
|
|
125
|
+
const extendsFrom = match[2];
|
|
126
|
+
const body = match[3];
|
|
127
|
+
const fields = parseInterfaceFields(body);
|
|
128
|
+
parsedInterfaces.push({ name, extendsFrom, fields });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const targetInterfaces = [
|
|
133
|
+
'IssueSummary',
|
|
134
|
+
'ReviewFinding',
|
|
135
|
+
'CodeReviewState',
|
|
136
|
+
'CodeReviewResult',
|
|
137
|
+
'VisualReviewFinding',
|
|
138
|
+
'RouteReview',
|
|
139
|
+
'AIRunLogEntry'
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
let pyContent = `# Automatically generated by sync-contracts.ts. Do not edit manually.\n`;
|
|
143
|
+
pyContent += `# pylint: disable=invalid-name,missing-class-docstring,missing-module-docstring,line-too-long,unused-import\n`;
|
|
144
|
+
pyContent += `from typing import Any, Dict, List, Literal, Optional, Union\n`;
|
|
145
|
+
pyContent += `from pydantic import Field\n`;
|
|
146
|
+
pyContent += `from dev_tools.models.base import CamelCaseModel\n\n`;
|
|
147
|
+
|
|
148
|
+
for (const [name, pyType] of typeMap.entries()) {
|
|
149
|
+
if (name === 'CodeReviewParseError') {
|
|
150
|
+
pyContent += `${name} = ${pyType}\n\n`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const targetName of targetInterfaces) {
|
|
155
|
+
const iface = parsedInterfaces.find(i => i.name === targetName);
|
|
156
|
+
if (!iface) {
|
|
157
|
+
console.warn(`⚠️ Warning: Interface ${targetName} not found in TS source files.`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const baseClass = iface.extendsFrom ? iface.extendsFrom : 'CamelCaseModel';
|
|
162
|
+
pyContent += `class ${iface.name}(${baseClass}):\n`;
|
|
163
|
+
|
|
164
|
+
const fields = iface.fields;
|
|
165
|
+
if (fields.length === 0) {
|
|
166
|
+
pyContent += ` pass\n\n`;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const required: InterfaceField[] = [];
|
|
171
|
+
const optional: InterfaceField[] = [];
|
|
172
|
+
for (const f of fields) {
|
|
173
|
+
if (f.isOptional) {
|
|
174
|
+
optional.push(f);
|
|
175
|
+
} else {
|
|
176
|
+
required.push(f);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sortedFields = [...required, ...optional];
|
|
181
|
+
|
|
182
|
+
for (const f of sortedFields) {
|
|
183
|
+
const snakeName = camelToSnake(f.name);
|
|
184
|
+
let pyType = mapFieldTsTypeToPython(f.name, f.tsType, typeMap);
|
|
185
|
+
|
|
186
|
+
if (f.isOptional) {
|
|
187
|
+
pyContent += ` ${snakeName}: Optional[${pyType}] = None\n`;
|
|
188
|
+
} else {
|
|
189
|
+
if (pyType.startsWith('List[')) {
|
|
190
|
+
pyContent += ` ${snakeName}: ${pyType} = Field(default_factory=list)\n`;
|
|
191
|
+
} else {
|
|
192
|
+
pyContent += ` ${snakeName}: ${pyType}\n`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
pyContent += `\n`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let pyPath = path.join(process.cwd(), 'cli/dev_tools/models/contracts.py');
|
|
200
|
+
if (!fs.existsSync(path.dirname(pyPath))) {
|
|
201
|
+
pyPath = path.join(process.cwd(), '../cli/dev_tools/models/contracts.py');
|
|
202
|
+
}
|
|
203
|
+
fs.writeFileSync(pyPath, pyContent.trim() + '\n');
|
|
204
|
+
console.log(`Generated ${pyPath}`);
|
|
205
|
+
}
|
|
3
206
|
|
|
4
207
|
/**
|
|
5
208
|
* Synchronizes contracts from JSON schema to Zod.
|
|
6
209
|
*/
|
|
7
210
|
async function syncContracts() {
|
|
211
|
+
// First, generate Python models from TS contracts
|
|
212
|
+
generatePythonContracts();
|
|
213
|
+
|
|
214
|
+
// Next, regenerate the python schema so it includes our generated classes
|
|
215
|
+
let rootDir = process.cwd();
|
|
216
|
+
if (!fs.existsSync(path.join(rootDir, 'pnpm-workspace.yaml'))) {
|
|
217
|
+
rootDir = path.dirname(rootDir);
|
|
218
|
+
}
|
|
219
|
+
const pythonScriptPath = path.join(rootDir, 'cli/dev_tools/schema_gen.py');
|
|
220
|
+
console.log(`Regenerating cli-schema.json...`);
|
|
221
|
+
execFileSync('python3', [pythonScriptPath], { cwd: rootDir, stdio: 'inherit' });
|
|
222
|
+
|
|
223
|
+
// Then perform the original Zod contract generation
|
|
8
224
|
let jsonSchemaToZod;
|
|
9
225
|
try {
|
|
10
226
|
const module = await import('json-schema-to-zod');
|
|
@@ -23,13 +239,11 @@ async function syncContracts() {
|
|
|
23
239
|
let outputPath = path.join(process.cwd(), 'mcp/src/tools/contract.ts');
|
|
24
240
|
|
|
25
241
|
if (!fs.existsSync(schemaPath)) {
|
|
26
|
-
// Try relative to package root if in extracted mode (run from root)
|
|
27
242
|
schemaPath = path.join(process.cwd(), 'cli/dev_tools/cli-schema.json');
|
|
28
243
|
outputPath = path.join(process.cwd(), 'mcp/src/tools/contract.ts');
|
|
29
244
|
}
|
|
30
245
|
|
|
31
246
|
if (!fs.existsSync(schemaPath)) {
|
|
32
|
-
// Try if run from mcp directory
|
|
33
247
|
schemaPath = path.join(process.cwd(), '../cli/dev_tools/cli-schema.json');
|
|
34
248
|
outputPath = path.join(process.cwd(), 'src/tools/contract.ts');
|
|
35
249
|
}
|
|
@@ -46,24 +260,20 @@ async function syncContracts() {
|
|
|
46
260
|
tsContent += `import { z } from "zod";\n\n`;
|
|
47
261
|
|
|
48
262
|
for (const [name, schema] of Object.entries(models)) {
|
|
49
|
-
// Get the raw Zod expression without any variable assignment or imports
|
|
50
263
|
const zodExpression = jsonSchemaToZod(schema as any, { module: 'none' });
|
|
51
264
|
|
|
52
265
|
tsContent += `export const ${name}Schema = ${zodExpression};\n\n`;
|
|
53
266
|
tsContent += `export type ${name} = z.infer<typeof ${name}Schema>;\n\n`;
|
|
54
267
|
|
|
55
|
-
// Also export the raw JSON schema for MCP definitions
|
|
56
268
|
tsContent += `export const ${name}JsonSchema = ${JSON.stringify(schema, null, 2)} as const;\n\n`;
|
|
57
269
|
}
|
|
58
270
|
|
|
59
|
-
// Normalize newlines to \n and ensure exactly one trailing newline at EOF
|
|
60
271
|
const finalContent = tsContent.replace(/\r\n/g, '\n').trimEnd() + '\n';
|
|
61
272
|
|
|
62
273
|
fs.writeFileSync(outputPath, finalContent);
|
|
63
274
|
console.log(`Generated ${outputPath}`);
|
|
64
275
|
}
|
|
65
276
|
|
|
66
|
-
// Ensure execution is awaited and errors are caught
|
|
67
277
|
(async () => {
|
|
68
278
|
try {
|
|
69
279
|
await syncContracts();
|
package/src/lib/td-cli.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { runCommand } from "../lib/shell.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
2
5
|
|
|
3
6
|
export interface TDCliResponse {
|
|
4
7
|
status: string;
|
|
@@ -6,6 +9,38 @@ export interface TDCliResponse {
|
|
|
6
9
|
[key: string]: any;
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
export interface AgentContext {
|
|
13
|
+
packageName: string;
|
|
14
|
+
gitCommit: string;
|
|
15
|
+
config: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const loadStandaloneAgentContext = (repoRoot: string = process.cwd()): AgentContext => {
|
|
19
|
+
const configPath = path.join(repoRoot, "project_config.json");
|
|
20
|
+
let config: Record<string, unknown> = {};
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(configPath)) {
|
|
23
|
+
try {
|
|
24
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.warn(`[MCP Warning] Failed to parse ${configPath}:`, e);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let gitCommit = "unknown";
|
|
31
|
+
try {
|
|
32
|
+
gitCommit = execSync("git rev-parse HEAD", { cwd: repoRoot, encoding: "utf-8" }).trim();
|
|
33
|
+
} catch {
|
|
34
|
+
// Fallback if git is unavailable (e.g. Docker container without git)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
packageName: (config.packageName as string) ?? "boomtick",
|
|
39
|
+
gitCommit,
|
|
40
|
+
config,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
9
44
|
function tryParseJson(text: string): TDCliResponse | null {
|
|
10
45
|
try {
|
|
11
46
|
return JSON.parse(text) as TDCliResponse;
|