@agilemonster/mcp-server 1.0.16 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,7 +74,7 @@ This generates:
74
74
 
75
75
  ## 🛠️ Available MCP Tools
76
76
 
77
- Agents have access to 8 purpose-built lifecycle tools:
77
+ Agents have access to these purpose-built lifecycle tools:
78
78
 
79
79
  | Tool | Purpose |
80
80
  | :--- | :--- |
@@ -86,6 +86,32 @@ Agents have access to 8 purpose-built lifecycle tools:
86
86
  | `agilemonster_add_progress` | Logs implementation progress notes to task comments. |
87
87
  | `agilemonster_submit_for_review` | Marks work complete, moves card to Review column, reassigns to human member for confirmation, and posts test instructions. |
88
88
  | `agilemonster_create_followup_tasks` | Creates follow-up cards (e.g. "Deploy to Test", "Deploy to Production", "Write integration tests"). |
89
+ | `agilemonster_get_deploy_profile` | How this project deploys: route, exact command, confidence, the evidence behind it, alternatives, and recent outcomes. |
90
+ | `agilemonster_record_deployment` | Records how a deployment went (or pins the route the user stated) so every later task deploys the same proven way. |
91
+
92
+ ---
93
+
94
+ ## 🚀 How deployment is decided (and remembered)
95
+
96
+ Cards started from the Agile Monster web app run in the background without your IDE's context, so the
97
+ server works out how a project ships from evidence and from what worked before:
98
+
99
+ 1. **Explicit** — `.agilemonster/deploy.json` in the repo (`{"method": "docker_compose", "command": "...", "live_url": "..."}`), or a route pinned with `agilemonster_record_deployment({ pin: true })` / a card reply.
100
+ 2. **Learned** — the last deployment that was verified on this machine.
101
+ 3. **Evidence, ranked** — a deploy workflow that really fires on a push to the checked-out branch (path filters and `workflow_run` chains included) › a deploy script › a cloud CLI config › the Docker Compose stack that is running from this folder (reused with its exact project name and files) › a compose file › a plain `git push`.
102
+
103
+ What the runner guarantees:
104
+
105
+ - **Commits only the files it changed** — never `git add -A`; local files holding your API key are filtered and added to `.git/info/exclude`.
106
+ - **Verifies before claiming "Deployed"** — deploy workflow run for that commit, container state and health, script exit code, and a smoke check of a URL you provided. A guessed URL can never fail a deploy.
107
+ - **Asks once, then remembers** — an unconfirmed deploy script, cloud CLI or manually-dispatched workflow is not run on a guess. Reply on the card with `deploy: <command or method>` to confirm; `retry` re-runs only the deployment, not the code change.
108
+ - **Heals what it can** — starts Docker Desktop/OrbStack if it is installed but stopped, rebases and re-pushes when the remote moved ahead, retries network errors, and falls back to the next route when the chosen one plainly does not apply.
109
+ - **Never reports a slow pipeline as failed** — a CI run that outlives the wait is marked pending and re-checked on later pulses; the outcome is posted on the card.
110
+
111
+ State lives outside your repositories: `~/.agilemonster/projects/` (per-project deployment history) and
112
+ `~/.agilemonster/logs/daemon.jsonl` (structured daemon log).
113
+
114
+ Card reply syntax: `retry` · `deploy: github_actions` · `deploy: docker_compose` · `deploy: manual` · `deploy: ./deploy/deploy.sh` · `live url: https://example.com`
89
115
 
90
116
  ---
91
117
 
