@agilemonster/mcp-server 1.0.11 → 1.0.14
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/dist/client.d.ts +19 -2
- package/dist/client.js +132 -2
- package/dist/server.js +222 -27
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgileMonsterProjectConfig, AutonomousStepResponse, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
|
|
2
|
-
export declare const MCP_SERVER_VERSION = "1.0.
|
|
2
|
+
export declare const MCP_SERVER_VERSION = "1.0.14";
|
|
3
3
|
export declare class AgileMonsterClient {
|
|
4
4
|
private apiKey;
|
|
5
5
|
private baseUrl;
|
|
@@ -36,7 +36,20 @@ export declare class AgileMonsterClient {
|
|
|
36
36
|
projectName?: string;
|
|
37
37
|
projectPath?: string;
|
|
38
38
|
discoveredProjects?: DiscoveredProject[];
|
|
39
|
+
discoveredModels?: Record<string, {
|
|
40
|
+
default_model?: string | null;
|
|
41
|
+
models: Record<string, string> | string[];
|
|
42
|
+
}>;
|
|
39
43
|
}): Promise<PulseResponse>;
|
|
44
|
+
/**
|
|
45
|
+
* Discover live models installed/configured across the developer's local AI coding agents
|
|
46
|
+
* (Codex, Cursor, Claude Code, Hermes, Antigravity) so the Agile Monster app & website
|
|
47
|
+
* always display the real models available for each selected agent.
|
|
48
|
+
*/
|
|
49
|
+
discoverLocalAgentModels(): Record<string, {
|
|
50
|
+
default_model?: string | null;
|
|
51
|
+
models: Record<string, string>;
|
|
52
|
+
}>;
|
|
40
53
|
/**
|
|
41
54
|
* Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
|
|
42
55
|
* to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
|
|
@@ -51,7 +64,11 @@ export declare class AgileMonsterClient {
|
|
|
51
64
|
addProgress(cardId: string, provider: string, update: string, tokensUsed?: number, costUsd?: number): Promise<Card>;
|
|
52
65
|
recordSpend(cardId: string, provider: string, tokens: number, costUsd: number, note?: string): Promise<Card>;
|
|
53
66
|
setBudget(cardId: string, maxBudgetUsd?: number | null, maxTokens?: number | null): Promise<Card>;
|
|
54
|
-
submitReview(cardId: string, provider: string, summary: string, verificationSteps?: string, targetList?: string, assignToUserId?: string
|
|
67
|
+
submitReview(cardId: string, provider: string, summary: string, verificationSteps?: string, targetList?: string, assignToUserId?: string, extra?: {
|
|
68
|
+
diffText?: string;
|
|
69
|
+
logText?: string;
|
|
70
|
+
filesModified?: string;
|
|
71
|
+
}): Promise<Card>;
|
|
55
72
|
completeTask(cardId: string, provider: string, comment?: string): Promise<Card>;
|
|
56
73
|
blockTask(cardId: string, provider: string, comment?: string): Promise<Card>;
|
|
57
74
|
createFollowupTasks(cardId: string, provider: string, tasks: Array<{
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
export const MCP_SERVER_VERSION = '1.0.
|
|
4
|
+
export const MCP_SERVER_VERSION = '1.0.14';
|
|
5
5
|
export class AgileMonsterClient {
|
|
6
6
|
apiKey = '';
|
|
7
7
|
baseUrl;
|
|
@@ -196,9 +196,136 @@ export class AgileMonsterClient {
|
|
|
196
196
|
project_path: options.projectPath,
|
|
197
197
|
mcp_version: MCP_SERVER_VERSION,
|
|
198
198
|
discovered_projects: options.discoveredProjects,
|
|
199
|
+
discovered_models: options.discoveredModels,
|
|
199
200
|
}),
|
|
200
201
|
});
|
|
201
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Discover live models installed/configured across the developer's local AI coding agents
|
|
205
|
+
* (Codex, Cursor, Claude Code, Hermes, Antigravity) so the Agile Monster app & website
|
|
206
|
+
* always display the real models available for each selected agent.
|
|
207
|
+
*/
|
|
208
|
+
discoverLocalAgentModels() {
|
|
209
|
+
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
|
210
|
+
const result = {};
|
|
211
|
+
if (!homedir)
|
|
212
|
+
return result;
|
|
213
|
+
// 1. Codex (~/.codex/models_cache.json & ~/.codex/config.toml)
|
|
214
|
+
try {
|
|
215
|
+
const codexCachePath = path.join(homedir, '.codex', 'models_cache.json');
|
|
216
|
+
const codexConfigPath = path.join(homedir, '.codex', 'config.toml');
|
|
217
|
+
let defaultCodex = null;
|
|
218
|
+
if (fs.existsSync(codexConfigPath)) {
|
|
219
|
+
const toml = fs.readFileSync(codexConfigPath, 'utf8');
|
|
220
|
+
const m = toml.match(/^\s*model\s*=\s*["']([^"']+)["']/m);
|
|
221
|
+
if (m?.[1])
|
|
222
|
+
defaultCodex = m[1].trim();
|
|
223
|
+
}
|
|
224
|
+
if (fs.existsSync(codexCachePath)) {
|
|
225
|
+
const raw = JSON.parse(fs.readFileSync(codexCachePath, 'utf8'));
|
|
226
|
+
const list = Array.isArray(raw?.models) ? raw.models : [];
|
|
227
|
+
const models = {};
|
|
228
|
+
for (const item of list) {
|
|
229
|
+
const slug = String(item?.slug || item?.id || '').trim();
|
|
230
|
+
if (!slug)
|
|
231
|
+
continue;
|
|
232
|
+
const label = String(item?.display_name || slug).trim();
|
|
233
|
+
models[slug] = label;
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(models).length > 0) {
|
|
236
|
+
result.codex = { default_model: defaultCodex, models };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
// 2. Cursor (~/Library/Application Support/Cursor/User/globalStorage/state.vscdb)
|
|
242
|
+
try {
|
|
243
|
+
const cursorDb = path.join(homedir, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
244
|
+
if (fs.existsSync(cursorDb)) {
|
|
245
|
+
const out = execSync(`sqlite3 "${cursorDb}" "SELECT value FROM ItemTable WHERE key = 'src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser' LIMIT 1;"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000 }).trim();
|
|
246
|
+
if (out) {
|
|
247
|
+
const data = JSON.parse(out);
|
|
248
|
+
const aiSettings = data?.aiSettings || {};
|
|
249
|
+
const defaultCursor = aiSettings?.modelConfig?.composer?.modelName ||
|
|
250
|
+
aiSettings?.openAIModel ||
|
|
251
|
+
null;
|
|
252
|
+
const avail = Array.isArray(data?.availableDefaultModels2) ? data.availableDefaultModels2 : [];
|
|
253
|
+
const models = {};
|
|
254
|
+
for (const m of avail) {
|
|
255
|
+
const name = String(m?.name || '').trim();
|
|
256
|
+
if (!name)
|
|
257
|
+
continue;
|
|
258
|
+
const label = String(m?.clientDisplayName || m?.serverModelName || name).trim();
|
|
259
|
+
models[name] = label;
|
|
260
|
+
}
|
|
261
|
+
if (Object.keys(models).length > 0) {
|
|
262
|
+
result.cursor = { default_model: defaultCursor, models };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch { }
|
|
268
|
+
// 3. Claude Code (~/.claude.json)
|
|
269
|
+
try {
|
|
270
|
+
const claudePath = path.join(homedir, '.claude.json');
|
|
271
|
+
if (fs.existsSync(claudePath)) {
|
|
272
|
+
const data = JSON.parse(fs.readFileSync(claudePath, 'utf8'));
|
|
273
|
+
const usageKeys = Object.keys(data?.CachedGrowthBookFeatures?.tengu_model_config || {});
|
|
274
|
+
const models = {
|
|
275
|
+
'claude-opus-5-5': 'Claude Opus 5.5',
|
|
276
|
+
'claude-sonnet-5-5': 'Claude Sonnet 5.5',
|
|
277
|
+
'claude-haiku-5-5': 'Claude Haiku 5.5',
|
|
278
|
+
'claude-opus-5': 'Claude Opus 5',
|
|
279
|
+
'claude-sonnet-5': 'Claude Sonnet 5',
|
|
280
|
+
'claude-fable-5-1': 'Claude Fable 5.1',
|
|
281
|
+
'claude-opus-4-8': 'Claude Opus 4.8',
|
|
282
|
+
'claude-opus-4-7': 'Claude Opus 4.7',
|
|
283
|
+
'claude-opus-4-6': 'Claude Opus 4.6',
|
|
284
|
+
'claude-sonnet-4-6': 'Claude Sonnet 4.6',
|
|
285
|
+
'claude-haiku-4-5': 'Claude Haiku 4.5',
|
|
286
|
+
};
|
|
287
|
+
for (const k of usageKeys) {
|
|
288
|
+
if (k.startsWith('claude-') && !models[k]) {
|
|
289
|
+
models[k] = k;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
result.claude_code = { default_model: 'claude-sonnet-5-5', models };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch { }
|
|
296
|
+
// 4. Hermes (~/.hermes/config.yaml)
|
|
297
|
+
try {
|
|
298
|
+
const hermesPath = path.join(homedir, '.hermes', 'config.yaml');
|
|
299
|
+
if (fs.existsSync(hermesPath)) {
|
|
300
|
+
const yaml = fs.readFileSync(hermesPath, 'utf8');
|
|
301
|
+
const defMatch = yaml.match(/^\s*default\s*:\s*([^\r\n#]+)/m);
|
|
302
|
+
const defModel = defMatch?.[1] ? defMatch[1].trim().replace(/^["']|["']$/g, '') : 'qwen/qwen3.8-27b';
|
|
303
|
+
const models = {};
|
|
304
|
+
if (defModel)
|
|
305
|
+
models[defModel] = `${defModel} (Default)`;
|
|
306
|
+
models['qwen/qwen3.8-27b'] = 'Qwen 3.8 27B (LM Studio)';
|
|
307
|
+
models['qwen3.5:397b'] = 'Qwen 3.5 397B';
|
|
308
|
+
models['glm-4.7'] = 'GLM 4.7';
|
|
309
|
+
models['theta-1'] = 'Theta 1';
|
|
310
|
+
models['claude-opus-4-6'] = 'Claude Opus 4.6';
|
|
311
|
+
models['google/gemini-3-flash-preview'] = 'Gemini 3 Flash Preview';
|
|
312
|
+
result.hermes = { default_model: defModel, models };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch { }
|
|
316
|
+
// 5. Antigravity
|
|
317
|
+
result.antigravity = {
|
|
318
|
+
default_model: 'gemini-3.1-pro',
|
|
319
|
+
models: {
|
|
320
|
+
'gemini-3.1-pro': 'Gemini 3.1 Pro',
|
|
321
|
+
'gemini-3.0-flash': 'Gemini 3.0 Flash',
|
|
322
|
+
'claude-opus-5-5': 'Claude Opus 5.5',
|
|
323
|
+
'claude-sonnet-5-5': 'Claude Sonnet 5.5',
|
|
324
|
+
'gpt-6.1-sol': 'GPT-6.1 Sol',
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
202
329
|
/**
|
|
203
330
|
* Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
|
|
204
331
|
* to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
|
|
@@ -344,7 +471,7 @@ export class AgileMonsterClient {
|
|
|
344
471
|
}),
|
|
345
472
|
});
|
|
346
473
|
}
|
|
347
|
-
async submitReview(cardId, provider, summary, verificationSteps, targetList, assignToUserId) {
|
|
474
|
+
async submitReview(cardId, provider, summary, verificationSteps, targetList, assignToUserId, extra) {
|
|
348
475
|
return this.request(`/api/agent/cards/${cardId}/submit-review`, {
|
|
349
476
|
method: 'POST',
|
|
350
477
|
body: JSON.stringify({
|
|
@@ -355,6 +482,9 @@ export class AgileMonsterClient {
|
|
|
355
482
|
target_list_id: targetList,
|
|
356
483
|
target_list_name: targetList,
|
|
357
484
|
assign_to_user_id: assignToUserId,
|
|
485
|
+
diff_text: extra?.diffText,
|
|
486
|
+
log_text: extra?.logText,
|
|
487
|
+
files_modified: extra?.filesModified,
|
|
358
488
|
}),
|
|
359
489
|
});
|
|
360
490
|
}
|
package/dist/server.js
CHANGED
|
@@ -863,19 +863,24 @@ function collectProjectSourceFiles(projectPath, keywords) {
|
|
|
863
863
|
}
|
|
864
864
|
catch { }
|
|
865
865
|
}
|
|
866
|
-
// Bonus score for UI components / screens
|
|
866
|
+
// Bonus score for UI components / screens AND backend controllers / models
|
|
867
867
|
if (ext === '.tsx' || ext === '.jsx' || ext === '.dart' || ext === '.vue') {
|
|
868
|
-
if (lowerRel.includes('component') || lowerRel.includes('screen') || lowerRel.includes('widget') || lowerRel.includes('modal')) {
|
|
868
|
+
if (lowerRel.includes('component') || lowerRel.includes('screen') || lowerRel.includes('widget') || lowerRel.includes('modal') || lowerRel.includes('model')) {
|
|
869
869
|
score += 8;
|
|
870
870
|
}
|
|
871
871
|
}
|
|
872
|
+
if (ext === '.php' || ext === '.py' || ext === '.ts') {
|
|
873
|
+
if (lowerRel.includes('controller') || lowerRel.includes('service') || lowerRel.includes('route')) {
|
|
874
|
+
score += 10;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
872
877
|
results.push({ rel, score });
|
|
873
878
|
}
|
|
874
879
|
}
|
|
875
880
|
};
|
|
876
881
|
walk(projectPath, 0);
|
|
877
882
|
results.sort((a, b) => b.score - a.score || a.rel.localeCompare(b.rel));
|
|
878
|
-
return results.slice(0,
|
|
883
|
+
return results.slice(0, 250).map((r) => r.rel);
|
|
879
884
|
}
|
|
880
885
|
function getAgileMonsterLocksDir() {
|
|
881
886
|
const dir = path.join(os.homedir(), '.agilemonster', 'locks');
|
|
@@ -887,20 +892,79 @@ function getAgileMonsterLocksDir() {
|
|
|
887
892
|
catch { }
|
|
888
893
|
return dir;
|
|
889
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Append a live, human-readable IDE activity entry to `<projectPath>/.agilemonster/ACTIVITY.md`
|
|
897
|
+
* (and exclude `.agilemonster/` in `.git/info/exclude` so it never pollutes `git status`).
|
|
898
|
+
* Developers can open `.agilemonster/ACTIVITY.md` in their IDE at any time to see full agent
|
|
899
|
+
* troubleshooting logs, files inspected, build verification output, and diffs.
|
|
900
|
+
*/
|
|
901
|
+
function appendIdeActivityLog(projectPath, entry) {
|
|
902
|
+
try {
|
|
903
|
+
const amDir = path.join(projectPath, '.agilemonster');
|
|
904
|
+
if (!fs.existsSync(amDir)) {
|
|
905
|
+
fs.mkdirSync(amDir, { recursive: true });
|
|
906
|
+
}
|
|
907
|
+
const gitExclude = path.join(projectPath, '.git', 'info', 'exclude');
|
|
908
|
+
if (fs.existsSync(gitExclude)) {
|
|
909
|
+
const exContent = fs.readFileSync(gitExclude, 'utf8');
|
|
910
|
+
if (!exContent.includes('.agilemonster/')) {
|
|
911
|
+
fs.appendFileSync(gitExclude, '\n.agilemonster/\n.agilemonster.json\n', 'utf8');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const logPath = path.join(amDir, 'ACTIVITY.md');
|
|
915
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
916
|
+
const header = !fs.existsSync(logPath)
|
|
917
|
+
? `# 🤖 Agile Monster — Live IDE Agent Activity Log\n\nThis file is automatically updated by the Agile Monster agent daemon so you can inspect every task, model, file change, and build check directly in your IDE.\n\n---\n\n`
|
|
918
|
+
: '';
|
|
919
|
+
const lines = [
|
|
920
|
+
`## [${timestamp}] ${entry.cardTitle} (\`${entry.cardId.slice(0, 8)}\`)`,
|
|
921
|
+
`- **Agent**: ${entry.agentName} | **Model**: \`${entry.model || 'auto'}\` | **Status**: \`${entry.status}\``,
|
|
922
|
+
];
|
|
923
|
+
if (entry.files && entry.files.length > 0) {
|
|
924
|
+
lines.push(`- **Files**: ${entry.files.map((f) => `\`${f}\``).join(', ')}`);
|
|
925
|
+
}
|
|
926
|
+
lines.push(`- **Summary**: ${entry.summary}`);
|
|
927
|
+
if (entry.diff && entry.diff.trim().length > 0) {
|
|
928
|
+
const clippedDiff = entry.diff.length > 4000 ? entry.diff.slice(0, 4000) + '\n... (truncated)' : entry.diff;
|
|
929
|
+
lines.push('', '```diff', clippedDiff.trim(), '```');
|
|
930
|
+
}
|
|
931
|
+
lines.push('', '---', '');
|
|
932
|
+
fs.appendFileSync(logPath, header + lines.join('\n'), 'utf8');
|
|
933
|
+
}
|
|
934
|
+
catch { }
|
|
935
|
+
}
|
|
890
936
|
export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
891
937
|
if (!projectPath || !fs.existsSync(projectPath))
|
|
892
938
|
return false;
|
|
893
939
|
if (activeAutonomousCards.has(card.id))
|
|
894
940
|
return false;
|
|
895
941
|
const lockFile = path.join(getAgileMonsterLocksDir(), `task-${card.id.slice(0, 8)}.lock`);
|
|
942
|
+
const cardUpdatedAt = card.updated_at || '';
|
|
896
943
|
try {
|
|
897
944
|
if (fs.existsSync(lockFile)) {
|
|
898
945
|
const stat = fs.statSync(lockFile);
|
|
899
|
-
|
|
900
|
-
|
|
946
|
+
let existingLock = {};
|
|
947
|
+
try {
|
|
948
|
+
existingLock = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
949
|
+
}
|
|
950
|
+
catch { }
|
|
951
|
+
const sameRevision = cardUpdatedAt !== '' && existingLock?.card_updated_at === cardUpdatedAt;
|
|
952
|
+
if (existingLock?.status === 'running' && Date.now() - stat.mtimeMs < 3 * 60 * 1000 && sameRevision) {
|
|
953
|
+
return false; // Currently running on this exact revision
|
|
954
|
+
}
|
|
955
|
+
if (existingLock?.status === 'completed' && sameRevision && Date.now() - stat.mtimeMs < 10 * 60 * 1000) {
|
|
956
|
+
return false; // Already completed this exact revision
|
|
957
|
+
}
|
|
958
|
+
if (!cardUpdatedAt && Date.now() - stat.mtimeMs < 3 * 60 * 1000) {
|
|
959
|
+
return false;
|
|
901
960
|
}
|
|
902
961
|
}
|
|
903
|
-
fs.writeFileSync(lockFile, JSON.stringify({
|
|
962
|
+
fs.writeFileSync(lockFile, JSON.stringify({
|
|
963
|
+
card_id: card.id,
|
|
964
|
+
card_updated_at: cardUpdatedAt,
|
|
965
|
+
status: 'running',
|
|
966
|
+
started_at: new Date().toISOString(),
|
|
967
|
+
}), 'utf8');
|
|
904
968
|
}
|
|
905
969
|
catch {
|
|
906
970
|
return false;
|
|
@@ -975,52 +1039,149 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
975
1039
|
if (currentContent.includes(edit.search)) {
|
|
976
1040
|
const updatedContent = currentContent.replace(edit.search, edit.replace);
|
|
977
1041
|
if (updatedContent !== currentContent) {
|
|
978
|
-
// Guard against unbalanced braces introduced by partial block replacement
|
|
1042
|
+
// Guard against unbalanced braces/parens/brackets introduced by partial block replacement
|
|
979
1043
|
const countChar = (s, ch) => s.split(ch).length - 1;
|
|
980
1044
|
const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
|
|
981
1045
|
const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
|
|
982
|
-
|
|
1046
|
+
const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
|
|
1047
|
+
const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
|
|
1048
|
+
if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
|
|
983
1049
|
continue;
|
|
984
1050
|
}
|
|
985
1051
|
fs.writeFileSync(fullPath, updatedContent, 'utf8');
|
|
1052
|
+
// Pre-submit static analysis gate for Dart/Flutter and PHP files
|
|
1053
|
+
if (relPath.endsWith('.dart')) {
|
|
1054
|
+
try {
|
|
1055
|
+
execSync(`dart analyze "${fullPath}"`, {
|
|
1056
|
+
cwd: projectPath,
|
|
1057
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1058
|
+
timeout: 25000,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
catch (analyzeErr) {
|
|
1062
|
+
const out = String(analyzeErr?.stdout || '') + String(analyzeErr?.stderr || '');
|
|
1063
|
+
if (/\berror\s+[•-]/i.test(out)) {
|
|
1064
|
+
// Revert broken Dart edit so it never breaks the build
|
|
1065
|
+
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
else if (relPath.endsWith('.php')) {
|
|
1071
|
+
try {
|
|
1072
|
+
execSync(`php -l "${fullPath}"`, {
|
|
1073
|
+
cwd: projectPath,
|
|
1074
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1075
|
+
timeout: 10000,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
986
1083
|
if (!modifiedFiles.includes(relPath)) {
|
|
987
1084
|
modifiedFiles.push(relPath);
|
|
988
1085
|
}
|
|
989
1086
|
}
|
|
990
1087
|
}
|
|
991
1088
|
}
|
|
1089
|
+
// Pre-submit frontend build verification gate for JS/TS/React/Vite/Next.js projects
|
|
1090
|
+
const hasFrontendEdits = modifiedFiles.some((f) => /\.(tsx?|jsx?|vue)$/i.test(f));
|
|
1091
|
+
const pkgJsonPath = path.join(projectPath, 'package.json');
|
|
1092
|
+
if (hasFrontendEdits && fs.existsSync(pkgJsonPath)) {
|
|
1093
|
+
try {
|
|
1094
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
1095
|
+
if (pkg?.scripts?.build && fs.existsSync(path.join(projectPath, 'node_modules'))) {
|
|
1096
|
+
execSync('npm run build', {
|
|
1097
|
+
cwd: projectPath,
|
|
1098
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1099
|
+
timeout: 45000,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (buildErr) {
|
|
1104
|
+
// Revert broken frontend edits so we never leave the project with a broken build
|
|
1105
|
+
for (const [fPath, orig] of originalContents.entries()) {
|
|
1106
|
+
try {
|
|
1107
|
+
fs.writeFileSync(fPath, orig, 'utf8');
|
|
1108
|
+
}
|
|
1109
|
+
catch { }
|
|
1110
|
+
}
|
|
1111
|
+
modifiedFiles.length = 0;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
992
1114
|
if (modifiedFiles.length > 0) {
|
|
993
|
-
let
|
|
1115
|
+
let rawDiff = '';
|
|
994
1116
|
try {
|
|
995
1117
|
const quotedFiles = modifiedFiles.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(' ');
|
|
996
|
-
|
|
1118
|
+
rawDiff = execSync(`git diff -- ${quotedFiles}`, {
|
|
997
1119
|
cwd: projectPath,
|
|
998
1120
|
encoding: 'utf8',
|
|
999
1121
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1000
1122
|
}).trim();
|
|
1001
|
-
if (rawDiff) {
|
|
1002
|
-
const clipped = rawDiff.length > 2500 ? rawDiff.slice(0, 2500) + '\n... (diff truncated)' : rawDiff;
|
|
1003
|
-
diffSnippet = `\n\n### Code Changes (\`git diff\`)\n\`\`\`diff\n${clipped}\n\`\`\``;
|
|
1004
|
-
}
|
|
1005
1123
|
}
|
|
1006
1124
|
catch { }
|
|
1007
1125
|
const baseSummary = editsRes.summary ||
|
|
1008
|
-
`
|
|
1009
|
-
|
|
1126
|
+
`Updated ${modifiedFiles.join(', ')} to complete "${card.title}".`;
|
|
1127
|
+
// Keep card comment concise plain English (no raw code diffs on the card!)
|
|
1128
|
+
const cleanPlainSummary = baseSummary
|
|
1129
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
1130
|
+
.trim();
|
|
1010
1131
|
const verificationSteps = editsRes.verification_steps ||
|
|
1011
|
-
`1.
|
|
1012
|
-
|
|
1132
|
+
`1. Check the updated behavior for "${card.title}".\n2. Click **Approve & Move to Done** to deploy and confirm.`;
|
|
1133
|
+
const logLines = [
|
|
1134
|
+
`Claimed card "${card.title}" (${card.id}) in ${projectPath}`,
|
|
1135
|
+
`Agent: ${workerLabel} | Model: ${card.agent_model || 'auto'}`,
|
|
1136
|
+
`Selected files: ${Object.keys(filesPayload).join(', ')}`,
|
|
1137
|
+
`Modified & verified files: ${modifiedFiles.join(', ')}`,
|
|
1138
|
+
`Summary: ${cleanPlainSummary}`,
|
|
1139
|
+
];
|
|
1140
|
+
appendIdeActivityLog(projectPath, {
|
|
1141
|
+
cardId: card.id,
|
|
1142
|
+
cardTitle: card.title,
|
|
1143
|
+
agentName: workerLabel,
|
|
1144
|
+
model: card.agent_model || 'auto',
|
|
1145
|
+
status: 'review',
|
|
1146
|
+
summary: cleanPlainSummary,
|
|
1147
|
+
files: modifiedFiles,
|
|
1148
|
+
diff: rawDiff,
|
|
1149
|
+
});
|
|
1150
|
+
await client.submitReview(card.id, provider, cleanPlainSummary, verificationSteps, undefined, undefined, {
|
|
1151
|
+
diffText: rawDiff,
|
|
1152
|
+
logText: logLines.join('\n'),
|
|
1153
|
+
filesModified: modifiedFiles.join(', '),
|
|
1154
|
+
});
|
|
1155
|
+
try {
|
|
1156
|
+
fs.writeFileSync(lockFile, JSON.stringify({
|
|
1157
|
+
card_id: card.id,
|
|
1158
|
+
card_updated_at: cardUpdatedAt,
|
|
1159
|
+
status: 'completed',
|
|
1160
|
+
completed_at: new Date().toISOString(),
|
|
1161
|
+
}), 'utf8');
|
|
1162
|
+
}
|
|
1163
|
+
catch { }
|
|
1013
1164
|
sendDesktopNotification(`Agile Monster -> Ready for Review`, `Completed "${card.title}" (${modifiedFiles.length} file(s) updated) and moved to Testing / Review!`);
|
|
1014
1165
|
return true;
|
|
1015
1166
|
}
|
|
1016
1167
|
else {
|
|
1017
|
-
|
|
1168
|
+
try {
|
|
1169
|
+
if (fs.existsSync(lockFile))
|
|
1170
|
+
fs.unlinkSync(lockFile);
|
|
1171
|
+
}
|
|
1172
|
+
catch { }
|
|
1173
|
+
await client.askQuestion(card.id, provider, `Inspected ${Object.keys(filesPayload)
|
|
1018
1174
|
.map((f) => `\`${f}\``)
|
|
1019
|
-
.join(', ')}.
|
|
1175
|
+
.join(', ')}, but needed more specific details to apply a verified code change automatically. Reply in the comments with additional details (or open your IDE agent) to continue!`);
|
|
1020
1176
|
return false;
|
|
1021
1177
|
}
|
|
1022
1178
|
}
|
|
1023
1179
|
catch (err) {
|
|
1180
|
+
try {
|
|
1181
|
+
if (fs.existsSync(lockFile))
|
|
1182
|
+
fs.unlinkSync(lockFile);
|
|
1183
|
+
}
|
|
1184
|
+
catch { }
|
|
1024
1185
|
try {
|
|
1025
1186
|
await client.addProgress(card.id, provider, `Autonomous runner note: ${err?.message || String(err)}`);
|
|
1026
1187
|
}
|
|
@@ -1036,13 +1197,27 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1036
1197
|
return false;
|
|
1037
1198
|
const shortId = card.id.slice(0, 8);
|
|
1038
1199
|
const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
|
|
1200
|
+
const cardUpdatedAt = card.updated_at || '';
|
|
1039
1201
|
if (fs.existsSync(deployLock)) {
|
|
1040
|
-
|
|
1202
|
+
try {
|
|
1203
|
+
const existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
|
|
1204
|
+
if (!cardUpdatedAt || existing?.card_updated_at === cardUpdatedAt) {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1041
1211
|
}
|
|
1042
1212
|
const provider = card.agent_provider || 'cursor';
|
|
1043
1213
|
const cleanTitle = (card.title || 'update').replace(/["`\r\n]+/g, ' ').trim();
|
|
1044
1214
|
try {
|
|
1045
|
-
fs.writeFileSync(deployLock, JSON.stringify({
|
|
1215
|
+
fs.writeFileSync(deployLock, JSON.stringify({
|
|
1216
|
+
card_id: card.id,
|
|
1217
|
+
card_updated_at: cardUpdatedAt,
|
|
1218
|
+
title: card.title,
|
|
1219
|
+
started_at: new Date().toISOString(),
|
|
1220
|
+
}), 'utf8');
|
|
1046
1221
|
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
1047
1222
|
cwd: projectPath,
|
|
1048
1223
|
encoding: 'utf8',
|
|
@@ -1072,8 +1247,17 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1072
1247
|
encoding: 'utf8',
|
|
1073
1248
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1074
1249
|
}).trim();
|
|
1075
|
-
|
|
1076
|
-
|
|
1250
|
+
const msg = `🚀 **[Auto-Deployed to Production]** Deployment confirmed! Committed and pushed \`${commitHash}\` to \`origin/${branch}\`, and moved card to **Done**.`;
|
|
1251
|
+
await client.completeTask(card.id, provider, msg);
|
|
1252
|
+
appendIdeActivityLog(projectPath, {
|
|
1253
|
+
cardId: card.id,
|
|
1254
|
+
cardTitle: card.title,
|
|
1255
|
+
agentName: card.agent_display_name || provider,
|
|
1256
|
+
model: card.agent_model || 'auto',
|
|
1257
|
+
status: 'done',
|
|
1258
|
+
summary: `Deployment confirmed (${commitHash} -> origin/${branch}). Moved card to Done.`,
|
|
1259
|
+
});
|
|
1260
|
+
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${commitHash}) and moved to Done!`);
|
|
1077
1261
|
return true;
|
|
1078
1262
|
}
|
|
1079
1263
|
else if (isContainerRestartCard) {
|
|
@@ -1090,7 +1274,16 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1090
1274
|
encoding: 'utf8',
|
|
1091
1275
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1092
1276
|
}).trim();
|
|
1093
|
-
|
|
1277
|
+
const msg = `🚀 **[Auto-Deployed to Production]** Triggered production container rebuild & restart (\`${commitHash}\` on \`origin/${branch}\`) and confirmed move to **Done**.`;
|
|
1278
|
+
await client.completeTask(card.id, provider, msg);
|
|
1279
|
+
appendIdeActivityLog(projectPath, {
|
|
1280
|
+
cardId: card.id,
|
|
1281
|
+
cardTitle: card.title,
|
|
1282
|
+
agentName: card.agent_display_name || provider,
|
|
1283
|
+
model: card.agent_model || 'auto',
|
|
1284
|
+
status: 'done',
|
|
1285
|
+
summary: `Triggered container restart (${commitHash}) and moved card to Done.`,
|
|
1286
|
+
});
|
|
1094
1287
|
sendDesktopNotification(`Agile Monster -> Container Redeploy`, `Triggered container restart (${commitHash}) on origin/${branch}!`);
|
|
1095
1288
|
return true;
|
|
1096
1289
|
}
|
|
@@ -1100,7 +1293,8 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1100
1293
|
encoding: 'utf8',
|
|
1101
1294
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1102
1295
|
}).trim();
|
|
1103
|
-
|
|
1296
|
+
const msg = `🚀 **[Auto-Deployed to Production]** Confirmed latest commit \`${headHash}\` is pushed to \`origin/${branch}\` and live in production. Moved card to **Done**.`;
|
|
1297
|
+
await client.completeTask(card.id, provider, msg);
|
|
1104
1298
|
return true;
|
|
1105
1299
|
}
|
|
1106
1300
|
}
|
|
@@ -1179,7 +1373,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
|
|
|
1179
1373
|
if (!config?.board_id || !apiKey) {
|
|
1180
1374
|
return;
|
|
1181
1375
|
}
|
|
1182
|
-
const includeDiscovery = pulseCount === 0 || pulseCount %
|
|
1376
|
+
const includeDiscovery = pulseCount === 0 || pulseCount % 3 === 0;
|
|
1183
1377
|
pulseCount++;
|
|
1184
1378
|
const res = await client.pulse({
|
|
1185
1379
|
boardId: config.board_id,
|
|
@@ -1187,6 +1381,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
|
|
|
1187
1381
|
projectName: config.project_name || path.basename(process.cwd()),
|
|
1188
1382
|
projectPath: config.project_path || process.cwd(),
|
|
1189
1383
|
discoveredProjects: includeDiscovery ? client.discoverLocalProjects() : undefined,
|
|
1384
|
+
discoveredModels: includeDiscovery ? client.discoverLocalAgentModels() : undefined,
|
|
1190
1385
|
});
|
|
1191
1386
|
if (res.saved_bindings && res.saved_bindings.length > 0) {
|
|
1192
1387
|
autoProvisionSavedBindings(apiKey, res.saved_bindings);
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilemonster/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Official Model Context Protocol (MCP) server & Orchestrator for Agile Monster (Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf)",
|
|
6
6
|
"main": "dist/server.js",
|