@agilemonster/mcp-server 1.0.15 → 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.
@@ -1 +1 @@
1
- export declare const AGENT_GUIDELINES_MARKDOWN = "# Autonomous AI Agent Operational Guidelines (Agile Monster)\n\nThis document provides mandatory operational protocols for AI coding assistants (**Cursor, Claude Code, Antigravity, Codex, Hermes Agent, Windsurf, Aider**) working inside repositories integrated with **Agile Monster** (https://app.agilemonster.com).\n\n---\n\n## \uD83E\uDDED Core Principles\n\n1. **The Agile Monster Board is the Source of Truth**: \n All non-trivial feature additions, bug fixes, refactors, and deployments must correspond to a task on the project's Agile Monster board.\n2. **One Active Assignee**: \n Work on cards assigned specifically to your agent provider (`cursor`, `claude_code`, `antigravity`, `codex`, `hermes`, `windsurf`, `aider`, or `custom`). Never claim or alter tasks assigned to other agents or human teammates without explicit user direction.\n3. **No Hallucinated Requirements**: \n When a specification, API schema, or environment variable is ambiguous, stop and ask the user rather than guessing.\n4. **Human Verification Gate**: \n Autonomous agents must **never** close a task directly to `Done`. You must submit work for human testing and verification using `agilemonster_submit_for_review`.\n\n---\n\n## \uD83D\uDD04 Task Lifecycle Protocol\n\nFollow this exact sequential protocol when working on any task:\n\n1. **Context & Discovery**:\n - Inspect `.agilemonster.json` in the project root if present.\n - Run `agilemonster_get_context` to retrieve the board ID, list columns (`Backlog`, `In Progress`, `Review`, `Done`), and active tasks.\n\n2. **Task Intake**:\n - Check which tasks are queued for your agent using `agilemonster_list_my_tasks({ \"status\": \"queued\" })`.\n - Confirm the card is assigned to your provider before proceeding.\n\n3. **Claiming the Task**:\n - Before editing files or running build tools, claim the task:\n `agilemonster_claim_task({ \"card_id\": \"<card_uuid>\" })`\n - This moves the card status to `in_progress` and logs an activity event on the board.\n\n4. **Implementation, Questions & Progress**:\n - **When Blocked or Unclear**: Call `agilemonster_ask_question({ \"card_id\": \"<card_uuid>\", \"question\": \"...\" })`. This sets status to `waiting_for_input` so the user is alerted.\n - **Logging Progress**: For non-trivial milestones, call `agilemonster_add_progress({ \"card_id\": \"<card_uuid>\", \"update\": \"...\", \"tokens_used\": 1500, \"cost_usd\": 0.003 })`.\n - **Cost Safeguards**: Use `agilemonster_set_budget` and `agilemonster_record_spend` to protect against runaway billing loops.\n\n5. **Submission for Human Review & Testing**:\n - When code is implemented, compiled, and tested locally:\n - Call `agilemonster_submit_for_review({ \"card_id\": \"<card_uuid>\", \"summary\": \"...\", \"verification_steps\": \"...\" })`.\n - This automatically moves the card to the board's **Testing / QA / Review** column, reassigns the card back to the human who requested it, sets `agent_status` to `review`, and posts step-by-step verification instructions.\n - Never mark the card directly as \"Done\". The user will verify your test steps and sign off.\n\n6. **Suggest Follow-Up Tasks**:\n - Proactively suggest follow-up cards (deploy to test/staging, deploy to production, add integration tests) using:\n `agilemonster_create_followup_tasks({ \"card_id\": \"<card_uuid>\", \"tasks\": [...] })`.\n";
1
+ export declare const AGENT_GUIDELINES_MARKDOWN = "# Autonomous AI Agent Operational Guidelines (Agile Monster)\n\nThis document provides mandatory operational protocols for AI coding assistants (**Cursor, Claude Code, Antigravity, Codex, Hermes Agent, Windsurf, Aider**) working inside repositories integrated with **Agile Monster** (https://app.agilemonster.com).\n\n---\n\n## \uD83E\uDDED Core Principles\n\n1. **The Agile Monster Board is the Source of Truth**: \n All non-trivial feature additions, bug fixes, refactors, and deployments must correspond to a task on the project's Agile Monster board.\n2. **One Active Assignee**: \n Work on cards assigned specifically to your agent provider (`cursor`, `claude_code`, `antigravity`, `codex`, `hermes`, `windsurf`, `aider`, or `custom`). Never claim or alter tasks assigned to other agents or human teammates without explicit user direction.\n3. **No Hallucinated Requirements**: \n When a specification, API schema, or environment variable is ambiguous, stop and ask the user rather than guessing.\n4. **Human Verification Gate**: \n Autonomous agents must **never** close a task directly to `Done`. You must submit work for human testing and verification using `agilemonster_submit_for_review`.\n\n---\n\n## \uD83D\uDD04 Task Lifecycle Protocol\n\nFollow this exact sequential protocol when working on any task:\n\n1. **Context & Discovery**:\n - Inspect `.agilemonster.json` in the project root if present.\n - Run `agilemonster_get_context` to retrieve the board ID, list columns (`Backlog`, `In Progress`, `Review`, `Done`), and active tasks.\n\n2. **Task Intake**:\n - Check which tasks are queued for your agent using `agilemonster_list_my_tasks({ \"status\": \"queued\" })`.\n - Confirm the card is assigned to your provider before proceeding.\n\n3. **Claiming the Task**:\n - Before editing files or running build tools, claim the task:\n `agilemonster_claim_task({ \"card_id\": \"<card_uuid>\" })`\n - This moves the card status to `in_progress` and logs an activity event on the board.\n\n4. **Implementation, Questions & Progress**:\n - **When Blocked or Unclear**: Call `agilemonster_ask_question({ \"card_id\": \"<card_uuid>\", \"question\": \"...\" })`. This sets status to `waiting_for_input` so the user is alerted.\n - **Logging Progress**: For non-trivial milestones, call `agilemonster_add_progress({ \"card_id\": \"<card_uuid>\", \"update\": \"...\", \"tokens_used\": 1500, \"cost_usd\": 0.003 })`.\n - **Cost Safeguards**: Use `agilemonster_set_budget` and `agilemonster_record_spend` to protect against runaway billing loops.\n\n5. **Submission for Human Review & Testing**:\n - When code is implemented, compiled, and tested locally:\n - Call `agilemonster_submit_for_review({ \"card_id\": \"<card_uuid>\", \"summary\": \"...\", \"verification_steps\": \"...\" })`.\n - This automatically moves the card to the board's **Testing / QA / Review** column, reassigns the card back to the human who requested it, sets `agent_status` to `review`, and posts step-by-step verification instructions.\n - Never mark the card directly as \"Done\". The user will verify your test steps and sign off.\n\n6. **Suggest Follow-Up Tasks**:\n - Proactively suggest follow-up cards (deploy to test/staging, deploy to production, add integration tests) using:\n `agilemonster_create_followup_tasks({ \"card_id\": \"<card_uuid>\", \"tasks\": [...] })`.\n\n7. **Deployment Memory (keeps web-started cards as informed as you are)**:\n - Before deploying or writing a deployment note, call `agilemonster_get_deploy_profile({})`. It returns the route this project really uses (GitHub Actions, local Docker Compose, deploy script, cloud CLI, manual), the exact command, the evidence, and what happened on recent deployments. Do not guess.\n - After every deployment you run or observe, call `agilemonster_record_deployment({ \"method\": \"...\", \"command\": \"...\", \"live_url\": \"...\", \"success\": true })`. On failure pass `\"success\": false` and `\"error\"`.\n - When the user tells you how the project is deployed, record it with `\"pin\": true` and put anything a future agent must know in `\"notes\"`.\n - This is how tasks created from the Agile Monster web app (which run in the background, without your IDE context) learn to deploy the same proven way you did.\n";
@@ -48,4 +48,10 @@ Follow this exact sequential protocol when working on any task:
48
48
  6. **Suggest Follow-Up Tasks**:
