@agilemonster/mcp-server 1.0.24 โ†’ 1.0.26

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
@@ -14,7 +14,9 @@ Seamlessly connects your Agile Monster boards with your favorite developer AI ag
14
14
 
15
15
  ## โšก Quick Start: Adding to your AI Agent
16
16
 
17
- Generate a **Personal API Key** in Agile Monster ([https://app.agilemonster.com](https://app.agilemonster.com) -> Profile -> API Keys). Your key will look like `am_live_...`.
17
+ The quickest way is the one command in [Connect a project folder](#-connect-a-project-folder-bootstrap) below: it does everything in this section for you.
18
+
19
+ To set a tool up by hand, generate a **Personal API Key** in Agile Monster ([https://app.agilemonster.com](https://app.agilemonster.com) -> Profile -> AI Agents & API Keys -> Generate API key). Your key will look like `am_live_...`.
18
20
 
19
21
  ### 1. Cursor (`.cursor/mcp.json` or Global Settings)
20
22
  ```json
@@ -58,14 +60,19 @@ Add to your Antigravity MCP directory (`~/.gemini/antigravity/mcp/agilemonster`
58
60
 
59
61
  ---
60
62
 
61
- ## ๐Ÿ“ 1-Click Project Initialization (`init`)
63
+ ## ๐Ÿ“ Connect a project folder (`bootstrap`)
62
64
 
63
- To automatically bind your repository to an Agile Monster board and generate operational instructions for your AI agents in one command, run:
65
+ Open a terminal in your project folder and run this once (it needs [Node.js](https://nodejs.org) 18.14 or newer):
64
66
 
65
67
  ```bash
66
- npx @agilemonster/mcp-server init
68
+ npx -y @agilemonster/mcp-server bootstrap
67
69
  ```
68
70
 
71
+ It asks for your API key the first time, lets you pick the board, writes the files below, **installs the
72
+ background runner** (see the next section) and ends by telling you which coding agent on this computer is
73
+ ready. `init` and `setup` are the same command. Add `--board="Board name"` to skip the question, or
74
+ `--no-service` to leave the background runner out.
75
+
69
76
  This generates:
70
77
  1. `.agilemonster.json` โ€” Binds the local repository to the board UUID, list columns, and default agent provider.
71
78
  2. `AGENTS.md` โ€” Codified operational rules for autonomous agents (Cursor, Claude Code, Antigravity, etc.) so they know when to claim tasks, ask clarifying questions, log progress, and submit work for human testing.
@@ -93,18 +100,50 @@ Agents have access to these purpose-built lifecycle tools:
93
100
 
94
101
  ## ๐Ÿง  Who works a card that is queued from the board
95
102
 
96
- A card you assign to an agent in the web app is picked up by the background runner on your machine. The
97
- runner hands it to a **real coding agent running headless in the project folder** โ€” the same agent you use
103
+ A card you assign to an agent in the web app is picked up by the background runner on your machine.
104
+
105
+ ### The background runner
106
+
107
+ `bootstrap` installs the runner as a service that starts with the computer, so cards are worked whether or
108
+ not a coding tool is open. Without it, cards are only picked up while an AI coding tool with this MCP server
109
+ is open in the project folder.
110
+
111
+ ```bash
112
+ npx -y @agilemonster/mcp-server service status # is it installed and running?
113
+ npx -y @agilemonster/mcp-server service install # install or repair it
114
+ npx -y @agilemonster/mcp-server service uninstall # stop and remove it
115
+ ```
116
+
117
+ - **macOS**: a launchd agent (`~/Library/LaunchAgents/com.agilemonster.pulse.plist`). **Linux**: a systemd
118
+ user unit (`agilemonster-runner.service`). **Windows**: not available as a service yet; keep your coding
119
+ tool open, or run `npx -y @agilemonster/mcp-server pulse-daemon` in a terminal.
120
+ - The service runs its own copy of this package from `~/.agilemonster/runner` and keeps it up to date.
121
+ - A runner that was set up by hand is left alone unless you pass `--force`.
122
+ - The board shows the result: next to a waiting card it says whether a computer is connected, whether it is
123
+ there right now, and which agent will do the work.
124
+
125
+ ### The agent that does the work
126
+
127
+ The runner hands a card to a **real coding agent running headless in the project folder** โ€” the same agent you use
98
128
  in the IDE, with the same ability to search the code, open what it finds, run the build and fix what it broke:
99
129
 
100
130
  | Card's agent | Command line agent used | One-time setup |
101
131
  | :--- | :--- | :--- |
102
132
  | Cursor | `cursor-agent -p --force` | `curl https://cursor.com/install -fsS \| bash`, then `cursor-agent login` |
103
- | Claude Code | `claude -p --permission-mode acceptEdits` | `npm install -g @anthropic-ai/claude-code`, then `claude` once to sign in |
133
+ | Claude Code | `claude -p --permission-mode acceptEdits --allowedTools Bash` | `npm install -g @anthropic-ai/claude-code`, then `claude auth login` |
104
134
  | Codex | `codex exec --sandbox workspace-write` | `npm install -g @openai/codex`, then `codex login` |
105
135
 
106
- If the card's own agent has no CLI here, any other installed one is used and the card says which. The agent
107
- gets the card, the requester's replies on it, and three rules: decide open details the way the codebase
136
+ The runner also finds an agent that is not on your `PATH` โ€” for example the Codex CLI that the Codex app and
137
+ its IDE plugin install, already signed in with your ChatGPT account โ€” so there is often nothing to set up.
138
+ If the card's own agent cannot run here, any other ready one stands in and the card says which, and why.
139
+
140
+ To see what this machine will use, and the one step that would fix an agent that is not ready:
141
+
142
+ ```bash
143
+ npx @agilemonster/mcp-server agents
144
+ ```
145
+
146
+ The agent gets the card, the requester's replies on it, and three rules: decide open details the way the codebase
108
147
  already does instead of asking, check your own work, and do not commit โ€” the runner commits and deploys
109
148
  exactly the files the agent changed, after its own build check.
110
149
 
@@ -144,7 +183,8 @@ What the runner guarantees:
144
183
  State lives outside your repositories: `~/.agilemonster/projects/` (per-project deployment history) and
145
184
  `~/.agilemonster/logs/daemon.jsonl` (structured daemon log).
146
185
 
147
- Card reply syntax: `retry` ยท `deploy: github_actions` ยท `deploy: docker_compose` ยท `deploy: manual` ยท `deploy: ./deploy/deploy.sh` ยท `live url: https://example.com`
186
+ When the runner asks a question, the card shows the answers it accepts as buttons, so nobody has to type
187
+ these. They still work as typed replies: `retry` ยท `deploy: github_actions` ยท `deploy: docker_compose` ยท `deploy: manual` ยท `deploy: ./deploy/deploy.sh` ยท `live url: https://example.com`
148
188
 
149
189
  ---
150
190
 
package/bin/mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { runStdioServer, runBackgroundPulseDaemon } from '../dist/server.js';
3
- import { MCP_SERVER_VERSION } from '../dist/client.js';
3
+ import { AgileMonsterClient, MCP_SERVER_VERSION } from '../dist/client.js';
4
4
 
5
5
  const cliCommands = new Set([
6
6
  'bootstrap',
@@ -23,6 +23,38 @@ const firstArg = process.argv[2];
23
23
  if (firstArg === '--version' || firstArg === '-v' || firstArg === 'version') {
24
24
  console.log(MCP_SERVER_VERSION);
25
25
  process.exit(0);
26
+ } else if (firstArg === 'agents' || firstArg === 'doctor') {
27
+ // Which coding agent on this machine works a card queued from the board, and what would fix the others
28
+ const { describeAgents, forgetEngineReadiness } = await import('../dist/agent-engine.js');
29
+ const { ensureUserPath } = await import('../dist/runtime.js');
30
+ await ensureUserPath();
31
+ forgetEngineReadiness();
32
+ console.log(await describeAgents());
33
+ const { describeService } = await import('../dist/service.js');
34
+ console.log(`\n${await describeService()}`);
35
+ process.exit(0);
36
+ } else if (firstArg === 'service') {
37
+ // The background runner that works cards with no coding tool open: install | status | uninstall
38
+ const { installService, uninstallService, describeService } = await import('../dist/service.js');
39
+ const { ensureUserPath } = await import('../dist/runtime.js');
40
+ await ensureUserPath();
41
+ const sub = process.argv[3] || 'status';
42
+ const force = process.argv.includes('--force');
43
+ if (sub === 'install') {
44
+ const key = new AgileMonsterClient().getApiKey();
45
+ // The service is not started inside a project folder, so it reads the key from the home config
46
+ if (key && !AgileMonsterClient.hasGlobalApiKey()) AgileMonsterClient.saveGlobalConfig({ api_key: key });
47
+ const res = await installService({ force, hasApiKey: Boolean(key) });
48
+ console.log(res.message);
49
+ process.exit(res.ok ? 0 : 1);
50
+ } else if (sub === 'uninstall' || sub === 'remove') {
51
+ const res = await uninstallService({ force });
52
+ console.log(res.message);
53
+ process.exit(res.ok ? 0 : 1);
54
+ } else {
55
+ console.log(await describeService());
56
+ process.exit(0);
57
+ }
26
58
  } else if (firstArg === 'pulse-daemon' || firstArg === '--pulse-daemon') {
27
59
  runBackgroundPulseDaemon(20000).catch((err) => {
28
60
  console.error('Fatal Pulse Daemon Error:', err);
@@ -38,6 +38,7 @@ async function main() {
38
38
  ide: ideArg,
39
39
  board: boardArg,
40
40
  provider: providerArg,
41
+ service: !args.includes('--no-service'),
41
42
  });
42
43
  break;
43
44
  }
@@ -35,6 +35,21 @@ export interface ResolvedEngine {
35
35
  label: string;
36
36
  bin: string;
37
37
  }
38
+ /** Forget cached sign-in checks (a developer who just signed in should not wait out the cache; tests). */
39
+ export declare function forgetEngineReadiness(): void;
40
+ export interface EngineStatus {
41
+ id: EngineId;
42
+ label: string;
43
+ bin: string | null;
44
+ /** Null when this agent can take a card now, otherwise what stands in the way, in words for the developer. */
45
+ problem: string | null;
46
+ }
47
+ /** Every agent CLI this runner knows, and whether each can take a card on this machine right now. */
48
+ export declare function engineStatuses(): Promise<EngineStatus[]>;
49
+ /** What `agilemonster agents` prints: which agent works a queued card here, and the one step that would fix the others. */
50
+ export declare function describeAgents(): Promise<string>;
51
+ /** How this machine works cards: with real agents, with the developer's own command, or with agents switched off. */
52
+ export declare function agentMode(): 'agents' | 'builtin' | 'custom';
38
53
  export declare function engineLabelFor(provider: string): string | null;
39
54
  /**
40
55
  * The agent CLIs this machine can run for a card, best first: the card's own agent, then any other one that
@@ -77,6 +92,8 @@ export interface AgentReport {
77
92
  summary: string;
78
93
  verificationSteps: string[];
79
94
  question: string;
95
+ /** Short answers the requester can pick with one tap, when the question has a few clear ones. */
96
+ options: string[];
80
97
  }
81
98
  /** Read the closing JSON object out of the agent's final message. Tolerates code fences and prose around it. */
82
99
  export declare function parseAgentReport(text: string): AgentReport;
@@ -41,7 +41,18 @@ const ENGINES = [
41
41
  label: 'Claude Code',
42
42
  providers: ['claude_code'],
43
43
  bins: ['claude'],
44
+ locations: () => [path.join(os.homedir(), '.local', 'bin', 'claude'), path.join(os.homedir(), '.claude', 'local', 'claude')],
44
45
  installHint: 'npm install -g @anthropic-ai/claude-code',
46
+ ready: async (bin) => {
47
+ if ((process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || '').trim())
48
+ return null;
49
+ const status = await exec(bin, ['auth', 'status'], { timeoutMs: 20000 });
50
+ const info = lastJsonValue(status.stdout);
51
+ // Only a plain "not signed in to Anthropic" is certain. Bedrock, Vertex and older CLIs answer differently: let the run tell.
52
+ return info && typeof info === 'object' && info.loggedIn === false && (!info.apiProvider || info.apiProvider === 'firstParty')
53
+ ? 'it is installed but not signed in โ€” run `claude auth login` once in a terminal'
54
+ : null;
55
+ },
45
56
  args: (opts) => {
46
57
  // The prompt directly follows -p; the list-valued flags come last so they cannot swallow it.
47
58
  const args = ['-p', opts.prompt, '--output-format', 'json', '--permission-mode', 'acceptEdits'];
@@ -71,6 +82,7 @@ const ENGINES = [
71
82
  label: 'Cursor Agent',
72
83
  providers: ['cursor'],
73
84
  bins: ['cursor-agent'],
85
+ locations: () => [path.join(os.homedir(), '.local', 'bin', 'cursor-agent')],
74
86
  installHint: 'curl https://cursor.com/install -fsS | bash',
75
87
  ready: async (bin) => {
76
88
  if ((process.env.CURSOR_API_KEY || '').trim())
@@ -106,8 +118,28 @@ const ENGINES = [
106
118
  label: 'Codex',
107
119
  providers: ['codex'],
108
120
  bins: ['codex'],
121
+ // The Codex app and its IDE plugin ship the same CLI, already signed in with the developer's ChatGPT account
122
+ locations: () => [
123
+ path.join(os.homedir(), '.codex', 'plugins', '.plugin-appserver', 'codex-cli', 'bin', 'codex'),
124
+ '/Applications/Codex.app/Contents/Resources/codex',
125
+ ],
109
126
  installHint: 'npm install -g @openai/codex',
110
- args: (opts, scratch) => ['exec', '--sandbox', 'workspace-write', '--skip-git-repo-check', '-o', scratch.lastMessageFile, opts.prompt],
127
+ ready: async (bin) => {
128
+ if ((process.env.CODEX_API_KEY || '').trim())
129
+ return null;
130
+ const status = await exec(bin, ['login', 'status'], { timeoutMs: 20000 });
131
+ return /not logged in/i.test(`${status.stdout}\n${status.stderr}`)
132
+ ? 'it is installed but not signed in โ€” run `codex login` once in a terminal'
133
+ : null;
134
+ },
135
+ args: (opts, scratch) => {
136
+ // workspace-write keeps its edits inside the project; network stays on so it can install and build like the IDE does
137
+ const args = ['exec', '--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true', '--skip-git-repo-check'];
138
+ if (opts.model && /^gpt-/i.test(opts.model))
139
+ args.push('-m', opts.model);
140
+ args.push('-o', scratch.lastMessageFile, opts.prompt);
141
+ return args;
142
+ },
111
143
  parse: (res, scratch) => {
112
144
  let text = '';
113
145
  try {
@@ -124,6 +156,77 @@ function customCommand() {
124
156
  }
125
157
  const READY_TTL_MS = 2 * 60 * 1000;
126
158
  const readyCache = new Map();
159
+ /** Forget cached sign-in checks (a developer who just signed in should not wait out the cache; tests). */
160
+ export function forgetEngineReadiness() {
161
+ readyCache.clear();
162
+ }
163
+ function isExecutableFile(file) {
164
+ try {
165
+ fs.accessSync(file, fs.constants.X_OK);
166
+ return fs.statSync(file).isFile();
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
172
+ /** The engine's CLI: on PATH, or where its installer, IDE plugin or desktop app keeps it. */
173
+ function findBin(spec) {
174
+ for (const bin of spec.bins) {
175
+ const onPath = which(bin);
176
+ if (onPath)
177
+ return onPath;
178
+ }
179
+ for (const candidate of spec.locations ? spec.locations() : []) {
180
+ if (isExecutableFile(candidate))
181
+ return candidate;
182
+ }
183
+ return null;
184
+ }
185
+ /** Every agent CLI this runner knows, and whether each can take a card on this machine right now. */
186
+ export async function engineStatuses() {
187
+ const out = [];
188
+ for (const spec of ENGINES) {
189
+ const bin = findBin(spec);
190
+ out.push({
191
+ id: spec.id,
192
+ label: spec.label,
193
+ bin,
194
+ problem: bin ? await readiness(spec, bin) : `it is not installed (\`${spec.installHint}\`)`,
195
+ });
196
+ }
197
+ return out;
198
+ }
199
+ /** What `agilemonster agents` prints: which agent works a queued card here, and the one step that would fix the others. */
200
+ export async function describeAgents() {
201
+ const pinned = (process.env.AGILEMONSTER_AGENT_ENGINE || '').trim().toLowerCase();
202
+ const lines = ['Coding agents on this machine that can work a card queued from the board:', ''];
203
+ if (customCommand())
204
+ lines.push(` โœ“ Your own command AGILEMONSTER_AGENT_COMMAND=${customCommand()}`, '');
205
+ const statuses = await engineStatuses();
206
+ for (const s of statuses) {
207
+ lines.push(` ${s.problem ? 'โœ—' : 'โœ“'} ${s.label.padEnd(14)} ${s.problem ? s.problem : s.bin}`);
208
+ }
209
+ lines.push('');
210
+ const usable = statuses.filter((s) => !s.problem);
211
+ if (['builtin', 'built-in', 'off', 'none', '0', 'false'].includes(pinned)) {
212
+ lines.push('AGILEMONSTER_AGENT_ENGINE switches agents off: every card runs on the built-in engine (a few files, one pass).');
213
+ }
214
+ else if (customCommand()) {
215
+ lines.push('Every card you created is handed to your own command.');
216
+ }
217
+ else if (usable.length === 0) {
218
+ lines.push('None is ready, so cards run on the built-in engine, which edits a few files in one pass and cannot explore or build.');
219
+ lines.push('Fix any one line above and the next card is worked by a real agent.');
220
+ }
221
+ else {
222
+ for (const s of statuses) {
223
+ const worker = s.problem ? usable[0] : s;
224
+ lines.push(`A card assigned to ${s.label.replace(/ Agent$/, '')} is worked by ${worker.label}${s.problem ? ` (standing in)` : ''}.`);
225
+ }
226
+ lines.push('Cards a teammate created always use the built-in engine: an agent with a shell only works cards you wrote.');
227
+ }
228
+ return lines.join('\n');
229
+ }
127
230
  async function readiness(spec, bin) {
128
231
  if (!spec.ready)
129
232
  return null;
@@ -140,6 +243,15 @@ async function readiness(spec, bin) {
140
243
  readyCache.set(bin, { at: Date.now(), reason });
141
244
  return reason;
142
245
  }
246
+ /** How this machine works cards: with real agents, with the developer's own command, or with agents switched off. */
247
+ export function agentMode() {
248
+ const pinned = (process.env.AGILEMONSTER_AGENT_ENGINE || '').trim().toLowerCase();
249
+ if (['builtin', 'built-in', 'off', 'none', '0', 'false'].includes(pinned))
250
+ return 'builtin';
251
+ if (customCommand() && (!pinned || pinned === 'auto' || pinned === 'custom'))
252
+ return 'custom';
253
+ return 'agents';
254
+ }
143
255
  export function engineLabelFor(provider) {
144
256
  const spec = ENGINES.find((e) => e.providers.includes((provider || '').toLowerCase()));
145
257
  return spec ? spec.label : null;
@@ -167,7 +279,7 @@ export async function resolveEngines(provider) {
167
279
  const notes = [];
168
280
  for (const spec of ordered) {
169
281
  const isNative = native.includes(spec);
170
- const bin = spec.bins.map((b) => which(b)).find(Boolean) || null;
282
+ const bin = findBin(spec);
171
283
  if (!bin) {
172
284
  if (isNative)
173
285
  notes.push(`${spec.label}'s command line agent is not installed on this machine (\`${spec.installHint}\`)`);
@@ -265,7 +377,7 @@ export function buildAgentPrompt(brief) {
265
377
  lines.push(`- An earlier attempt at this card left uncommitted changes in ${brief.earlierFiles.slice(0, 40).map((f) => `\`${f}\``).join(', ')}. They are yours to keep, fix or replace.` +
266
378
  (brief.earlierSummary ? ` It reported: ${brief.earlierSummary.trim().slice(0, 600)}` : ''));
267
379
  }
268
- lines.push('', '## When you are done', 'End your reply with one JSON object on its own, and nothing after it:', '{"status":"done","summary":"2-4 plain sentences for the person reviewing the card: what changed and where they will see it","verification_steps":["a step they can do in the running app","..."]}', 'Only if the work is impossible or unsafe without something that is not in the repository:', '{"status":"blocked","question":"the one specific thing you need, and what you already tried"}');
380
+ lines.push('', '## When you are done', 'End your reply with one JSON object on its own, and nothing after it:', '{"status":"done","summary":"2-4 plain sentences for the person reviewing the card: what changed and where they will see it","verification_steps":["a step they can do in the running app","..."]}', 'Only if the work is impossible or unsafe without something that is not in the repository:', '{"status":"blocked","question":"the one specific thing you need, and what you already tried","options":["up to four short answers the requester can pick with one tap, if the question has clear ones"]}');
269
381
  return lines.join('\n');
270
382
  }
271
383
  /** Read the closing JSON object out of the agent's final message. Tolerates code fences and prose around it. */
@@ -292,13 +404,16 @@ export function parseAgentReport(text) {
292
404
  summary: String(obj.summary || '').trim(),
293
405
  verificationSteps: steps,
294
406
  question: String(obj.question || '').trim(),
407
+ options: Array.isArray(obj.options)
408
+ ? obj.options.map((o) => String(o).trim()).filter((o) => o.length > 0 && o.length <= 80).slice(0, 4)
409
+ : [],
295
410
  };
296
411
  }
297
412
  catch { }
298
413
  end = clean.lastIndexOf('}', end - 1);
299
414
  }
300
415
  }
301
- return { status: 'unknown', summary: '', verificationSteps: [], question: '' };
416
+ return { status: 'unknown', summary: '', verificationSteps: [], question: '', options: [] };
302
417
  }
303
418
  /** The agent's prose without the closing JSON object: what to show a human when the report is missing a field. */
304
419
  export function proseOf(text, maxChars = 900) {
package/dist/client.d.ts CHANGED
@@ -1,5 +1,10 @@
1
+ import type { MachineReport } from './machine.js';
1
2
  import { ActiveClaim, AgileMonsterProjectConfig, AutonomousStepResponse, CardComment, DeployReport, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
2
- export declare const MCP_SERVER_VERSION = "1.0.24";
3
+ export declare const MCP_SERVER_VERSION = "1.0.26";
4
+ export type QuestionOption = string | {
5
+ label: string;
6
+ reply: string;
7
+ };
3
8
  /** An API call the server answered with an error status. `status` lets callers tell "refused" from "broken". */
4
9
  export declare class ApiError extends Error {
5
10
  readonly status: number;
@@ -22,6 +27,8 @@ export declare class AgileMonsterClient {
22
27
  setApiKey(key: string): void;
23
28
  loadProjectConfig(): AgileMonsterProjectConfig | null;
24
29
  saveProjectConfig(config: AgileMonsterProjectConfig, dir?: string): string;
30
+ /** True when ~/.agilemonster/config.json already holds a key (what a background service reads). */
31
+ static hasGlobalApiKey(): boolean;
25
32
  static saveGlobalConfig(data: {
26
33
  api_key?: string;
27
34
  }): string;
@@ -38,7 +45,8 @@ export declare class AgileMonsterClient {
38
45
  agents: BoardAgentConfig[];
39
46
  }>;
40
47
  pulse(options: {
41
- boardId: string;
48
+ /** Omitted by a background service that was not started inside a project folder. */
49
+ boardId?: string;
42
50
  provider?: string;
43
51
  projectName?: string;
44
52
  projectPath?: string;
@@ -50,6 +58,8 @@ export declare class AgileMonsterClient {
50
58
  installedAgents?: string[];
51
59
  /** Cards this process holds: the server extends their claim (heartbeat). */
52
60
  activeClaims?: ActiveClaim[];
61
+ /** This computer and the agents on it that can take a card (shown on the board next to a waiting card). */
62
+ machine?: MachineReport;
53
63
  }): Promise<PulseResponse>;
54
64
  /** Extend the claims this process holds. Used while a long job keeps the pulse loop busy. */
55
65
  heartbeat(activeClaims: ActiveClaim[]): Promise<{
@@ -93,7 +103,11 @@ export declare class AgileMonsterClient {
93
103
  cards: Card[];
94
104
  }>;
95
105
  claimTask(cardId: string, provider: string, runRef?: string, expectedBoardId?: string): Promise<Card>;
96
- askQuestion(cardId: string, provider: string, question: string): Promise<Card>;
106
+ /**
107
+ * `options` become reply buttons on the card: a plain string is both the button and the reply, a
108
+ * {label, reply} pair shows one thing and sends another ("Yes, deploy this way" -> "deploy: ./deploy.sh").
109
+ */
110
+ askQuestion(cardId: string, provider: string, question: string, options?: QuestionOption[]): Promise<Card>;
97
111
  addProgress(cardId: string, provider: string, update: string, tokensUsed?: number, costUsd?: number): Promise<Card>;
98
112
  recordSpend(cardId: string, provider: string, tokens: number, costUsd: number, note?: string): Promise<Card>;
99
113
  setBudget(cardId: string, maxBudgetUsd?: number | null, maxTokens?: number | null): Promise<Card>;
package/dist/client.js CHANGED
@@ -4,7 +4,7 @@ import { execSync } from 'node:child_process';
4
4
  import { resolveDeployProfile } from './deploy-profile.js';
5
5
  import { loadMemory } from './deploy-memory.js';
6
6
  import { stripRemoteCredentials } from './redact.js';
7
- export const MCP_SERVER_VERSION = '1.0.24';
7
+ export const MCP_SERVER_VERSION = '1.0.26';
8
8
  /** An API call the server answered with an error status. `status` lets callers tell "refused" from "broken". */
9
9
  export class ApiError extends Error {
10
10
  status;
@@ -111,6 +111,19 @@ export class AgileMonsterClient {
111
111
  this.cachedConfig = config;
112
112
  return configPath;
113
113
  }
114
+ /** True when ~/.agilemonster/config.json already holds a key (what a background service reads). */
115
+ static hasGlobalApiKey() {
116
+ try {
117
+ const homedir = process.env.HOME || process.env.USERPROFILE || '';
118
+ if (!homedir)
119
+ return false;
120
+ const parsed = JSON.parse(fs.readFileSync(path.join(homedir, '.agilemonster', 'config.json'), 'utf8'));
121
+ return typeof parsed?.api_key === 'string' && parsed.api_key.trim() !== '';
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
114
127
  static saveGlobalConfig(data) {
115
128
  const homedir = process.env.HOME || process.env.USERPROFILE || '';
116
129
  if (!homedir)
@@ -216,6 +229,7 @@ export class AgileMonsterClient {
216
229
  discovered_models: options.discoveredModels,
217
230
  installed_agents: installed,
218
231
  active_claims: options.activeClaims && options.activeClaims.length > 0 ? options.activeClaims : undefined,
232
+ machine: options.machine,
219
233
  }),
220
234
  });
221
235
  }
@@ -453,11 +467,12 @@ export class AgileMonsterClient {
453
467
  */
454
468
  discoverLocalProjects() {
455
469
  const roots = new Set();
470
+ const homedir = process.env.HOME || process.env.USERPROFILE || '';
456
471
  const parentDir = path.dirname(this.cwd);
457
- if (parentDir && parentDir !== '/' && parentDir !== this.cwd) {
472
+ // A background service runs from the home folder: its parent holds other people's homes, not projects
473
+ if (parentDir && parentDir !== '/' && parentDir !== this.cwd && path.resolve(this.cwd) !== path.resolve(homedir || '/')) {
458
474
  roots.add(parentDir);
459
475
  }
460
- const homedir = process.env.HOME || process.env.USERPROFILE || '';
461
476
  if (homedir) {
462
477
  for (const sub of ['dev', 'Projects', 'Code', 'repos', 'Developer']) {
463
478
  const candidate = path.join(homedir, sub);
@@ -555,10 +570,14 @@ export class AgileMonsterClient {
555
570
  body: JSON.stringify({ provider, run_ref: runRef, board_id: boardId }),
556
571
  });
557
572
  }
558
- async askQuestion(cardId, provider, question) {
573
+ /**
574
+ * `options` become reply buttons on the card: a plain string is both the button and the reply, a
575
+ * {label, reply} pair shows one thing and sends another ("Yes, deploy this way" -> "deploy: ./deploy.sh").
576
+ */
577
+ async askQuestion(cardId, provider, question, options) {
559
578
  return this.request(`/api/agent/cards/${cardId}/ask-question`, {
560
579
  method: 'POST',
561
- body: JSON.stringify({ provider, question }),
580
+ body: JSON.stringify({ provider, question, options: options && options.length > 0 ? options.slice(0, 4) : undefined }),
562
581
  });
563
582
  }
564
583
  async addProgress(cardId, provider, update, tokensUsed, costUsd) {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What this computer tells the board with every pulse: which machine it is, whether the runner is the
3
+ * background service or a coding tool that happens to be open, and which agents on it can take a card.
4
+ * The board turns that into what a person reads next to a waiting card ("Codex on Studio Mac is ready",
5
+ * "Cursor is not signed in").
6
+ */
7
+ export interface MachineReport {
8
+ id: string;
9
+ name: string;
10
+ platform: string;
11
+ runner: 'service' | 'ide';
12
+ mode: 'agents' | 'builtin' | 'custom';
13
+ agents: Array<{
14
+ id: string;
15
+ label: string;
16
+ ready: boolean;
17
+ problem: string | null;
18
+ }>;
19
+ }
20
+ /** A stable id for this computer, made once and kept in ~/.agilemonster/machine.json. */
21
+ export declare function machineIdentity(): {
22
+ id: string;
23
+ name: string;
24
+ };
25
+ export declare function machineReport(role: 'daemon' | 'stdio'): Promise<MachineReport>;
@@ -0,0 +1,32 @@
1
+ import crypto from 'node:crypto';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { agentMode, engineStatuses } from './agent-engine.js';
5
+ import { agileMonsterHome, readJson, writeJsonAtomic } from './runtime.js';
6
+ /** A stable id for this computer, made once and kept in ~/.agilemonster/machine.json. */
7
+ export function machineIdentity() {
8
+ const file = path.join(agileMonsterHome(), 'machine.json');
9
+ const saved = readJson(file);
10
+ let id = typeof saved?.id === 'string' && /^[A-Za-z0-9_-]{8,64}$/.test(saved.id) ? saved.id : '';
11
+ if (!id) {
12
+ id = crypto.randomUUID();
13
+ try {
14
+ writeJsonAtomic(file, { id });
15
+ }
16
+ catch { }
17
+ }
18
+ const name = (os.hostname() || 'computer').replace(/\.(local|lan|home|localdomain)$/i, '').slice(0, 120);
19
+ return { id, name };
20
+ }
21
+ export async function machineReport(role) {
22
+ const { id, name } = machineIdentity();
23
+ const agents = await engineStatuses();
24
+ return {
25
+ id,
26
+ name,
27
+ platform: process.platform,
28
+ runner: role === 'daemon' ? 'service' : 'ide',
29
+ mode: agentMode(),
30
+ agents: agents.map((a) => ({ id: a.id, label: a.label, ready: !a.problem, problem: a.problem })),
31
+ };
32
+ }
@@ -21,6 +21,8 @@ export declare class AgileMonsterOrchestrator {
21
21
  provider?: string;
22
22
  ide?: string;
23
23
  board?: string;
24
+ /** False skips installing the background runner (`--no-service`). */
25
+ service?: boolean;
24
26
  }): Promise<void>;
25
27
  bootstrapInteractive(options?: {
26
28
  provider?: string;
@@ -6,6 +6,9 @@ import { AGENT_GUIDELINES_MARKDOWN } from './guidelines.js';
6
6
  import { configureIdeTargets } from './ide-config.js';
7
7
  import { ensureLocalGitExcludes } from './deploy-runner.js';
8
8
  import { stripRemoteCredentials } from './redact.js';
9
+ import { describeAgents } from './agent-engine.js';
10
+ import { ensureUserPath } from './runtime.js';
11
+ import { installService } from './service.js';
9
12
  export class AgileMonsterOrchestrator {
10
13
  client;
11
14
  constructor(client = new AgileMonsterClient()) {
@@ -313,15 +316,38 @@ export class AgileMonsterOrchestrator {
313
316
  // Configure IDEs (Cursor, Claude Code, Windsurf, Antigravity, VS Code)
314
317
  this.configureIdes(apiKey, options?.ide || options?.provider);
315
318
  this.ensureGitignore();
316
- console.log('\n๐ŸŽ‰ Project successfully connected to Agile Monster!');
319
+ // The background runner: without it cards are only worked while a coding tool is open in this folder
320
+ let background = false;
321
+ if (options?.service !== false && process.env.AGILEMONSTER_NO_SERVICE !== '1') {
322
+ console.log('\nโš™๏ธ Setting up the background runner, so cards are worked with no coding tool open...');
323
+ try {
324
+ await ensureUserPath();
325
+ // The service is not started inside a project folder, so it reads the key from the home config
326
+ if (!AgileMonsterClient.hasGlobalApiKey())
327
+ AgileMonsterClient.saveGlobalConfig({ api_key: apiKey });
328
+ const svc = await installService({ hasApiKey: true });
329
+ background = svc.outcome === 'installed' || svc.outcome === 'kept';
330
+ console.log(`${svc.ok ? 'โœ…' : 'โš ๏ธ '} ${svc.message}`);
331
+ }
332
+ catch (err) {
333
+ console.log(`โš ๏ธ The background runner could not be set up: ${err instanceof Error ? err.message : String(err)}`);
334
+ }
335
+ }
336
+ // Which coding agent will actually do the work, and the one step that fixes one that is not ready
337
+ try {
338
+ console.log(`\n${await describeAgents()}`);
339
+ }
340
+ catch { }
341
+ console.log('\n๐ŸŽ‰ This folder is connected to Agile Monster.');
317
342
  console.log('\n======================================================');
318
- console.log(' ๐Ÿš€ HOW TO USE IT NOW ');
343
+ console.log(' ๐Ÿš€ WHAT HAPPENS NOW ');
319
344
  console.log('======================================================');
320
- console.log(`1. Open board "${targetBoard.name}" on https://app.agilemonster.com`);
321
- console.log(`2. Open any card and assign Worker to "${shortTag ? `Cursor-${shortTag}` : `Cursor: ${targetBoard.name}`}" (or Claude Code / Antigravity).`);
322
- console.log('3. In your AI IDE chat inside this project, simply say:');
323
- console.log(' "Check my Agile Monster tasks and start the next queued card."');
324
- console.log(' Or connect another project folder (e.g. Planogram.ai in Cursor) anytime by running:');
345
+ console.log(`1. Open the board "${targetBoard.name}" on https://app.agilemonster.com`);
346
+ console.log(`2. Add a card that says what you want, open it, and under "Assign to AI Agent or Team Member" choose "${shortTag ? `Cursor-${shortTag}` : `Cursor: ${targetBoard.name}`}" (or another agent).`);
347
+ console.log(background
348
+ ? '3. That is all. The card is picked up within about 20 seconds, worked in this folder, and comes back to you for review.'
349
+ : '3. Keep your AI coding tool open in this folder: the card is picked up within about 20 seconds while it is open, and comes back to you for review.');
350
+ console.log('\nTo connect another project folder, run the same command there:');
325
351
  console.log(' cd /path/to/other-project && npx -y @agilemonster/mcp-server bootstrap\n');
326
352
  }
327
353
  async bootstrapInteractive(options) {
package/dist/server.d.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { AgileMonsterClient } from './client.js';
2
+ import { AgileMonsterClient, QuestionOption } from './client.js';
3
3
  import { ActiveClaim, Card } from './types.js';
4
4
  import { BoundProfile } from './deploy-profile.js';
5
+ /**
6
+ * The answers a deploy question accepts, as reply buttons: nobody should have to type `deploy: ./deploy.sh`.
7
+ * A first-time route gets "yes, this way" and "I deploy it myself"; a failure gets "try again".
8
+ */
9
+ export declare function deployReplyOptions(res: {
10
+ errorKind?: string | null;
11
+ method?: string | null;
12
+ command?: string | null;
13
+ }): QuestionOption[];
5
14
  export declare function createServer(client?: AgileMonsterClient): Server<{
6
15
  method: string;
7
16
  params?: {