package/bin/mcp.js CHANGED
File without changes
File without changes
package/bin/worker.js CHANGED
File without changes
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { AgileMonsterProjectConfig, AutonomousStepResponse, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
2
- export declare const MCP_SERVER_VERSION = "1.0.16";
1
+ import { AgileMonsterProjectConfig, AutonomousStepResponse, CardComment, DeployReport, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
2
+ export declare const MCP_SERVER_VERSION = "1.0.21";
3
3
  export declare class AgileMonsterClient {
4
4
  private apiKey;
5
5
  private baseUrl;
@@ -40,7 +40,13 @@ export declare class AgileMonsterClient {
40
40
  default_model?: string | null;
41
41
  models: Record<string, string> | string[];
42
42
  }>;
43
+ installedAgents?: string[];
43
44
  }): Promise<PulseResponse>;
45
+ /**
46
+ * Cross-platform detection of AI coding agents installed on this computer.
47
+ * Checks for Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf, Aider.
48
+ */
49
+ static detectInstalledAgents(): string[];
44
50
  /**
45
51
  * Discover live models installed/configured across the developer's local AI coding agents
46
52
  * (Codex, Cursor, Claude Code, Hermes, Antigravity) so the Agile Monster app & website
@@ -51,13 +57,18 @@ export declare class AgileMonsterClient {
51
57
  models: Record<string, string>;
52
58
  }>;
53
59
  /**
54
- * Auto-detect build command, deployment method, deploy script, and live URL for a project repository.
60
+ * Describe how a project folder builds and deploys, for the server's project/board bindings.
61
+ * Backed by the evidence-ranked resolver (running containers, real workflow triggers, deploy scripts,
62
+ * project docs) plus this machine's deployment memory, so what the web app shows matches what will run.
55
63
  */
56
- static inspectProjectProfile(fullPath: string, name: string): {
64
+ static inspectProjectProfile(fullPath: string, _name?: string): {
57
65
  build_command: string | null;
58
66
  deploy_method: string | null;
59
67
  deploy_command: string | null;
60
68
  live_url: string | null;
69
+ deploy_source: string;
70
+ deploy_confidence: string;
71
+ deploy_evidence: string;
61
72
  };
62
73
  /**
63
74
  * Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
@@ -88,6 +99,12 @@ export declare class AgileMonsterClient {
88
99
  }>): Promise<{
89
100
  tasks: Card[];
90
101
  }>;
102
+ listComments(cardId: string): Promise<CardComment[]>;
103
+ /**
104
+ * Tell the server how a deployment went so the board binding learns the verified route (or the failure).
105
+ * Older backends do not have this endpoint; that must never break a deployment.
106
+ */
107
+ reportDeployment(report: DeployReport): Promise<boolean>;
91
108
  autonomousStep(payload: {
92
109
  card_id: string;
93
110
  action: 'select_files' | 'generate_edits';
package/dist/client.js CHANGED
@@ -1,7 +1,10 @@
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.16';
4
+ import { resolveDeployProfile } from './deploy-profile.js';
5
+ import { loadMemory } from './deploy-memory.js';
6
+ import { stripRemoteCredentials } from './redact.js';
7
+ export const MCP_SERVER_VERSION = '1.0.21';
5
8
  export class AgileMonsterClient {
6
9
  apiKey = '';
7
10
  baseUrl;
@@ -85,7 +88,7 @@ export class AgileMonsterClient {
85
88
  catch {
86
89
  // Ignore reading error
87
90
  }
88
- this.cachedConfig = null;
91
+ // Not cached: bootstrap may write the file while this server is running, and the tools must then see it
89
92
  return null;
90
93
  }
91
94
  saveProjectConfig(config, dir) {
@@ -155,7 +158,7 @@ export class AgileMonsterClient {
155
158
  if (boardId)
156
159
  params.set('board_id', boardId);
157
160
  if (gitRemote)
158
- params.set('git_remote', gitRemote);
161
+ params.set('git_remote', stripRemoteCredentials(gitRemote));
159
162
  if (detectedIdes && detectedIdes.length > 0) {
160
163
  params.set('detected_ides', detectedIdes.join(','));
161
164
  }
@@ -180,13 +183,14 @@ export class AgileMonsterClient {
180
183
  agents,
181
184
  project_name: projectName,
182
185
  project_path: projectPath,
183
- git_remote: gitRemote,
186
+ git_remote: gitRemote ? stripRemoteCredentials(gitRemote) : gitRemote,
184
187
  mcp_version: MCP_SERVER_VERSION,
185
188
  connected: true,
186
189
  }),
187
190
  });
188
191
  }
189
192
  async pulse(options) {
193
+ const installed = options.installedAgents || AgileMonsterClient.detectInstalledAgents();
190
194
  return this.request('/api/agent/pulse', {
191
195
  method: 'POST',
192
196
  body: JSON.stringify({
@@ -197,9 +201,73 @@ export class AgileMonsterClient {
197
201
  mcp_version: MCP_SERVER_VERSION,
198
202
  discovered_projects: options.discoveredProjects,
199
203
  discovered_models: options.discoveredModels,
204
+ installed_agents: installed,
200
205
  }),
201
206
  });
202
207
  }
208
+ /**
209
+ * Cross-platform detection of AI coding agents installed on this computer.
210
+ * Checks for Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf, Aider.
211
+ */
212
+ static detectInstalledAgents() {
213
+ const homedir = process.env.HOME || process.env.USERPROFILE || '';
214
+ const detected = new Set();
215
+ const exists = (...p) => {
216
+ try {
217
+ const full = path.join(...p);
218
+ return fs.existsSync(full);
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ };
224
+ // 1. Antigravity
225
+ if (exists(homedir, '.gemini') ||
226
+ exists(homedir, '.antigravity') ||
227
+ Boolean(process.env.ANTIGRAVITY_APP_DATA_DIR) ||
228
+ Boolean(process.env.GEMINI_CLI)) {
229
+ detected.add('antigravity');
230
+ }
231
+ // 2. Cursor
232
+ if (exists(homedir, '.cursor') ||
233
+ exists(homedir, 'Library', 'Application Support', 'Cursor') ||
234
+ exists(process.env.APPDATA || '', 'Cursor') ||
235
+ exists(homedir, '.config', 'Cursor') ||
236
+ exists(process.cwd(), '.cursor')) {
237
+ detected.add('cursor');
238
+ }
239
+ // 3. Claude Code
240
+ if (exists(homedir, '.claude') ||
241
+ exists(homedir, '.claude.json') ||
242
+ exists(homedir, '.config', 'claude') ||
243
+ exists(process.cwd(), '.mcp.json')) {
244
+ detected.add('claude_code');
245
+ }
246
+ // 4. Codex
247
+ if (exists(homedir, '.codex') ||
248
+ exists(process.cwd(), '.codex')) {
249
+ detected.add('codex');
250
+ }
251
+ // 5. Hermes
252
+ if (exists(homedir, '.hermes') ||
253
+ exists(homedir, '.local', 'bin', 'hermes')) {
254
+ detected.add('hermes');
255
+ }
256
+ // 6. Windsurf
257
+ if (exists(homedir, '.codeium', 'windsurf') ||
258
+ exists(homedir, '.windsurf') ||
259
+ exists(homedir, 'Library', 'Application Support', 'Windsurf') ||
260
+ exists(process.env.APPDATA || '', 'Windsurf') ||
261
+ exists(homedir, '.config', 'Windsurf')) {
262
+ detected.add('windsurf');
263
+ }
264
+ // 7. Aider
265
+ if (exists(homedir, '.aider') ||
266
+ exists(homedir, '.aider.conf.yml')) {
267
+ detected.add('aider');
268
+ }
269
+ return Array.from(detected);
270
+ }
203
271
  /**
204
272
  * Discover live models installed/configured across the developer's local AI coding agents
205
273
  * (Codex, Cursor, Claude Code, Hermes, Antigravity) so the Agile Monster app & website
@@ -328,103 +396,35 @@ export class AgileMonsterClient {
328
396
  return result;
329
397
  }
330
398
  /**
331
- * Auto-detect build command, deployment method, deploy script, and live URL for a project repository.
399
+ * Describe how a project folder builds and deploys, for the server's project/board bindings.
400
+ * Backed by the evidence-ranked resolver (running containers, real workflow triggers, deploy scripts,
401
+ * project docs) plus this machine's deployment memory, so what the web app shows matches what will run.
332
402
  */
333
- static inspectProjectProfile(fullPath, name) {
334
- let buildCmd = null;
335
- let deployMethod = null;
336
- let deployCmd = null;
337
- let liveUrl = null;
338
- // 1. Detect Build Command
403
+ static inspectProjectProfile(fullPath, _name) {
339
404
  try {
340
- const fePkg = path.join(fullPath, 'frontend', 'package.json');
341
- const rootPkg = path.join(fullPath, 'package.json');
342
- if (fs.existsSync(fePkg)) {
343
- const pkg = JSON.parse(fs.readFileSync(fePkg, 'utf8'));
344
- if (pkg?.scripts?.build) {
345
- buildCmd = fs.existsSync(path.join(fullPath, 'frontend', 'pnpm-lock.yaml'))
346
- ? 'cd frontend && pnpm run build'
347
- : 'cd frontend && npm run build';
348
- }
349
- }
350
- else if (fs.existsSync(rootPkg)) {
351
- const pkg = JSON.parse(fs.readFileSync(rootPkg, 'utf8'));
352
- if (pkg?.scripts?.build) {
353
- buildCmd = fs.existsSync(path.join(fullPath, 'pnpm-lock.yaml'))
354
- ? 'pnpm run build'
355
- : 'npm run build';
356
- }
357
- }
358
- else if (fs.existsSync(path.join(fullPath, 'frontend-flutter', 'pubspec.yaml'))) {
359
- buildCmd = 'cd frontend-flutter && flutter analyze';
360
- }
361
- else if (fs.existsSync(path.join(fullPath, 'pubspec.yaml'))) {
362
- buildCmd = 'flutter analyze';
363
- }
405
+ const profile = resolveDeployProfile(fullPath, { memory: loadMemory(fullPath) });
406
+ const source = profile.source === 'explicit' ? 'user' : profile.source === 'learned' || profile.source === 'verified' ? 'verified' : 'auto';
407
+ return {
408
+ build_command: profile.buildCommand,
409
+ deploy_method: profile.method === 'none' ? null : profile.method,
410
+ deploy_command: profile.command,
411
+ live_url: profile.liveUrl,
412
+ deploy_source: source,
413
+ deploy_confidence: profile.confidence,
414
+ deploy_evidence: profile.evidence.slice(0, 3).join(' · ').slice(0, 480),
415
+ };
364
416
  }
365
- catch { }
366
- // 2. Detect Deploy Method & Command
367
- try {
368
- const deploySh = path.join(fullPath, 'deploy', 'deploy.sh');
369
- const scriptsDeploySh = path.join(fullPath, 'scripts', 'deploy.sh');
370
- if (fs.existsSync(deploySh)) {
371
- deployMethod = 'deploy_script';
372
- const content = fs.readFileSync(deploySh, 'utf8');
373
- deployCmd = content.includes('--skip-preflight')
374
- ? './deploy/deploy.sh --skip-preflight'
375
- : './deploy/deploy.sh';
376
- }
377
- else if (fs.existsSync(scriptsDeploySh)) {
378
- deployMethod = 'deploy_script';
379
- deployCmd = './scripts/deploy.sh';
380
- }
381
- else if (fs.existsSync(path.join(fullPath, '.github', 'workflows'))) {
382
- deployMethod = 'github_actions';
383
- deployCmd = 'git push origin HEAD (GitHub Actions CI/CD)';
384
- }
385
- else if (fs.existsSync(path.join(fullPath, 'docker-compose.yml'))) {
386
- deployMethod = 'docker_compose';
387
- deployCmd = 'docker compose up -d --build';
388
- }
389
- else if (fs.existsSync(path.join(fullPath, '.git'))) {
390
- deployMethod = 'git_push';
391
- deployCmd = 'git push origin HEAD';
392
- }
393
- }
394
- catch { }
395
- // 3. Detect Live URL
396
- try {
397
- for (const docFile of ['CLAUDE.md', 'AGENTS.md', 'README.md', '.env']) {
398
- const docPath = path.join(fullPath, docFile);
399
- if (fs.existsSync(docPath)) {
400
- const text = fs.readFileSync(docPath, 'utf8').slice(0, 6000);
401
- const urlMatch = text.match(/https:\/\/(?:www\.)?([a-z0-9-]+\.(?:com|ai|io|app|dev|org|net))\b/i);
402
- if (urlMatch && !/github\.com|openai\.com|anthropic\.com|googleapis\.com|npmjs\.com|localhost/i.test(urlMatch[0])) {
403
- liveUrl = urlMatch[0];
404
- break;
405
- }
406
- }
407
- }
408
- if (!liveUrl) {
409
- const clean = name.toLowerCase().replace(/^nieuw\.games[-_]*/i, '');
410
- if (/^[a-z0-9-]+\.(com|ai|io|app|dev|org|net)$/i.test(clean)) {
411
- liveUrl = `https://${clean}`;
412
- }
413
- else if (clean === 'meerlink') {
414
- liveUrl = 'https://meerlink.com';
415
- }
416
- else if (clean === 'werkinghard' || clean === 'workinghard') {
417
- liveUrl = 'https://werkinghard.com';
418
- }
419
- }
417
+ catch {
418
+ return {
419
+ build_command: null,
420
+ deploy_method: null,
421
+ deploy_command: null,
422
+ live_url: null,
423
+ deploy_source: 'auto',
424
+ deploy_confidence: 'low',
425
+ deploy_evidence: '',
426
+ };
420
427
  }
421
- catch { }
422
- return {
423
- build_command: buildCmd,
424
- deploy_method: deployMethod,
425
- deploy_command: deployCmd,
426
- live_url: liveUrl,
427
- };
428
428
  }
429
429
  /**
430
430
  * Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
@@ -496,12 +496,10 @@ export class AgileMonsterClient {
496
496
  if (fs.existsSync(path.join(fullPath, '.git'))) {
497
497
  try {
498
498
  gitRemote =
499
- execSync('git config --get remote.origin.url', {
499
+ stripRemoteCredentials(execSync('git config --get remote.origin.url', {
500
500
  cwd: fullPath,
501
501
  stdio: ['ignore', 'pipe', 'ignore'],
502
- })
503
- .toString()
504
- .trim() || null;
502
+ }).toString()) || null;
505
503
  }
506
504
  catch { }
507
505
  }
@@ -608,6 +606,26 @@ export class AgileMonsterClient {
608
606
  body: JSON.stringify({ provider, tasks }),
609
607
  });
610
608
  }
609
+ async listComments(cardId) {
610
+ const res = await this.request(`/api/cards/${cardId}/comments`);
611
+ return Array.isArray(res) ? res : res?.comments || [];
612
+ }
613
+ /**
614
+ * Tell the server how a deployment went so the board binding learns the verified route (or the failure).
615
+ * Older backends do not have this endpoint; that must never break a deployment.
616
+ */
617
+ async reportDeployment(report) {
618
+ try {
619
+ await this.request('/api/agent/deploy-report', {
620
+ method: 'POST',
621
+ body: JSON.stringify({ ...report, mcp_version: MCP_SERVER_VERSION }),
622
+ });
623
+ return true;
624
+ }
625
+ catch {
626
+ return false;
627
+ }
628
+ }
611
629
  async autonomousStep(payload) {
612
630
  return this.request('/api/agent/autonomous-step', {
613
631
  method: 'POST',
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Machine-local deployment memory, one file per project folder.
3
+ *
4
+ * This is the feedback loop that was missing: every deployment attempt (from the daemon or reported
5
+ * by an IDE agent) is recorded here, so the next card starts from "what worked last time" instead of
6
+ * re-guessing — and a method that keeps failing because it does not apply gets demoted instead of retried.
7
+ *
8
+ * Lives in ~/.agilemonster/projects/ (never inside the repository, so it can never be committed).
9
+ */
10
+ export interface DeployAttempt {
11
+ at: string;
12
+ card_id?: string | null;
13
+ method: string;
14
+ command?: string | null;
15
+ ok: boolean;
16
+ pending?: boolean;
17
+ kind?: string | null;
18
+ error?: string | null;
19
+ commit?: string | null;
20
+ duration_ms?: number;
21
+ reported_by?: string | null;
22
+ }
23
+ export interface MethodFailures {
24
+ count: number;
25
+ last_kind: string | null;
26
+ last_error: string | null;
27
+ last_at: string;
28
+ /** The failure says the method does not apply here (as opposed to a real deploy breaking). */
29
+ wrong_method: boolean;
30
+ }
31
+ /** A deployment whose CI run had not finished when the daemon stopped waiting; re-checked on later pulses. */
32
+ export interface PendingVerification {
33
+ card_id: string;
34
+ card_title: string;
35
+ provider: string;
36
+ sha: string;
37
+ branch: string;
38
+ workflow_names: string[];
39
+ started_at: string;
40
+ run_url?: string | null;
41
+ }
42
+ /** What the agent changed for a card whose deployment has not succeeded (or not run) yet. */
43
+ export interface CardDeployState {
44
+ stage: 'deploy_failed' | 'awaiting_approval';
45
+ files: string[];
46
+ summary: string;
47
+ verification_steps: string;
48
+ diff: string;
49
+ commit: string | null;
50
+ at: string;
51
+ attempts: number;
52
+ last_error?: string | null;
53
+ /** Set when the agent asked "may I deploy with X?": a plain "yes"/"retry" then confirms X. */
54
+ pending_confirmation?: {
55
+ method: string;
56
+ command: string | null;
57
+ } | null;
58
+ }
59
+ export interface ProjectMemory {
60
+ project_path: string;
61
+ updated_at: string;
62
+ explicit?: {
63
+ method: string;
64
+ command?: string | null;
65
+ live_url?: string | null;
66
+ notes?: string | null;
67
+ set_by?: string | null;
68
+ set_at: string;
69
+ } | null;
70
+ last_success?: {
71
+ method: string;
72
+ command?: string | null;
73
+ live_url?: string | null;
74
+ at: string;
75
+ commit?: string | null;
76
+ card_id?: string | null;
77
+ } | null;
78
+ failures: Record<string, MethodFailures>;
79
+ history: DeployAttempt[];
80
+ pending: PendingVerification[];
81
+ cards: Record<string, CardDeployState>;
82
+ /**
83
+ * Cards this machine already shipped (kept apart from the rolling history, so approving a card long after
84
+ * its review-time deployment never looks like "not deployed yet" and triggers a second one).
85
+ */
86
+ deployed_cards: Record<string, {
87
+ at: string;
88
+ commit: string | null;
89
+ method: string;
90
+ verified: boolean;
91
+ }>;
92
+ }
93
+ export declare function memoryFileFor(projectPath: string): string;
94
+ /**
95
+ * Recover "how the last deployment worked" from the activity log earlier daemon versions wrote into the
96
+ * project (`.agilemonster/ACTIVITY.md`), so projects that already deployed successfully keep their route
97
+ * instead of starting from a fresh guess.
98
+ */
99
+ export declare function parseLastDeploymentFromActivityLog(logText: string): {
100
+ method: string;
101
+ command: null;
102
+ } | null;
103
+ export declare function loadMemory(projectPath: string): ProjectMemory;
104
+ export declare function saveMemory(memory: ProjectMemory): void;
105
+ /** Record the outcome of an attempt and update what the project "knows" about its own deployment. */
106
+ export declare function recordAttempt(projectPath: string, attempt: Omit<DeployAttempt, 'at'> & {
107
+ live_url?: string | null;
108
+ wrong_method?: boolean;
109
+ /** The change went out but nothing could confirm the deployment (no CI run found, `gh` unavailable). */
110
+ unverified?: boolean;
111
+ }): ProjectMemory;
112
+ export declare function setExplicitProfile(projectPath: string, explicit: {
113
+ method: string;
114
+ command?: string | null;
115
+ live_url?: string | null;
116
+ notes?: string | null;
117
+ set_by?: string | null;
118
+ }): ProjectMemory;
119
+ export declare function setCardState(projectPath: string, cardId: string, state: CardDeployState | null): void;
120
+ export declare function getCardState(projectPath: string, cardId: string): CardDeployState | null;
121
+ export declare function addPendingVerification(projectPath: string, pending: PendingVerification): void;
122
+ export declare function removePendingVerification(projectPath: string, cardId: string): void;