@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 +50 -10
- package/bin/mcp.js +33 -1
- package/bin/orchestrator.js +1 -0
- package/dist/agent-engine.d.ts +17 -0
- package/dist/agent-engine.js +119 -4
- package/dist/client.d.ts +17 -3
- package/dist/client.js +24 -5
- package/dist/machine.d.ts +25 -0
- package/dist/machine.js +32 -0
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +33 -7
- package/dist/server.d.ts +10 -1
- package/dist/server.js +83 -29
- package/dist/service.d.ts +82 -0
- package/dist/service.js +327 -0
- package/package.json +4 -1
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
|
-
|
|
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
|
-
## ๐
|
|
63
|
+
## ๐ Connect a project folder (`bootstrap`)
|
|
62
64
|
|
|
63
|
-
|
|
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
|
|
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.
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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);
|
package/bin/orchestrator.js
CHANGED
package/dist/agent-engine.d.ts
CHANGED
|
@@ -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;
|
package/dist/agent-engine.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/machine.js
ADDED
|
@@ -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
|
+
}
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -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;
|
package/dist/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
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(' ๐
|
|
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.
|
|
322
|
-
console.log(
|
|
323
|
-
|
|
324
|
-
|
|
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?: {
|