@agilemonster/mcp-server 1.0.12 → 1.0.15
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 +370 -95
- 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.15";
|
|
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.15';
|
|
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
|
@@ -892,20 +892,79 @@ function getAgileMonsterLocksDir() {
|
|
|
892
892
|
catch { }
|
|
893
893
|
return dir;
|
|
894
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
|
+
}
|
|
895
936
|
export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
896
937
|
if (!projectPath || !fs.existsSync(projectPath))
|
|
897
938
|
return false;
|
|
898
939
|
if (activeAutonomousCards.has(card.id))
|
|
899
940
|
return false;
|
|
900
941
|
const lockFile = path.join(getAgileMonsterLocksDir(), `task-${card.id.slice(0, 8)}.lock`);
|
|
942
|
+
const cardUpdatedAt = card.updated_at || '';
|
|
901
943
|
try {
|
|
902
944
|
if (fs.existsSync(lockFile)) {
|
|
903
945
|
const stat = fs.statSync(lockFile);
|
|
904
|
-
|
|
905
|
-
|
|
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;
|
|
906
960
|
}
|
|
907
961
|
}
|
|
908
|
-
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');
|
|
909
968
|
}
|
|
910
969
|
catch {
|
|
911
970
|
return false;
|
|
@@ -929,15 +988,29 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
929
988
|
await client.addProgress(card.id, provider, `Card is now **In Progress**, but no matching source files were found in \`${projectPath}\`.`);
|
|
930
989
|
return false;
|
|
931
990
|
}
|
|
932
|
-
// 3. Ask Agile Monster Autonomous Step to pick the 1-
|
|
991
|
+
// 3. Ask Agile Monster Autonomous Step to pick the 1-6 most relevant files
|
|
933
992
|
const selectionRes = await client.autonomousStep({
|
|
934
993
|
card_id: card.id,
|
|
935
994
|
action: 'select_files',
|
|
936
995
|
file_index: fileIndex,
|
|
937
996
|
});
|
|
938
|
-
const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0,
|
|
997
|
+
const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
|
|
939
998
|
if (selectedFiles.length === 0) {
|
|
940
|
-
selectedFiles.push(...fileIndex.slice(0,
|
|
999
|
+
selectedFiles.push(...fileIndex.slice(0, 4));
|
|
1000
|
+
}
|
|
1001
|
+
// Automatically include project type definition files (e.g. types.ts,frontend/src/lib/types.ts) so the model knows exact schemas
|
|
1002
|
+
for (const candidateTypeFile of [
|
|
1003
|
+
'frontend/src/lib/types.ts',
|
|
1004
|
+
'src/lib/types.ts',
|
|
1005
|
+
'src/types.ts',
|
|
1006
|
+
'src/types/index.ts',
|
|
1007
|
+
'frontend/src/types.ts',
|
|
1008
|
+
]) {
|
|
1009
|
+
if (fileIndex.includes(candidateTypeFile) &&
|
|
1010
|
+
!selectedFiles.includes(candidateTypeFile) &&
|
|
1011
|
+
selectedFiles.length < 7) {
|
|
1012
|
+
selectedFiles.push(candidateTypeFile);
|
|
1013
|
+
}
|
|
941
1014
|
}
|
|
942
1015
|
const filesPayload = {};
|
|
943
1016
|
for (const rel of selectedFiles) {
|
|
@@ -956,96 +1029,217 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
956
1029
|
.map((f) => `\`${f}\``)
|
|
957
1030
|
.join(', ')}. Generating code changes...`);
|
|
958
1031
|
// 4. Generate surgical code edits
|
|
959
|
-
|
|
1032
|
+
let editsRes = await client.autonomousStep({
|
|
960
1033
|
card_id: card.id,
|
|
961
1034
|
action: 'generate_edits',
|
|
962
1035
|
files: filesPayload,
|
|
963
1036
|
});
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
fs.writeFileSync(fullPath, updatedContent, 'utf8');
|
|
993
|
-
// Pre-submit static analysis gate for Dart/Flutter files
|
|
994
|
-
if (relPath.endsWith('.dart')) {
|
|
995
|
-
try {
|
|
996
|
-
execSync(`dart analyze "${fullPath}"`, {
|
|
997
|
-
cwd: projectPath,
|
|
998
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
999
|
-
timeout: 25000,
|
|
1000
|
-
});
|
|
1037
|
+
const applyEditsPass = (editsList) => {
|
|
1038
|
+
const modFiles = [];
|
|
1039
|
+
for (const edit of editsList) {
|
|
1040
|
+
const relPath = edit.file || edit.path;
|
|
1041
|
+
if (!relPath || !edit.search || typeof edit.replace !== 'string')
|
|
1042
|
+
continue;
|
|
1043
|
+
const fullPath = path.resolve(projectPath, relPath);
|
|
1044
|
+
if (!fullPath.startsWith(path.resolve(projectPath)))
|
|
1045
|
+
continue;
|
|
1046
|
+
if (!fs.existsSync(fullPath))
|
|
1047
|
+
continue;
|
|
1048
|
+
const currentContent = fs.readFileSync(fullPath, 'utf8');
|
|
1049
|
+
if (!originalContents.has(fullPath)) {
|
|
1050
|
+
originalContents.set(fullPath, currentContent);
|
|
1051
|
+
}
|
|
1052
|
+
if (currentContent.includes(edit.search)) {
|
|
1053
|
+
const updatedContent = currentContent.replace(edit.search, edit.replace);
|
|
1054
|
+
if (updatedContent !== currentContent) {
|
|
1055
|
+
const countChar = (s, ch) => s.split(ch).length - 1;
|
|
1056
|
+
const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
|
|
1057
|
+
const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
|
|
1058
|
+
const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
|
|
1059
|
+
const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
|
|
1060
|
+
if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
|
|
1061
|
+
continue;
|
|
1001
1062
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1063
|
+
fs.writeFileSync(fullPath, updatedContent, 'utf8');
|
|
1064
|
+
if (relPath.endsWith('.dart')) {
|
|
1065
|
+
try {
|
|
1066
|
+
execSync(`dart analyze "${fullPath}"`, {
|
|
1067
|
+
cwd: projectPath,
|
|
1068
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1069
|
+
timeout: 25000,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
catch (analyzeErr) {
|
|
1073
|
+
const out = String(analyzeErr?.stdout || '') + String(analyzeErr?.stderr || '');
|
|
1074
|
+
if (/\berror\s+[•-]/i.test(out)) {
|
|
1075
|
+
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else if (relPath.endsWith('.php')) {
|
|
1081
|
+
try {
|
|
1082
|
+
execSync(`php -l "${fullPath}"`, {
|
|
1083
|
+
cwd: projectPath,
|
|
1084
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1085
|
+
timeout: 10000,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1006
1089
|
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1007
1090
|
continue;
|
|
1008
1091
|
}
|
|
1009
1092
|
}
|
|
1093
|
+
if (!modFiles.includes(relPath)) {
|
|
1094
|
+
modFiles.push(relPath);
|
|
1095
|
+
}
|
|
1010
1096
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return modFiles;
|
|
1100
|
+
};
|
|
1101
|
+
const originalContents = new Map();
|
|
1102
|
+
const modifiedFiles = applyEditsPass(editsRes.edits || []);
|
|
1103
|
+
// Pre-submit frontend build verification gate for root AND monorepo subdirectories (frontend/, web/, client/)
|
|
1104
|
+
const verifyFrontendBuilds = () => {
|
|
1105
|
+
const hasFrontendEdits = modifiedFiles.some((f) => /\.(tsx?|jsx?|vue|css)$/i.test(f));
|
|
1106
|
+
if (!hasFrontendEdits)
|
|
1107
|
+
return { ok: true };
|
|
1108
|
+
const candidateDirs = [
|
|
1109
|
+
projectPath,
|
|
1110
|
+
path.join(projectPath, 'frontend'),
|
|
1111
|
+
path.join(projectPath, 'web'),
|
|
1112
|
+
path.join(projectPath, 'client'),
|
|
1113
|
+
];
|
|
1114
|
+
for (const dir of candidateDirs) {
|
|
1115
|
+
const pkgJsonPath = path.join(dir, 'package.json');
|
|
1116
|
+
if (!fs.existsSync(pkgJsonPath) || !fs.existsSync(path.join(dir, 'node_modules')))
|
|
1117
|
+
continue;
|
|
1118
|
+
try {
|
|
1119
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
1120
|
+
if (pkg?.scripts?.build) {
|
|
1121
|
+
const buildCmd = fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))
|
|
1122
|
+
? 'pnpm run build'
|
|
1123
|
+
: 'npm run build';
|
|
1124
|
+
execSync(buildCmd, {
|
|
1125
|
+
cwd: dir,
|
|
1126
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1127
|
+
timeout: 60000,
|
|
1128
|
+
});
|
|
1013
1129
|
}
|
|
1014
1130
|
}
|
|
1131
|
+
catch (buildErr) {
|
|
1132
|
+
const errOut = (String(buildErr?.stdout || '') + '\n' + String(buildErr?.stderr || '')).trim();
|
|
1133
|
+
return { ok: false, errorOutput: errOut.slice(-1500) };
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return { ok: true };
|
|
1137
|
+
};
|
|
1138
|
+
let buildCheck = verifyFrontendBuilds();
|
|
1139
|
+
if (!buildCheck.ok) {
|
|
1140
|
+
// Revert first pass, attach compiler error context, and do 1 self-correction pass
|
|
1141
|
+
for (const [fPath, orig] of originalContents.entries()) {
|
|
1142
|
+
try {
|
|
1143
|
+
fs.writeFileSync(fPath, orig, 'utf8');
|
|
1144
|
+
}
|
|
1145
|
+
catch { }
|
|
1146
|
+
}
|
|
1147
|
+
modifiedFiles.length = 0;
|
|
1148
|
+
const retryFiles = {
|
|
1149
|
+
...filesPayload,
|
|
1150
|
+
'__COMPILER_ERROR_TO_FIX__.txt': `The previous edit failed to compile with:\n${buildCheck.errorOutput}\nPlease fix the TypeScript/build error using the exact property names from types.ts.`,
|
|
1151
|
+
};
|
|
1152
|
+
editsRes = await client.autonomousStep({
|
|
1153
|
+
card_id: card.id,
|
|
1154
|
+
action: 'generate_edits',
|
|
1155
|
+
files: retryFiles,
|
|
1156
|
+
});
|
|
1157
|
+
const retryMods = applyEditsPass(editsRes.edits || []);
|
|
1158
|
+
modifiedFiles.push(...retryMods);
|
|
1159
|
+
buildCheck = verifyFrontendBuilds();
|
|
1160
|
+
if (!buildCheck.ok) {
|
|
1161
|
+
for (const [fPath, orig] of originalContents.entries()) {
|
|
1162
|
+
try {
|
|
1163
|
+
fs.writeFileSync(fPath, orig, 'utf8');
|
|
1164
|
+
}
|
|
1165
|
+
catch { }
|
|
1166
|
+
}
|
|
1167
|
+
modifiedFiles.length = 0;
|
|
1015
1168
|
}
|
|
1016
1169
|
}
|
|
1017
1170
|
if (modifiedFiles.length > 0) {
|
|
1018
|
-
let
|
|
1171
|
+
let rawDiff = '';
|
|
1019
1172
|
try {
|
|
1020
1173
|
const quotedFiles = modifiedFiles.map((f) => `"${f.replace(/"/g, '\\"')}"`).join(' ');
|
|
1021
|
-
|
|
1174
|
+
rawDiff = execSync(`git diff -- ${quotedFiles}`, {
|
|
1022
1175
|
cwd: projectPath,
|
|
1023
1176
|
encoding: 'utf8',
|
|
1024
1177
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1025
1178
|
}).trim();
|
|
1026
|
-
if (rawDiff) {
|
|
1027
|
-
const clipped = rawDiff.length > 2500 ? rawDiff.slice(0, 2500) + '\n... (diff truncated)' : rawDiff;
|
|
1028
|
-
diffSnippet = `\n\n### Code Changes (\`git diff\`)\n\`\`\`diff\n${clipped}\n\`\`\``;
|
|
1029
|
-
}
|
|
1030
1179
|
}
|
|
1031
1180
|
catch { }
|
|
1181
|
+
// Automatically commit, push, and run the project's defined deployment pipeline (e.g. deploy/deploy.sh, GitHub Actions, or Docker)
|
|
1182
|
+
// so changes are live for testing as soon as the card moves to Testing / QA!
|
|
1183
|
+
const deployInfo = runProjectDeploymentPipeline(projectPath, card.title || 'update', card.id.slice(0, 8));
|
|
1032
1184
|
const baseSummary = editsRes.summary ||
|
|
1033
|
-
`
|
|
1034
|
-
const
|
|
1185
|
+
`Updated ${modifiedFiles.join(', ')} to complete "${card.title}".`;
|
|
1186
|
+
const cleanPlainSummary = baseSummary.replace(/```[\s\S]*?```/g, '').trim() +
|
|
1187
|
+
(deployInfo.deployedNote ? `\n\n${deployInfo.deployedNote}` : '');
|
|
1035
1188
|
const verificationSteps = editsRes.verification_steps ||
|
|
1036
|
-
`1.
|
|
1037
|
-
|
|
1189
|
+
`1. Check the live update for "${card.title}".\n2. Click **Approve & Deploy to Done** when verified.`;
|
|
1190
|
+
const logLines = [
|
|
1191
|
+
`Claimed card "${card.title}" (${card.id}) in ${projectPath}`,
|
|
1192
|
+
`Agent: ${workerLabel} | Model: ${card.agent_model || 'auto'}`,
|
|
1193
|
+
`Selected files: ${Object.keys(filesPayload).join(', ')}`,
|
|
1194
|
+
`Modified & verified files: ${modifiedFiles.join(', ')}`,
|
|
1195
|
+
...(deployInfo.deployedNote ? [`Deployment: ${deployInfo.deployedNote}`] : []),
|
|
1196
|
+
`Summary: ${cleanPlainSummary}`,
|
|
1197
|
+
];
|
|
1198
|
+
appendIdeActivityLog(projectPath, {
|
|
1199
|
+
cardId: card.id,
|
|
1200
|
+
cardTitle: card.title,
|
|
1201
|
+
agentName: workerLabel,
|
|
1202
|
+
model: card.agent_model || 'auto',
|
|
1203
|
+
status: 'review',
|
|
1204
|
+
summary: cleanPlainSummary,
|
|
1205
|
+
files: modifiedFiles,
|
|
1206
|
+
diff: rawDiff,
|
|
1207
|
+
});
|
|
1208
|
+
await client.submitReview(card.id, provider, cleanPlainSummary, verificationSteps, undefined, undefined, {
|
|
1209
|
+
diffText: rawDiff,
|
|
1210
|
+
logText: logLines.join('\n'),
|
|
1211
|
+
filesModified: modifiedFiles.join(', '),
|
|
1212
|
+
});
|
|
1213
|
+
try {
|
|
1214
|
+
fs.writeFileSync(lockFile, JSON.stringify({
|
|
1215
|
+
card_id: card.id,
|
|
1216
|
+
card_updated_at: cardUpdatedAt,
|
|
1217
|
+
status: 'completed',
|
|
1218
|
+
completed_at: new Date().toISOString(),
|
|
1219
|
+
}), 'utf8');
|
|
1220
|
+
}
|
|
1221
|
+
catch { }
|
|
1038
1222
|
sendDesktopNotification(`Agile Monster -> Ready for Review`, `Completed "${card.title}" (${modifiedFiles.length} file(s) updated) and moved to Testing / Review!`);
|
|
1039
1223
|
return true;
|
|
1040
1224
|
}
|
|
1041
1225
|
else {
|
|
1042
|
-
|
|
1226
|
+
try {
|
|
1227
|
+
if (fs.existsSync(lockFile))
|
|
1228
|
+
fs.unlinkSync(lockFile);
|
|
1229
|
+
}
|
|
1230
|
+
catch { }
|
|
1231
|
+
await client.askQuestion(card.id, provider, `Inspected ${Object.keys(filesPayload)
|
|
1043
1232
|
.map((f) => `\`${f}\``)
|
|
1044
|
-
.join(', ')}.
|
|
1233
|
+
.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!`);
|
|
1045
1234
|
return false;
|
|
1046
1235
|
}
|
|
1047
1236
|
}
|
|
1048
1237
|
catch (err) {
|
|
1238
|
+
try {
|
|
1239
|
+
if (fs.existsSync(lockFile))
|
|
1240
|
+
fs.unlinkSync(lockFile);
|
|
1241
|
+
}
|
|
1242
|
+
catch { }
|
|
1049
1243
|
try {
|
|
1050
1244
|
await client.addProgress(card.id, provider, `Autonomous runner note: ${err?.message || String(err)}`);
|
|
1051
1245
|
}
|
|
@@ -1056,32 +1250,28 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1056
1250
|
activeAutonomousCards.delete(card.id);
|
|
1057
1251
|
}
|
|
1058
1252
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1253
|
+
/**
|
|
1254
|
+
* Detect and execute the project's defined deployment pipeline:
|
|
1255
|
+
* 1. Commits any modified tracked files and pushes to `origin/<branch>`.
|
|
1256
|
+
* 2. Runs `deploy/deploy.sh` (or `scripts/deploy.sh`) if present in the repo (e.g. Meerlink cPanel SSH deploy).
|
|
1257
|
+
* 3. Or triggers GitHub Actions CI/CD via `git push`.
|
|
1258
|
+
*/
|
|
1259
|
+
function runProjectDeploymentPipeline(projectPath, rawTitle, shortId, forceEmptyCommit = false) {
|
|
1260
|
+
const cleanTitle = (rawTitle || 'update').replace(/["`\r\n]+/g, ' ').trim();
|
|
1261
|
+
let branch = 'main';
|
|
1262
|
+
let commitHash = '';
|
|
1263
|
+
const actions = [];
|
|
1069
1264
|
try {
|
|
1070
|
-
|
|
1071
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
1265
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
1072
1266
|
cwd: projectPath,
|
|
1073
1267
|
encoding: 'utf8',
|
|
1074
1268
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1075
|
-
}).trim();
|
|
1076
|
-
if (!branch || branch === 'HEAD')
|
|
1077
|
-
return false;
|
|
1269
|
+
}).trim() || 'main';
|
|
1078
1270
|
const statusOut = execSync('git status --porcelain -uno', {
|
|
1079
1271
|
cwd: projectPath,
|
|
1080
1272
|
encoding: 'utf8',
|
|
1081
1273
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1082
1274
|
}).trim();
|
|
1083
|
-
const rawText = `${card.title || ''} ${card.description || ''}`;
|
|
1084
|
-
const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
|
|
1085
1275
|
if (statusOut.length > 0) {
|
|
1086
1276
|
execSync('git add -u', { cwd: projectPath, stdio: 'ignore' });
|
|
1087
1277
|
execSync(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`, {
|
|
@@ -1091,17 +1281,16 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1091
1281
|
execSync(`git push origin ${branch}`, {
|
|
1092
1282
|
cwd: projectPath,
|
|
1093
1283
|
stdio: 'ignore',
|
|
1284
|
+
timeout: 45000,
|
|
1094
1285
|
});
|
|
1095
|
-
|
|
1286
|
+
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1096
1287
|
cwd: projectPath,
|
|
1097
1288
|
encoding: 'utf8',
|
|
1098
1289
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1099
1290
|
}).trim();
|
|
1100
|
-
|
|
1101
|
-
sendDesktopNotification(`Agile Monster -> Production Deploy`, `Pushed "${card.title}" (${commitHash}) to origin/${branch}!`);
|
|
1102
|
-
return true;
|
|
1291
|
+
actions.push(`pushed \`${commitHash}\` to \`origin/${branch}\``);
|
|
1103
1292
|
}
|
|
1104
|
-
else if (
|
|
1293
|
+
else if (forceEmptyCommit) {
|
|
1105
1294
|
execSync(`git commit --allow-empty -m "chore: trigger container redeploy [AM-${shortId}]"`, {
|
|
1106
1295
|
cwd: projectPath,
|
|
1107
1296
|
stdio: 'ignore',
|
|
@@ -1109,29 +1298,97 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1109
1298
|
execSync(`git push origin ${branch}`, {
|
|
1110
1299
|
cwd: projectPath,
|
|
1111
1300
|
stdio: 'ignore',
|
|
1301
|
+
timeout: 45000,
|
|
1112
1302
|
});
|
|
1113
|
-
|
|
1303
|
+
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1114
1304
|
cwd: projectPath,
|
|
1115
1305
|
encoding: 'utf8',
|
|
1116
1306
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1117
1307
|
}).trim();
|
|
1118
|
-
|
|
1119
|
-
sendDesktopNotification(`Agile Monster -> Container Redeploy`, `Triggered container restart (${commitHash}) on origin/${branch}!`);
|
|
1120
|
-
return true;
|
|
1308
|
+
actions.push(`triggered redeploy commit \`${commitHash}\` on \`origin/${branch}\``);
|
|
1121
1309
|
}
|
|
1122
1310
|
else {
|
|
1123
|
-
|
|
1311
|
+
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1124
1312
|
cwd: projectPath,
|
|
1125
1313
|
encoding: 'utf8',
|
|
1126
1314
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1127
1315
|
}).trim();
|
|
1128
|
-
await client.addProgress(card.id, provider, `🚀 **[Auto-Deployed to Production]** Verified latest commit \`${headHash}\` is pushed to \`origin/${branch}\` and live in the production CI/CD pipeline.`);
|
|
1129
|
-
return true;
|
|
1130
1316
|
}
|
|
1317
|
+
// Check if the project has a dedicated deploy script (e.g. deploy/deploy.sh in Meerlink)
|
|
1318
|
+
const deployScript = path.join(projectPath, 'deploy', 'deploy.sh');
|
|
1319
|
+
if (fs.existsSync(deployScript)) {
|
|
1320
|
+
const scriptContent = fs.readFileSync(deployScript, 'utf8');
|
|
1321
|
+
const flag = scriptContent.includes('--skip-preflight') ? ' --skip-preflight' : '';
|
|
1322
|
+
execSync(`bash "${deployScript}"${flag}`, {
|
|
1323
|
+
cwd: projectPath,
|
|
1324
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1325
|
+
timeout: 180000,
|
|
1326
|
+
});
|
|
1327
|
+
actions.push('deployed live via `deploy/deploy.sh`');
|
|
1328
|
+
}
|
|
1329
|
+
else if (fs.existsSync(path.join(projectPath, '.github', 'workflows'))) {
|
|
1330
|
+
actions.push('triggered GitHub Actions CI/CD deployment');
|
|
1331
|
+
}
|
|
1332
|
+
const deployedNote = actions.length > 0
|
|
1333
|
+
? `🚀 **Deployed**: Automatically ${actions.join(' and ')}.`
|
|
1334
|
+
: `🚀 **Deployed**: Verified commit \`${commitHash}\` is live on \`origin/${branch}\`.`;
|
|
1335
|
+
return { ok: true, commitHash, branch, deployedNote };
|
|
1336
|
+
}
|
|
1337
|
+
catch (err) {
|
|
1338
|
+
return {
|
|
1339
|
+
ok: false,
|
|
1340
|
+
commitHash,
|
|
1341
|
+
branch,
|
|
1342
|
+
deployedNote: '',
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
1347
|
+
if (!projectPath || !fs.existsSync(projectPath))
|
|
1348
|
+
return false;
|
|
1349
|
+
const shortId = card.id.slice(0, 8);
|
|
1350
|
+
const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
|
|
1351
|
+
const cardUpdatedAt = card.updated_at || '';
|
|
1352
|
+
if (fs.existsSync(deployLock)) {
|
|
1353
|
+
try {
|
|
1354
|
+
const existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
|
|
1355
|
+
if (!cardUpdatedAt || existing?.card_updated_at === cardUpdatedAt) {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const provider = card.agent_provider || 'cursor';
|
|
1364
|
+
const rawText = `${card.title || ''} ${card.description || ''}`;
|
|
1365
|
+
const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
|
|
1366
|
+
try {
|
|
1367
|
+
fs.writeFileSync(deployLock, JSON.stringify({
|
|
1368
|
+
card_id: card.id,
|
|
1369
|
+
card_updated_at: cardUpdatedAt,
|
|
1370
|
+
title: card.title,
|
|
1371
|
+
started_at: new Date().toISOString(),
|
|
1372
|
+
}), 'utf8');
|
|
1373
|
+
const res = runProjectDeploymentPipeline(projectPath, card.title || 'update', shortId, isContainerRestartCard);
|
|
1374
|
+
const msg = res.deployedNote
|
|
1375
|
+
? `${res.deployedNote} Confirmed and moved card to **Done**.`
|
|
1376
|
+
: `🚀 **Deployed**: Confirmed latest commit \`${res.commitHash}\` is live on \`origin/${res.branch}\` and moved card to **Done**.`;
|
|
1377
|
+
await client.completeTask(card.id, provider, msg);
|
|
1378
|
+
appendIdeActivityLog(projectPath, {
|
|
1379
|
+
cardId: card.id,
|
|
1380
|
+
cardTitle: card.title,
|
|
1381
|
+
agentName: card.agent_display_name || provider,
|
|
1382
|
+
model: card.agent_model || 'auto',
|
|
1383
|
+
status: 'done',
|
|
1384
|
+
summary: msg,
|
|
1385
|
+
});
|
|
1386
|
+
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${res.commitHash}) and moved to Done!`);
|
|
1387
|
+
return true;
|
|
1131
1388
|
}
|
|
1132
1389
|
catch (err) {
|
|
1133
1390
|
try {
|
|
1134
|
-
await client.addProgress(card.id, provider, `⚠️ **[Auto-Deploy Note]** Attempted to
|
|
1391
|
+
await client.addProgress(card.id, provider, `⚠️ **[Auto-Deploy Note]** Attempted to deploy approved changes in \`${projectPath}\`: ${err?.message || String(err)}`);
|
|
1135
1392
|
}
|
|
1136
1393
|
catch { }
|
|
1137
1394
|
return false;
|
|
@@ -1204,7 +1461,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
|
|
|
1204
1461
|
if (!config?.board_id || !apiKey) {
|
|
1205
1462
|
return;
|
|
1206
1463
|
}
|
|
1207
|
-
const includeDiscovery = pulseCount === 0 || pulseCount %
|
|
1464
|
+
const includeDiscovery = pulseCount === 0 || pulseCount % 3 === 0;
|
|
1208
1465
|
pulseCount++;
|
|
1209
1466
|
const res = await client.pulse({
|
|
1210
1467
|
boardId: config.board_id,
|
|
@@ -1212,6 +1469,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
|
|
|
1212
1469
|
projectName: config.project_name || path.basename(process.cwd()),
|
|
1213
1470
|
projectPath: config.project_path || process.cwd(),
|
|
1214
1471
|
discoveredProjects: includeDiscovery ? client.discoverLocalProjects() : undefined,
|
|
1472
|
+
discoveredModels: includeDiscovery ? client.discoverLocalAgentModels() : undefined,
|
|
1215
1473
|
});
|
|
1216
1474
|
if (res.saved_bindings && res.saved_bindings.length > 0) {
|
|
1217
1475
|
autoProvisionSavedBindings(apiKey, res.saved_bindings);
|
|
@@ -1276,6 +1534,23 @@ export async function runBackgroundPulseDaemon(intervalMs = 20000) {
|
|
|
1276
1534
|
const client = new AgileMonsterClient();
|
|
1277
1535
|
console.log(`[Agile Monster Pulse Daemon v${MCP_SERVER_VERSION}] Listening for queued agent tasks across all bound repositories (interval: ${intervalMs / 1000}s)...`);
|
|
1278
1536
|
startBuiltInPullPulse(null, client, intervalMs);
|
|
1279
|
-
|
|
1280
|
-
|
|
1537
|
+
const selfScript = process.argv[1] || '';
|
|
1538
|
+
const initialMtime = selfScript && fs.existsSync(selfScript) ? fs.statSync(selfScript).mtimeMs : 0;
|
|
1539
|
+
// Auto-restart daemon if mcp-server binary/dist on disk is updated so it never runs stale code in memory
|
|
1540
|
+
setInterval(() => {
|
|
1541
|
+
try {
|
|
1542
|
+
if (selfScript && initialMtime > 0 && fs.existsSync(selfScript)) {
|
|
1543
|
+
const curMtime = fs.statSync(selfScript).mtimeMs;
|
|
1544
|
+
if (curMtime > initialMtime + 1000) {
|
|
1545
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
1546
|
+
detached: true,
|
|
1547
|
+
stdio: 'ignore',
|
|
1548
|
+
});
|
|
1549
|
+
child.unref();
|
|
1550
|
+
process.exit(0);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
catch { }
|
|
1555
|
+
}, 30000);
|
|
1281
1556
|
}
|
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.15",
|
|
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",
|