@agilemonster/mcp-server 1.0.25 → 1.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts 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.25";
3
+ export declare const MCP_SERVER_VERSION = "1.0.27";
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.25';
7
+ export const MCP_SERVER_VERSION = '1.0.27';
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) {
@@ -50,6 +50,11 @@ export interface CardDeployState {
50
50
  at: string;
51
51
  attempts: number;
52
52
  last_error?: string | null;
53
+ /**
54
+ * Where the last attempt stopped. 'git' = the change never reached the remote, so a retry only has to push it
55
+ * (or find that a person already did) — it must not add an empty "redeploy" commit.
56
+ */
57
+ failed_stage?: 'git' | 'deploy' | null;
53
58
  /** Set when the agent asked "may I deploy with X?": a plain "yes"/"retry" then confirms X. */
54
59
  pending_confirmation?: {
55
60
  method: string;
@@ -89,6 +94,13 @@ export interface ProjectMemory {
89
94
  method: string;
90
95
  verified: boolean;
91
96
  }>;
97
+ /**
98
+ * Cards that are waiting for the owner to decide what to do with a branch that cannot be pushed
99
+ * (asked BEFORE any work started). Only a reply written after `at` answers the question.
100
+ */
101
+ branch_questions: Record<string, {
102
+ at: string;
103
+ }>;
92
104
  }
93
105
  export declare function memoryFileFor(projectPath: string): string;
94
106
  /**
@@ -118,5 +130,10 @@ export declare function setExplicitProfile(projectPath: string, explicit: {
118
130
  }): ProjectMemory;
119
131
  export declare function setCardState(projectPath: string, cardId: string, state: CardDeployState | null): void;
120
132
  export declare function getCardState(projectPath: string, cardId: string): CardDeployState | null;
133
+ /** Remember (or, with null, forget) that this card is waiting for a decision about its branch. */
134
+ export declare function setBranchQuestion(projectPath: string, cardId: string, at: string | null): void;
135
+ export declare function getBranchQuestion(projectPath: string, cardId: string): {
136
+ at: string;
137
+ } | null;
121
138
  export declare function addPendingVerification(projectPath: string, pending: PendingVerification): void;
122
139
  export declare function removePendingVerification(projectPath: string, cardId: string): void;
@@ -71,6 +71,7 @@ export function loadMemory(projectPath) {
71
71
  pending: [],
72
72
  cards: {},
73
73
  deployed_cards: {},
74
+ branch_questions: {},
74
75
  };
75
76
  saveMemory(memory);
76
77
  return memory;
@@ -86,6 +87,7 @@ export function loadMemory(projectPath) {
86
87
  pending: Array.isArray(stored?.pending) ? stored.pending : [],
87
88
  cards: stored?.cards && typeof stored.cards === 'object' ? stored.cards : {},
88
89
  deployed_cards: stored?.deployed_cards && typeof stored.deployed_cards === 'object' ? stored.deployed_cards : {},
90
+ branch_questions: stored?.branch_questions && typeof stored.branch_questions === 'object' ? stored.branch_questions : {},
89
91
  };
90
92
  }
91
93
  export function saveMemory(memory) {
@@ -110,6 +112,10 @@ export function saveMemory(memory) {
110
112
  .slice(0, deployedIds.length - DEPLOYED_CARDS_LIMIT)
111
113
  .forEach((id) => delete memory.deployed_cards[id]);
112
114
  }
115
+ for (const id of Object.keys(memory.branch_questions)) {
116
+ if (Date.now() - (Date.parse(memory.branch_questions[id].at) || 0) > STALE_CARD_STATE_MS)
117
+ delete memory.branch_questions[id];
118
+ }
113
119
  memory.history = memory.history.slice(-HISTORY_LIMIT);
114
120
  try {
115
121
  writeJsonAtomic(memoryFileFor(memory.project_path), memory);
@@ -176,6 +182,20 @@ export function setCardState(projectPath, cardId, state) {
176
182
  export function getCardState(projectPath, cardId) {
177
183
  return loadMemory(projectPath).cards[cardId] || null;
178
184
  }
185
+ /** Remember (or, with null, forget) that this card is waiting for a decision about its branch. */
186
+ export function setBranchQuestion(projectPath, cardId, at) {
187
+ const memory = loadMemory(projectPath);
188
+ if (at)
189
+ memory.branch_questions[cardId] = { at };
190
+ else if (memory.branch_questions[cardId])
191
+ delete memory.branch_questions[cardId];
192
+ else
193
+ return;
194
+ saveMemory(memory);
195
+ }
196
+ export function getBranchQuestion(projectPath, cardId) {
197
+ return loadMemory(projectPath).branch_questions[cardId] || null;
198
+ }
179
199
  export function addPendingVerification(projectPath, pending) {
180
200
  const memory = loadMemory(projectPath);
181
201
  memory.pending = memory.pending.filter((p) => p.card_id !== pending.card_id);
@@ -1,5 +1,6 @@
1
1
  import { BoundProfile, DeployMethod, DeployProfile } from './deploy-profile.js';
2
2
  import { PendingVerification } from './deploy-memory.js';
3
+ import { PushConflict } from './branch-health.js';
3
4
  /**
4
5
  * Runs a project's deployment and proves it worked.
5
6
  *
@@ -39,6 +40,12 @@ export interface DeployResult {
39
40
  commitHash: string;
40
41
  fullSha: string;
41
42
  branch: string;
43
+ /** The commit reached the remote in this attempt (or was already there). */
44
+ pushed: boolean;
45
+ /** Where a failed attempt stopped: 'git' = before the change reached the remote; 'deploy' = after. */
46
+ stage: 'git' | 'deploy' | null;
47
+ /** A refused push, worked out: what clashes and which ways out exist. */
48
+ conflict?: PushConflict;
42
49
  deployedNote: string;
43
50
  errorDetail: string;
44
51
  errorKind: string | null;
@@ -59,6 +66,8 @@ export interface DeployDirective {
59
66
  method: DeployMethod | null;
60
67
  command: string | null;
61
68
  liveUrl: string | null;
69
+ /** "Ship only this change": leave older unpushed commits out and push just this card's commit. */
70
+ onlyThis?: boolean;
62
71
  }
63
72
  /**
64
73
  * Parse a human reply to a "deployment failed / may I deploy with X?" question.
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { composeArgs, isRunnableCommand, methodLabel, normalizeMethod, pushPathsMatch, requiresConfirmation, resolveDeployProfile, } from './deploy-profile.js';
5
5
  import { loadMemory, recordAttempt } from './deploy-memory.js';
6
+ import { describePushConflict, explainPushConflict } from './branch-health.js';
6
7
  import { ensureUserPath, exec, execShell, log, sleep, which } from './runtime.js';
7
8
  const FAILURE_TABLE = [
8
9
  [
@@ -69,6 +70,7 @@ export function classifyFailure(text) {
69
70
  }
70
71
  return { kind: 'unknown', hint: '', healable: false };
71
72
  }
73
+ const ONLY_THIS_RE = /^(ship |push |deploy )?(only|just) this( one)?( change| card| commit)?$/i;
72
74
  const AFFIRMATIVE_RE = /^(retry|re-?deploy|try again|deploy again|go ahead|fixed|yes|yep|ok|okay|confirm(ed)?|do it)[\s.!]*$/i;
73
75
  /** A directive value only counts as a command when it starts like one — prose after "Deployment:" never runs. */
74
76
  const COMMAND_START_RE = /^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:\.{1,2}\/|\/|~\/|(?:docker|docker-compose|make|npm|pnpm|yarn|npx|bun|bunx|node|deno|python3?|bash|sh|zsh|fly|flyctl|vercel|netlify|wrangler|firebase|gh|git|rsync|scp|ssh|ansible-playbook|kubectl|helm|terraform|cap|bundle|php|composer|gcloud|aws|az|heroku|task|just|cargo|go|dotnet|mvn|gradle|rake|fab|serverless|sls|cdk|pulumi)(?=\s|$))/;
@@ -91,7 +93,12 @@ export function parseDeployDirective(text) {
91
93
  if (directive) {
92
94
  const value = directive[1].replace(/^`+|`+$/g, '').trim();
93
95
  const asMethod = normalizeMethod(value);
94
- if (AFFIRMATIVE_RE.test(value)) {
96
+ if (ONLY_THIS_RE.test(value.replace(/[\s.!]+$/, ''))) {
97
+ out.onlyThis = true;
98
+ out.retry = true;
99
+ matched = true;
100
+ }
101
+ else if (AFFIRMATIVE_RE.test(value)) {
95
102
  // "deploy: yes" confirms; it is not a program called `yes`
96
103
  out.retry = true;
97
104
  matched = true;
@@ -298,12 +305,21 @@ async function commitAndPush(req, profile, pushPolicy) {
298
305
  push = await git(['push', state.remote, state.branch], 90000);
299
306
  }
300
307
  else {
308
+ const unmergedFiles = unmerged.split(/\r?\n/).filter(Boolean);
301
309
  await git(['rebase', '--abort']);
310
+ // The raw Git output is for the log. The card gets what clashes, in words, and the ways out.
311
+ log.warn('deploy.rebase_conflict', { card: req.cardId, project: cwd, branch: state.branch, files: unmergedFiles, output: pull.tail.slice(0, 600) });
312
+ if (unmergedFiles.length === 0)
313
+ return fail(pull, `Could not bring in the newer commits from ${state.remote}/${state.branch}`);
314
+ const conflict = await explainPushConflict(cwd, req.cardId, unmergedFiles);
302
315
  return {
303
316
  ...state,
304
317
  ok: false,
305
318
  errorKind: 'rebase_conflict',
306
- errorDetail: `The remote branch \`${state.branch}\` has new commits that conflict with this change, so it could not be pushed: ${pull.tail}`,
319
+ conflict: conflict || undefined,
320
+ errorDetail: conflict
321
+ ? describePushConflict(conflict).detail
322
+ : `The change is committed on this computer, but the remote branch \`${state.branch}\` has newer commits that clash with it in ${unmergedFiles.slice(0, 3).map((f) => `\`${f}\``).join(', ')}, so it could not be pushed.`,
307
323
  };
308
324
  }
309
325
  }
@@ -959,6 +975,8 @@ export async function runDeployment(req) {
959
975
  commitHash: (git?.fullSha || '').slice(0, 7),
960
976
  fullSha: git?.fullSha || '',
961
977
  branch: git?.branch || '',
978
+ pushed: Boolean(git?.pushed),
979
+ stage: null,
962
980
  deployedNote: '',
963
981
  errorDetail: '',
964
982
  errorKind: null,
@@ -983,7 +1001,14 @@ export async function runDeployment(req) {
983
1001
  const kind = git.errorKind || 'git';
984
1002
  recordAttempt(req.projectPath, { card_id: req.cardId, method: profile.method, command: profile.command, ok: false, kind, error: git.errorDetail, duration_ms: Date.now() - startedAt });
985
1003
  log.warn('deploy.git_failed', { card: req.cardId, project: req.projectPath, kind, error: git.errorDetail });
986
- return result({ errorDetail: git.errorDetail || 'Git failed', errorKind: kind, hint: classifyFailure(git.errorDetail || '').hint || HINTS[kind] || '', healed: git.healed }, git);
1004
+ return result({
1005
+ errorDetail: git.errorDetail || 'Git failed',
1006
+ errorKind: kind,
1007
+ stage: 'git',
1008
+ conflict: git.conflict,
1009
+ hint: git.conflict ? describePushConflict(git.conflict).hint : classifyFailure(git.errorDetail || '').hint || HINTS[kind] || '',
1010
+ healed: git.healed,
1011
+ }, git);
987
1012
  }
988
1013
  let step = await runMethod(req, profile, git, changedFiles);
989
1014
  const healed = [...git.healed, ...step.healed];
@@ -1019,6 +1044,7 @@ export async function runDeployment(req) {
1019
1044
  return result({
1020
1045
  errorDetail: step.errorDetail || 'Deployment failed',
1021
1046
  errorKind: kind,
1047
+ stage: 'deploy',
1022
1048
  hint: HINTS[kind] || classifyFailure(step.errorDetail || '').hint,
1023
1049
  actions: [...git.actions, ...step.actions],
1024
1050
  healed,
@@ -1035,7 +1061,7 @@ export async function runDeployment(req) {
1035
1061
  else if (trusted) {
1036
1062
  const detail = `The deployment ran, but ${profile.liveUrl} returned ${status === 0 ? 'no response' : `HTTP ${status}`}.`;
1037
1063
  recordAttempt(req.projectPath, { card_id: req.cardId, method: profile.method, command: profile.command, ok: false, kind: 'live_check_failed', error: detail, commit: git.fullSha.slice(0, 7), duration_ms: Date.now() - startedAt });
1038
- return result({ errorDetail: detail, errorKind: 'live_check_failed', hint: 'Check the site, then reply "retry".', actions, healed }, git);
1064
+ return result({ errorDetail: detail, errorKind: 'live_check_failed', stage: 'deploy', hint: 'Check the site, then reply "retry".', actions, healed }, git);
1039
1065
  }
1040
1066
  }
1041
1067
  // A human-pinned route is its own proof of intent; anything else only becomes "what worked" once verified
@@ -1097,6 +1123,19 @@ export function describeFailureForHuman(res) {
1097
1123
  `Or reply \`deploy: manual\` if you deploy this project yourself${others.length > 0 ? `, or pick another route I found: ${others.map((a) => `\`deploy: ${a.command || a.method}\``).join(', ')}` : ''}.`,
1098
1124
  ].join('\n');
1099
1125
  }
1126
+ if (res.conflict) {
1127
+ return [res.errorDetail, '', 'You can choose:', res.hint].join('\n');
1128
+ }
1129
+ if (res.stage === 'git') {
1130
+ // The change never reached the remote. Which deploy route was picked is beside the point here:
1131
+ // say what is in the way first, and keep Git's own words for whoever wants them.
1132
+ const text = res.hint
1133
+ ? [`The change is made, but Git stopped before it reached the remote. ${res.hint}`, '', `Technical detail: ${res.errorDetail.replace(/\s+/g, ' ').slice(0, 400)}`]
1134
+ : [res.errorDetail.slice(0, 900)];
1135
+ if (!/\bretry\b/i.test(text.join(' ')))
1136
+ text.push('', 'Reply **retry** once it is fixed: I will not redo the work, only push it and check the deployment.');
1137
+ return text.join('\n');
1138
+ }
1100
1139
  const lines = [`What failed: ${res.errorDetail.slice(0, 900)}`];
1101
1140
  if (res.hint)
1102
1141
  lines.push('', res.hint);
@@ -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,19 @@
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
+ import { PushConflict } from './branch-health.js';
6
+ /**
7
+ * The answers a deploy question accepts, as reply buttons: nobody should have to type `deploy: ./deploy.sh`.
8
+ * A first-time route gets "yes, this way" and "I deploy it myself"; a failure gets "try again".
9
+ */
10
+ export declare function deployReplyOptions(res: {
11
+ errorKind?: string | null;
12
+ method?: string | null;
13
+ command?: string | null;
14
+ stage?: 'git' | 'deploy' | null;
15
+ conflict?: PushConflict;
16
+ }): QuestionOption[];
5
17
  export declare function createServer(client?: AgileMonsterClient): Server<{
6
18
  method: string;
7
19
  params?: {