@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.
@@ -19,6 +19,6 @@ export async function getPrDiffHandler(args) {
19
19
  prNumber: output.prNumber,
20
20
  files: output.files,
21
21
  diffText: output.diffText,
22
- truncated: output.truncated
22
+ isTruncated: output.isTruncated
23
23
  };
24
24
  }
@@ -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 contextPath = path.join(config.repoPath, ".agent-context.json");
8
- try {
9
- const content = await fs.readFile(contextPath, "utf-8");
10
- const parsed = JSON.parse(content);
11
- return {
12
- status: "success",
13
- ...parsed
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.5",
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;