49
49
  - Proactively suggest follow-up cards (deploy to test/staging, deploy to production, add integration tests) using:
50
50
  \`agilemonster_create_followup_tasks({ "card_id": "<card_uuid>", "tasks": [...] })\`.
51
+
52
+ 7. **Deployment Memory (keeps web-started cards as informed as you are)**:
53
+ - Before deploying or writing a deployment note, call \`agilemonster_get_deploy_profile({})\`. It returns the route this project really uses (GitHub Actions, local Docker Compose, deploy script, cloud CLI, manual), the exact command, the evidence, and what happened on recent deployments. Do not guess.
54
+ - After every deployment you run or observe, call \`agilemonster_record_deployment({ "method": "...", "command": "...", "live_url": "...", "success": true })\`. On failure pass \`"success": false\` and \`"error"\`.
55
+ - When the user tells you how the project is deployed, record it with \`"pin": true\` and put anything a future agent must know in \`"notes"\`.
56
+ - This is how tasks created from the Agile Monster web app (which run in the background, without your IDE context) learn to deploy the same proven way you did.
51
57
  `;
@@ -0,0 +1,44 @@
1
+ export declare const SERVER_NAME = "agilemonster";
2
+ export interface McpServerEntry {
3
+ command: string;
4
+ args: string[];
5
+ env: Record<string, string>;
6
+ }
7
+ export declare function serverEntry(apiKey: string): McpServerEntry;
8
+ export type WriteOutcome = 'written' | 'unreadable';
9
+ /** Remove // and block comments and trailing commas, leaving string contents alone. */
10
+ export declare function stripJsonComments(text: string): string;
11
+ /** Parse a config file's text. `strict` is false when it only parsed after removing comments. */
12
+ export declare function parseConfigJson(raw: string): {
13
+ value: any;
14
+ strict: boolean;
15
+ } | null;
16
+ /**
17
+ * Add or refresh our server under `rootKey` in a JSON config file. `removeFrom` names a root key an older
18
+ * version of this package wrote to by mistake; our entry is removed from there.
19
+ */
20
+ export declare function mergeJsonMcpConfig(file: string, rootKey: string, entry: object, removeFrom?: string): WriteOutcome;
21
+ /** Codex CLI keeps MCP servers in TOML tables: [mcp_servers.<name>] with an env sub-table. */
22
+ export declare function upsertCodexToml(existing: string, entry: McpServerEntry): string;
23
+ export declare function writeCodexConfig(file: string, entry: McpServerEntry): WriteOutcome;
24
+ export interface IdeTarget {
25
+ /** Names accepted for --ide / provider */
26
+ ids: string[];
27
+ label: string;
28
+ /** Path shown to the user */
29
+ display: string;
30
+ file: string;
31
+ /** Written for "all" only when this exists, so configs are not created for tools that are not installed */
32
+ presentIf: string | null;
33
+ write: (file: string, apiKey: string) => WriteOutcome;
34
+ }
35
+ /** Every client this package can configure, and where each keeps its MCP servers. */
36
+ export declare function ideTargets(cwd: string, homedir: string): IdeTarget[];
37
+ export interface IdeResult {
38
+ label: string;
39
+ display: string;
40
+ outcome: WriteOutcome | 'error';
41
+ error?: string;
42
+ }
43
+ /** Configure one named client, or with "all" every project-level client plus the installed global ones. */
44
+ export declare function configureIdeTargets(apiKey: string, selectedIde: string | undefined, cwd: string, homedir: string): IdeResult[];
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Writes the Agile Monster entry into an agent's or IDE's MCP configuration without disturbing anything else
3
+ * in that file. Each client has its own file, root key and format; the rules that apply to all of them:
4
+ *
5
+ * - merge, never replace: the user's other MCP servers stay;
6
+ * - a file that cannot be understood is left exactly as it is (it used to be overwritten);
7
+ * - a file with comments or trailing commas (JSONC) is read correctly, and backed up before it is rewritten.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ export const SERVER_NAME = 'agilemonster';
12
+ const PACKAGE_SPEC = '@agilemonster/mcp-server@latest';
13
+ const API_URL = 'https://api.agilemonster.com';
14
+ export function serverEntry(apiKey) {
15
+ return {
16
+ command: 'npx',
17
+ args: ['-y', PACKAGE_SPEC],
18
+ env: { AGILEMONSTER_API_KEY: apiKey, AGILEMONSTER_API_URL: API_URL },
19
+ };
20
+ }
21
+ /** Remove // and block comments and trailing commas, leaving string contents alone. */
22
+ export function stripJsonComments(text) {
23
+ let out = '';
24
+ let i = 0;
25
+ let inString = false;
26
+ while (i < text.length) {
27
+ const ch = text[i];
28
+ const next = text[i + 1];
29
+ if (inString) {
30
+ out += ch;
31
+ if (ch === '\\' && i + 1 < text.length) {
32
+ out += next;
33
+ i += 2;
34
+ continue;
35
+ }
36
+ if (ch === '"')
37
+ inString = false;
38
+ i++;
39
+ continue;
40
+ }
41
+ if (ch === '"') {
42
+ inString = true;
43
+ out += ch;
44
+ i++;
45
+ continue;
46
+ }
47
+ if (ch === '/' && next === '/') {
48
+ while (i < text.length && text[i] !== '\n')
49
+ i++;
50
+ continue;
51
+ }
52
+ if (ch === '/' && next === '*') {
53
+ const end = text.indexOf('*/', i + 2);
54
+ i = end === -1 ? text.length : end + 2;
55
+ continue;
56
+ }
57
+ if (ch === ',') {
58
+ // A comma followed only by whitespace/comments and a closing bracket is a trailing comma
59
+ let j = i + 1;
60
+ for (;;) {
61
+ while (j < text.length && /\s/.test(text[j]))
62
+ j++;
63
+ if (text[j] === '/' && text[j + 1] === '/') {
64
+ while (j < text.length && text[j] !== '\n')
65
+ j++;
66
+ continue;
67
+ }
68
+ if (text[j] === '/' && text[j + 1] === '*') {
69
+ const end = text.indexOf('*/', j + 2);
70
+ j = end === -1 ? text.length : end + 2;
71
+ continue;
72
+ }
73
+ break;
74
+ }
75
+ if (text[j] === '}' || text[j] === ']') {
76
+ i++;
77
+ continue;
78
+ }
79
+ }
80
+ out += ch;
81
+ i++;
82
+ }
83
+ return out;
84
+ }
85
+ /** Parse a config file's text. `strict` is false when it only parsed after removing comments. */
86
+ export function parseConfigJson(raw) {
87
+ const text = raw.replace(/^/, '');
88
+ if (text.trim() === '')
89
+ return { value: {}, strict: true };
90
+ try {
91
+ return { value: JSON.parse(text), strict: true };
92
+ }
93
+ catch {
94
+ try {
95
+ return { value: JSON.parse(stripJsonComments(text)), strict: false };
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Add or refresh our server under `rootKey` in a JSON config file. `removeFrom` names a root key an older
104
+ * version of this package wrote to by mistake; our entry is removed from there.
105
+ */
106
+ export function mergeJsonMcpConfig(file, rootKey, entry, removeFrom) {
107
+ let config = {};
108
+ if (fs.existsSync(file)) {
109
+ const raw = fs.readFileSync(file, 'utf8');
110
+ const parsed = parseConfigJson(raw);
111
+ if (!parsed || typeof parsed.value !== 'object' || parsed.value === null || Array.isArray(parsed.value)) {
112
+ return 'unreadable';
113
+ }
114
+ config = parsed.value;
115
+ if (!parsed.strict) {
116
+ // Rewriting drops the comments, so keep the original next to it
117
+ fs.writeFileSync(`${file}.bak`, raw, 'utf8');
118
+ }
119
+ }
120
+ else {
121
+ fs.mkdirSync(path.dirname(file), { recursive: true });
122
+ }
123
+ if (typeof config[rootKey] !== 'object' || config[rootKey] === null || Array.isArray(config[rootKey])) {
124
+ config[rootKey] = {};
125
+ }
126
+ config[rootKey][SERVER_NAME] = entry;
127
+ if (removeFrom && removeFrom !== rootKey && config[removeFrom] && typeof config[removeFrom] === 'object') {
128
+ delete config[removeFrom][SERVER_NAME];
129
+ if (Object.keys(config[removeFrom]).length === 0)
130
+ delete config[removeFrom];
131
+ }
132
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
133
+ return 'written';
134
+ }
135
+ /** Codex CLI keeps MCP servers in TOML tables: [mcp_servers.<name>] with an env sub-table. */
136
+ export function upsertCodexToml(existing, entry) {
137
+ const ours = new RegExp(`^\\s*\\[\\s*mcp_servers\\.${SERVER_NAME}(\\.[^\\]]+)?\\s*\\]\\s*(#.*)?$`);
138
+ const anyTable = /^\s*\[/;
139
+ const kept = [];
140
+ let skipping = false;
141
+ for (const line of existing.split(/\r?\n/)) {
142
+ if (ours.test(line)) {
143
+ skipping = true;
144
+ continue;
145
+ }
146
+ if (skipping && anyTable.test(line))
147
+ skipping = false;
148
+ if (!skipping)
149
+ kept.push(line);
150
+ }
151
+ const before = kept.join('\n').trimEnd();
152
+ const q = (v) => JSON.stringify(v);
153
+ const block = [
154
+ `[mcp_servers.${SERVER_NAME}]`,
155
+ `command = ${q(entry.command)}`,
156
+ `args = [${entry.args.map(q).join(', ')}]`,
157
+ '',
158
+ `[mcp_servers.${SERVER_NAME}.env]`,
159
+ ...Object.entries(entry.env).map(([k, v]) => `${k} = ${q(v)}`),
160
+ '',
161
+ ].join('\n');
162
+ return `${before ? `${before}\n\n` : ''}${block}`;
163
+ }
164
+ export function writeCodexConfig(file, entry) {
165
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
166
+ fs.mkdirSync(path.dirname(file), { recursive: true });
167
+ fs.writeFileSync(file, upsertCodexToml(existing, entry), 'utf8');
168
+ return 'written';
169
+ }
170
+ /** Every client this package can configure, and where each keeps its MCP servers. */
171
+ export function ideTargets(cwd, homedir) {
172
+ const json = (rootKey, extra = {}, removeFrom) => (file, apiKey) => mergeJsonMcpConfig(file, rootKey, { ...extra, ...serverEntry(apiKey) }, removeFrom);
173
+ const targets = [
174
+ {
175
+ ids: ['cursor'],
176
+ label: 'Cursor',
177
+ display: '.cursor/mcp.json',
178
+ file: path.join(cwd, '.cursor', 'mcp.json'),
179
+ presentIf: null,
180
+ write: json('mcpServers'),
181
+ },
182
+ {
183
+ ids: ['claude', 'claude_code', 'claude-code'],
184
+ label: 'Claude Code',
185
+ display: '.mcp.json',
186
+ file: path.join(cwd, '.mcp.json'),
187
+ presentIf: null,
188
+ write: json('mcpServers'),
189
+ },
190
+ {
191
+ // VS Code (Copilot agent mode) reads "servers", not "mcpServers"; earlier versions wrote the wrong key
192
+ ids: ['vscode', 'vs_code', 'copilot', 'github_copilot'],
193
+ label: 'VS Code',
194
+ display: '.vscode/mcp.json',
195
+ file: path.join(cwd, '.vscode', 'mcp.json'),
196
+ presentIf: path.join(cwd, '.vscode'),
197
+ write: json('servers', { type: 'stdio' }, 'mcpServers'),
198
+ },
199
+ ];
200
+ if (homedir) {
201
+ targets.push({
202
+ ids: ['windsurf'],
203
+ label: 'Windsurf',
204
+ display: '~/.codeium/windsurf/mcp_config.json',
205
+ file: path.join(homedir, '.codeium', 'windsurf', 'mcp_config.json'),
206
+ presentIf: path.join(homedir, '.codeium', 'windsurf'),
207
+ write: json('mcpServers'),
208
+ }, {
209
+ ids: ['antigravity', 'gemini'],
210
+ label: 'Antigravity',
211
+ display: '~/.gemini/config/mcp_config.json',
212
+ file: path.join(homedir, '.gemini', 'config', 'mcp_config.json'),
213
+ presentIf: path.join(homedir, '.gemini'),
214
+ write: json('mcpServers'),
215
+ }, {
216
+ ids: ['gemini_cli', 'gemini-cli', 'gemini'],
217
+ label: 'Gemini CLI',
218
+ display: '~/.gemini/settings.json',
219
+ file: path.join(homedir, '.gemini', 'settings.json'),
220
+ presentIf: path.join(homedir, '.gemini', 'settings.json'),
221
+ write: json('mcpServers'),
222
+ }, {
223
+ ids: ['codex', 'codex_cli', 'codex-cli'],
224
+ label: 'Codex CLI',
225
+ display: '~/.codex/config.toml',
226
+ file: path.join(homedir, '.codex', 'config.toml'),
227
+ presentIf: path.join(homedir, '.codex'),
228
+ write: (file, apiKey) => writeCodexConfig(file, serverEntry(apiKey)),
229
+ });
230
+ }
231
+ return targets;
232
+ }
233
+ /** Configure one named client, or with "all" every project-level client plus the installed global ones. */
234
+ export function configureIdeTargets(apiKey, selectedIde, cwd, homedir) {
235
+ const ide = (selectedIde || 'all').toLowerCase().trim();
236
+ const results = [];
237
+ for (const target of ideTargets(cwd, homedir)) {
238
+ const named = target.ids.includes(ide);
239
+ const implied = ide === 'all' && (target.presentIf === null || fs.existsSync(target.presentIf));
240
+ if (!named && !implied)
241
+ continue;
242
+ try {
243
+ results.push({ label: target.label, display: target.display, outcome: target.write(target.file, apiKey) });
244
+ }
245
+ catch (e) {
246
+ results.push({ label: target.label, display: target.display, outcome: 'error', error: e?.message || String(e) });
247
+ }
248
+ }
249
+ return results;
250
+ }
@@ -28,5 +28,6 @@ export declare class AgileMonsterOrchestrator {
28
28
  board?: string;
29
29
  }): Promise<void>;
30
30
  configureIdes(apiKey: string, selectedIde?: string): void;
31
+ /** Keep the files that hold the personal API key out of commits. */
31
32
  private ensureGitignore;
32
33
  }
@@ -3,6 +3,9 @@ import fs from 'node:fs';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { AgileMonsterClient } from './client.js';
5
5
  import { AGENT_GUIDELINES_MARKDOWN } from './guidelines.js';
6
+ import { configureIdeTargets } from './ide-config.js';
7
+ import { ensureLocalGitExcludes } from './deploy-runner.js';
8
+ import { stripRemoteCredentials } from './redact.js';
6
9
  export class AgileMonsterOrchestrator {
7
10
  client;
8
11
  constructor(client = new AgileMonsterClient()) {
@@ -114,32 +117,15 @@ export class AgileMonsterOrchestrator {
114
117
  })
115
118
  .toString()
116
119
  .trim();
117
- return out || undefined;
120
+ // A token embedded in the remote URL must never be sent to the server
121
+ return stripRemoteCredentials(out) || undefined;
118
122
  }
119
123
  catch {
120
124
  return undefined;
121
125
  }
122
126
  }
123
127
  detectInstalledIdes() {
124
- const homedir = process.env.HOME || process.env.USERPROFILE || '';
125
- const detected = [];
126
- if (fs.existsSync(path.resolve(process.cwd(), '.cursor')) ||
127
- (homedir && fs.existsSync(path.join(homedir, '.cursor')))) {
128
- detected.push('cursor');
129
- }
130
- if (fs.existsSync(path.resolve(process.cwd(), '.mcp.json')) ||
131
- (homedir && fs.existsSync(path.join(homedir, '.claude')))) {
132
- detected.push('claude_code');
133
- }
134
- if (homedir && fs.existsSync(path.join(homedir, '.gemini'))) {
135
- detected.push('antigravity');
136
- }
137
- if (homedir && fs.existsSync(path.join(homedir, '.codeium', 'windsurf'))) {
138
- detected.push('windsurf');
139
- }
140
- if (fs.existsSync(path.resolve(process.cwd(), '.vscode'))) {
141
- detected.push('codex');
142
- }
128
+ const detected = AgileMonsterClient.detectInstalledAgents();
143
129
  return detected.length > 0 ? detected : ['cursor', 'claude_code', 'antigravity'];
144
130
  }
145
131
  async initInteractive(options) {
@@ -343,162 +329,29 @@ export class AgileMonsterOrchestrator {
343
329
  }
344
330
  configureIdes(apiKey, selectedIde) {
345
331
  const homedir = process.env.HOME || process.env.USERPROFILE || '';
346
- const ide = (selectedIde || 'all').toLowerCase();
347
- // 1. Cursor (.cursor/mcp.json)
348
- if (ide === 'all' || ide === 'cursor') {
349
- try {
350
- const cursorDir = path.resolve(process.cwd(), '.cursor');
351
- const cursorMcpPath = path.join(cursorDir, 'mcp.json');
352
- if (!fs.existsSync(cursorDir))
353
- fs.mkdirSync(cursorDir, { recursive: true });
354
- let existing = { mcpServers: {} };
355
- if (fs.existsSync(cursorMcpPath)) {
356
- try {
357
- existing = JSON.parse(fs.readFileSync(cursorMcpPath, 'utf8'));
358
- }
359
- catch { }
360
- }
361
- existing.mcpServers = existing.mcpServers || {};
362
- existing.mcpServers.agilemonster = {
363
- command: 'npx',
364
- args: ['-y', '@agilemonster/mcp-server@latest'],
365
- env: {
366
- AGILEMONSTER_API_KEY: apiKey,
367
- AGILEMONSTER_API_URL: 'https://api.agilemonster.com',
368
- },
369
- };
370
- fs.writeFileSync(cursorMcpPath, JSON.stringify(existing, null, 2), 'utf8');
371
- console.log(`🤖 Configured Cursor in: .cursor/mcp.json`);
372
- }
373
- catch (e) {
374
- console.warn(`Could not write Cursor config: ${e.message}`);
375
- }
376
- }
377
- // 2. Claude Code (.mcp.json)
378
- if (ide === 'all' || ide === 'claude' || ide === 'claude_code') {
379
- try {
380
- const claudeMcpPath = path.resolve(process.cwd(), '.mcp.json');
381
- let existing = { mcpServers: {} };
382
- if (fs.existsSync(claudeMcpPath)) {
383
- try {
384
- existing = JSON.parse(fs.readFileSync(claudeMcpPath, 'utf8'));
385
- }
386
- catch { }
387
- }
388
- existing.mcpServers = existing.mcpServers || {};
389
- existing.mcpServers.agilemonster = {
390
- command: 'npx',
391
- args: ['-y', '@agilemonster/mcp-server@latest'],
392
- env: {
393
- AGILEMONSTER_API_KEY: apiKey,
394
- AGILEMONSTER_API_URL: 'https://api.agilemonster.com',
395
- },
396
- };
397
- fs.writeFileSync(claudeMcpPath, JSON.stringify(existing, null, 2), 'utf8');
398
- console.log(`🤖 Configured Claude Code in: .mcp.json`);
399
- }
400
- catch (e) {
401
- console.warn(`Could not write Claude Code config: ${e.message}`);
332
+ for (const r of configureIdeTargets(apiKey, selectedIde, process.cwd(), homedir)) {
333
+ if (r.outcome === 'written') {
334
+ console.log(`🤖 Configured ${r.label} in: ${r.display}`);
402
335
  }
403
- }
404
- // 3. Windsurf (~/.codeium/windsurf/mcp_config.json)
405
- if ((ide === 'all' || ide === 'windsurf') && homedir) {
406
- try {
407
- const windsurfDir = path.join(homedir, '.codeium', 'windsurf');
408
- const windsurfMcpPath = path.join(windsurfDir, 'mcp_config.json');
409
- if (fs.existsSync(windsurfDir) || ide === 'windsurf') {
410
- if (!fs.existsSync(windsurfDir))
411
- fs.mkdirSync(windsurfDir, { recursive: true });
412
- let existing = { mcpServers: {} };
413
- if (fs.existsSync(windsurfMcpPath)) {
414
- try {
415
- existing = JSON.parse(fs.readFileSync(windsurfMcpPath, 'utf8'));
416
- }
417
- catch { }
418
- }
419
- existing.mcpServers = existing.mcpServers || {};
420
- existing.mcpServers.agilemonster = {
421
- command: 'npx',
422
- args: ['-y', '@agilemonster/mcp-server@latest'],
423
- env: {
424
- AGILEMONSTER_API_KEY: apiKey,
425
- AGILEMONSTER_API_URL: 'https://api.agilemonster.com',
426
- },
427
- };
428
- fs.writeFileSync(windsurfMcpPath, JSON.stringify(existing, null, 2), 'utf8');
429
- console.log(`🤖 Configured Windsurf in: ~/.codeium/windsurf/mcp_config.json`);
430
- }
431
- }
432
- catch { }
433
- }
434
- // 4. Antigravity / Gemini (~/.gemini/config/mcp_config.json)
435
- if ((ide === 'all' || ide === 'antigravity' || ide === 'gemini') && homedir) {
436
- try {
437
- const antigravityDir = path.join(homedir, '.gemini', 'config');
438
- const antigravityMcpPath = path.join(antigravityDir, 'mcp_config.json');
439
- if (!fs.existsSync(antigravityDir))
440
- fs.mkdirSync(antigravityDir, { recursive: true });
441
- let existing = { mcpServers: {} };
442
- if (fs.existsSync(antigravityMcpPath)) {
443
- try {
444
- const raw = fs.readFileSync(antigravityMcpPath, 'utf8').trim();
445
- if (raw)
446
- existing = JSON.parse(raw);
447
- }
448
- catch { }
449
- }
450
- existing.mcpServers = existing.mcpServers || {};
451
- existing.mcpServers.agilemonster = {
452
- command: 'npx',
453
- args: ['-y', '@agilemonster/mcp-server@latest'],
454
- env: {
455
- AGILEMONSTER_API_KEY: apiKey,
456
- AGILEMONSTER_API_URL: 'https://api.agilemonster.com',
457
- },
458
- };
459
- fs.writeFileSync(antigravityMcpPath, JSON.stringify(existing, null, 2), 'utf8');
460
- console.log(`🤖 Configured Antigravity in: ~/.gemini/config/mcp_config.json`);
336
+ else if (r.outcome === 'unreadable') {
337
+ console.warn(`⚠️ ${r.label}: ${r.display} could not be read as JSON, so it was left untouched. ` +
338
+ `Add the "agilemonster" server to it by hand (see the README).`);
461
339
  }
462
- catch { }
463
- }
464
- // 5. VS Code (.vscode/mcp.json)
465
- if (ide === 'all' || ide === 'vscode' || ide === 'codex') {
466
- try {
467
- const vscodeDir = path.resolve(process.cwd(), '.vscode');
468
- const vscodeMcpPath = path.join(vscodeDir, 'mcp.json');
469
- if (fs.existsSync(vscodeDir) || ide === 'vscode') {
470
- if (!fs.existsSync(vscodeDir))
471
- fs.mkdirSync(vscodeDir, { recursive: true });
472
- let existing = { mcpServers: {} };
473
- if (fs.existsSync(vscodeMcpPath)) {
474
- try {
475
- existing = JSON.parse(fs.readFileSync(vscodeMcpPath, 'utf8'));
476
- }
477
- catch { }
478
- }
479
- existing.mcpServers = existing.mcpServers || {};
480
- existing.mcpServers.agilemonster = {
481
- command: 'npx',
482
- args: ['-y', '@agilemonster/mcp-server@latest'],
483
- env: {
484
- AGILEMONSTER_API_KEY: apiKey,
485
- AGILEMONSTER_API_URL: 'https://api.agilemonster.com',
486
- },
487
- };
488
- fs.writeFileSync(vscodeMcpPath, JSON.stringify(existing, null, 2), 'utf8');
489
- console.log(`🤖 Configured VS Code in: .vscode/mcp.json`);
490
- }
340
+ else {
341
+ console.warn(`Could not write ${r.label} config (${r.display}): ${r.error}`);
491
342
  }
492
- catch { }
493
343
  }
494
344
  }
345
+ /** Keep the files that hold the personal API key out of commits. */
495
346
  ensureGitignore() {
347
+ // Per-clone exclude list: works whether or not the project has a .gitignore
348
+ ensureLocalGitExcludes(process.cwd());
496
349
  try {
497
350
  const gitignorePath = path.resolve(process.cwd(), '.gitignore');
498
351
  if (!fs.existsSync(gitignorePath))
499
352
  return;
500
353
  const content = fs.readFileSync(gitignorePath, 'utf8');
501
- const entries = ['.agilemonster.json', '.cursor/mcp.json', '.mcp.json'];
354
+ const entries = ['.agilemonster.json', '.cursor/mcp.json', '.mcp.json', '.vscode/mcp.json'];
502
355
  const missing = entries.filter((e) => !content.split(/\r?\n/).includes(e));
503
356
  if (missing.length > 0) {
504
357
  const prefix = content.endsWith('\n') ? '' : '\n';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Keeps credentials out of anything the MCP server hands back to an agent. Tool results and resources end up
3
+ * in model context, chat transcripts and IDE logs, so the API key must never appear in them, whichever tool
4
+ * produced the text and wherever the key came from (project config, env, a server response, an error).
5
+ */
6
+ export declare function redactSecrets(text: string, secrets?: Array<string | undefined | null>): string;
7
+ /** The secrets this process knows about, for exact-match masking. */
8
+ export declare function knownSecrets(apiKey?: string): string[];
9
+ /** Mask every text part of a tool result or resource read, leaving its shape untouched. */
10
+ export declare function redactResult<T>(result: T, secrets?: Array<string | undefined | null>): T;
11
+ /**
12
+ * A git remote as it may leave this machine: `https://user:token@host/...` and `https://token@host/...`
13
+ * lose their credentials; the ssh login name (`git@host:...`) is not a secret and stays.
14
+ */
15
+ export declare function stripRemoteCredentials(remote: string | null | undefined): string;
package/dist/redact.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Keeps credentials out of anything the MCP server hands back to an agent. Tool results and resources end up
3
+ * in model context, chat transcripts and IDE logs, so the API key must never appear in them, whichever tool
4
+ * produced the text and wherever the key came from (project config, env, a server response, an error).
5
+ */
6
+ const MASK = '***';
7
+ /** Agile Monster API keys, e.g. am_live_<hex>. The prefix is kept so a reader can tell what was removed. */
8
+ const AM_KEY_PATTERN = /\b(am_(?:live|test)_)[A-Za-z0-9]{8,}\b/g;
9
+ /** Credentials embedded in a URL: https://user:token@host/... (a git remote with a personal access token). */
10
+ const URL_CREDENTIALS_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi;
11
+ /** `Authorization: Bearer <token>` as it shows up in echoed requests and error dumps. */
12
+ const BEARER_PATTERN = /\b(Bearer\s+)[A-Za-z0-9._~+/=-]{12,}/g;
13
+ export function redactSecrets(text, secrets = []) {
14
+ if (!text)
15
+ return text;
16
+ let out = text;
17
+ for (const secret of secrets) {
18
+ const value = (secret || '').trim();
19
+ // Short values would mask ordinary words; real keys are far longer
20
+ if (value.length >= 8) {
21
+ out = out.split(value).join(MASK);
22
+ }
23
+ }
24
+ return out
25
+ .replace(AM_KEY_PATTERN, `$1${MASK}`)
26
+ .replace(URL_CREDENTIALS_PATTERN, `$1${MASK}@`)
27
+ .replace(BEARER_PATTERN, `$1${MASK}`);
28
+ }
29
+ /** The secrets this process knows about, for exact-match masking. */
30
+ export function knownSecrets(apiKey) {
31
+ return [apiKey, process.env.AGILEMONSTER_API_KEY, process.env.AM_API_KEY].filter((v) => typeof v === 'string' && v.trim() !== '');
32
+ }
33
+ /** Mask every text part of a tool result or resource read, leaving its shape untouched. */
34
+ export function redactResult(result, secrets = []) {
35
+ const fix = (part) => part && typeof part === 'object' && typeof part.text === 'string'
36
+ ? { ...part, text: redactSecrets(part.text, secrets) }
37
+ : part;
38
+ const r = result;
39
+ if (!r || typeof r !== 'object')
40
+ return result;
41
+ if (Array.isArray(r.content))
42
+ return { ...r, content: r.content.map(fix) };
43
+ if (Array.isArray(r.contents))
44
+ return { ...r, contents: r.contents.map(fix) };
45
+ return result;
46
+ }
47
+ /**
48
+ * A git remote as it may leave this machine: `https://user:token@host/...` and `https://token@host/...`
49
+ * lose their credentials; the ssh login name (`git@host:...`) is not a secret and stays.
50
+ */
51
+ export function stripRemoteCredentials(remote) {
52
+ const value = (remote || '').trim();
53
+ if (!value)
54
+ return '';
55
+ return value
56
+ .replace(/^(https?:\/\/)[^/@\s]+@/i, '$1')
57
+ .replace(/^([a-z][a-z0-9+.-]*:\/\/[^/@:\s]+):[^/@\s]+@/i, '$1@');
58
+ }