@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/dist/server.js CHANGED
@@ -8,13 +8,30 @@ import { spawn, execSync, execFileSync } from 'node:child_process';
8
8
  import { isUtf8 } from 'node:buffer';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { AgileMonsterClient, MCP_SERVER_VERSION, isClaimConflict } from './client.js';
11
+ import { machineReport } from './machine.js';
12
+ import { managedRunnerDir, updateArgs } from './service.js';
11
13
  import { AGENT_GUIDELINES_MARKDOWN } from './guidelines.js';
12
14
  import { knownSecrets, redactResult } from './redact.js';
13
15
  import { describeProfile, methodLabel, normalizeMethod, resolveDeployProfile, trustBoundRoute, } from './deploy-profile.js';
14
16
  import { addPendingVerification, getCardState, loadMemory, recordAttempt, removePendingVerification, setCardState, setExplicitProfile, } from './deploy-memory.js';
15
17
  import { checkCiOnce, describeFailureForHuman, ensureLocalGitExcludes, parseDeployDirective, runDeployment, } from './deploy-runner.js';
16
- import { baselineFor, buildAgentPrompt, changesSince, clearBaseline, engineLabelFor, markNewFilesForDiff, parseAgentReport, proseOf, resolveEngines, restoreWorkingTree, runEngine, } from './agent-engine.js';
18
+ import { baselineFor, buildAgentPrompt, changesSince, clearBaseline, engineLabelFor, engineStatuses, markNewFilesForDiff, parseAgentReport, proseOf, resolveEngines, restoreWorkingTree, runEngine, } from './agent-engine.js';
17
19
  import { addWellKnownToolDirs, agileMonsterHome, configureLogger, ensureUserPath, errorMessage, exec, log, readJson, which, writeJsonAtomic, } from './runtime.js';
20
+ /**
21
+ * The answers a deploy question accepts, as reply buttons: nobody should have to type `deploy: ./deploy.sh`.
22
+ * A first-time route gets "yes, this way" and "I deploy it myself"; a failure gets "try again".
23
+ */
24
+ export function deployReplyOptions(res) {
25
+ if (res.errorKind === 'needs_confirmation') {
26
+ const route = (res.command || res.method || '').trim();
27
+ const options = [];
28
+ if (route)
29
+ options.push({ label: 'Yes, deploy this way', reply: `deploy: ${route}` });
30
+ options.push({ label: 'I deploy it myself', reply: 'deploy: manual' });
31
+ return options;
32
+ }
33
+ return [{ label: 'Try again', reply: 'retry' }];
34
+ }
18
35
  export function createServer(client = new AgileMonsterClient()) {
19
36
  const server = new Server({
20
37
  name: 'agilemonster-mcp-server',
@@ -263,6 +280,11 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
263
280
  type: 'string',
264
281
  description: 'The question for the user.',
265
282
  },
283
+ options: {
284
+ type: 'array',
285
+ items: { type: 'string' },
286
+ description: 'Optional: up to four short answers the user can pick with one tap. The card shows them as buttons, and the user can still type a different answer.',
287
+ },
266
288
  },
267
289
  required: ['card_id', 'provider', 'question'],
268
290
  },
@@ -640,7 +662,10 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
640
662
  const cardId = args.card_id;
641
663
  const provider = args.provider || projectConfig?.provider || 'antigravity';
642
664
  const question = args.question;
643
- const card = await client.askQuestion(cardId, provider, question);
665
+ const answers = Array.isArray(args.options)
666
+ ? args.options.map((o) => String(o).trim()).filter((o) => o.length > 0 && o.length <= 80).slice(0, 4)
667
+ : [];
668
+ const card = await client.askQuestion(cardId, provider, question, answers);
644
669
  releaseClaim(cardId);
645
670
  return {
646
671
  content: [
@@ -1015,11 +1040,16 @@ function triggerBackgroundSelfUpdate(targetVersion) {
1015
1040
  catch { }
1016
1041
  }
1017
1042
  }
1018
- // 2. Install @agilemonster/mcp-server@latest into global/npx cache, then exit once idle so launchd respawns the daemon
1019
- const child = spawn('sh', ['-c', 'npm install -g @agilemonster/mcp-server@latest >/dev/null 2>&1 || npx -y @agilemonster/mcp-server@latest --version >/dev/null 2>&1'], {
1020
- stdio: 'ignore',
1021
- detached: true,
1022
- });
1043
+ // 2. Fetch the new version, then exit once idle so launchd/systemd starts it. The background service
1044
+ // updates its own copy in place; a runner started any other way refreshes the global install / npx cache.
1045
+ const ownDir = managedRunnerDir(fileURLToPath(import.meta.url));
1046
+ const npm = which('npm');
1047
+ const child = ownDir && npm
1048
+ ? spawn(npm, updateArgs(ownDir), { stdio: 'ignore', detached: true, cwd: ownDir })
1049
+ : spawn('sh', ['-c', 'npm install -g @agilemonster/mcp-server@latest >/dev/null 2>&1 || npx -y @agilemonster/mcp-server@latest --version >/dev/null 2>&1'], {
1050
+ stdio: 'ignore',
1051
+ detached: true,
1052
+ });
1023
1053
  child.on('exit', () => {
1024
1054
  appendIdeActivityLog(cwd, {
1025
1055
  cardId: 'system-update',
@@ -1579,7 +1609,7 @@ async function runRedeployOnlyCard(client, card, projectPath, provider, workerLa
1579
1609
  pending_confirmation: res.errorKind === 'needs_confirmation' ? { method: res.method, command: res.command } : null,
1580
1610
  }));
1581
1611
  removeTaskLock(lockFile);
1582
- await client.askQuestion(card.id, provider, res.errorKind === 'needs_confirmation' ? describeFailureForHuman(res) : `The redeploy did not go through.\n\n${describeFailureForHuman(res)}`);
1612
+ await client.askQuestion(card.id, provider, res.errorKind === 'needs_confirmation' ? describeFailureForHuman(res) : `The redeploy did not go through.\n\n${describeFailureForHuman(res)}`, deployReplyOptions(res));
1583
1613
  return false;
1584
1614
  }
1585
1615
  if (res.pending && res.pendingVerification)
@@ -1683,7 +1713,7 @@ async function resumeFailedDeployment(client, card, projectPath, provider, worke
1683
1713
  // Nothing is ever confirmed on the human's behalf.
1684
1714
  removeTaskLock(lockFile);
1685
1715
  setCardState(projectPath, card.id, { ...state, at: new Date().toISOString() });
1686
- await client.askQuestion(card.id, provider, `I still need an answer before I deploy this project for the first time. Reply \`deploy: ${pendingRoute.command || pendingRoute.method}\` to confirm, \`deploy: manual\` if you deploy it yourself, or tell me the right route.`);
1716
+ await client.askQuestion(card.id, provider, `I still need an answer before I deploy this project for the first time. Reply \`deploy: ${pendingRoute.command || pendingRoute.method}\` to confirm, \`deploy: manual\` if you deploy it yourself, or tell me the right route.`, deployReplyOptions({ errorKind: 'needs_confirmation', method: pendingRoute.method, command: pendingRoute.command }));
1687
1717
  return 'failed';
1688
1718
  }
1689
1719
  // No fresh reply at all: a re-queue of a deploy-failed card means "try the same deployment again".
@@ -1736,7 +1766,7 @@ async function resumeFailedDeployment(client, card, projectPath, provider, worke
1736
1766
  });
1737
1767
  await client.askQuestion(card.id, provider, res.errorKind === 'needs_confirmation'
1738
1768
  ? describeFailureForHuman(res)
1739
- : `The deployment still did not go through, so this card stays in progress.\n\n${describeFailureForHuman(res)}`);
1769
+ : `The deployment still did not go through, so this card stays in progress.\n\n${describeFailureForHuman(res)}`, deployReplyOptions(res));
1740
1770
  return 'failed';
1741
1771
  }
1742
1772
  if (res.pending && res.pendingVerification) {
@@ -1878,8 +1908,10 @@ async function produceChangeWithAgent(run) {
1878
1908
  const { client, card, projectPath, provider, workerLabel, lockFile, carryOverFiles } = run;
1879
1909
  const { engines, notes } = await resolveEngines(provider);
1880
1910
  run.engineNotes.push(...notes);
1881
- if (engines.length === 0)
1911
+ if (engines.length === 0) {
1912
+ log.info('agent.unavailable', { card: card.id, provider, notes });
1882
1913
  return 'unavailable';
1914
+ }
1883
1915
  // The agent gets a shell. Like a deploy command, what runs on this machine is for its owner to decide:
1884
1916
  // only cards its owner wrote are handed over (and only the owner's replies were kept in the conversation).
1885
1917
  if (!run.ownerId || !card.created_by || card.created_by !== run.ownerId) {
@@ -1902,7 +1934,7 @@ async function produceChangeWithAgent(run) {
1902
1934
  return files.length > 0;
1903
1935
  };
1904
1936
  run.rollback.fn = async () => revert((await changesSince(projectPath, baseline)).changed);
1905
- const backToHuman = async (question, summary, files) => {
1937
+ const backToHuman = async (question, summary, files, options = []) => {
1906
1938
  run.rollback.fn = null;
1907
1939
  removeTaskLock(lockFile);
1908
1940
  appendIdeActivityLog(projectPath, {
@@ -1914,9 +1946,10 @@ async function produceChangeWithAgent(run) {
1914
1946
  summary,
1915
1947
  files,
1916
1948
  });
1917
- await client.askQuestion(card.id, provider, question);
1949
+ await client.askQuestion(card.id, provider, question, options);
1918
1950
  return 'handled';
1919
1951
  };
1952
+ const TRY_AGAIN = [{ label: 'Try again', reply: 'Please try again.' }];
1920
1953
  const prompt = buildAgentPrompt({
1921
1954
  title: card.title || '',
1922
1955
  description: card.description || '',
@@ -1931,8 +1964,14 @@ async function produceChangeWithAgent(run) {
1931
1964
  // 1. Run the first agent that actually starts. One that is logged out or crashes on launch touches nothing.
1932
1965
  let engine = engines[0];
1933
1966
  let result = null;
1967
+ const nativeLabel = engineLabelFor(provider);
1934
1968
  for (const candidate of engines) {
1935
- await client.addProgress(card.id, provider, `Autonomous background runner claimed task in \`${projectPath}\` and moved card to **In Progress**. **${candidate.label}** is working on it in the project folder — reading, searching, editing and building, as it would in the IDE.`);
1969
+ // A card assigned to Cursor that Codex ends up working must say so, with the one step that fixes it
1970
+ const passedOver = nativeLabel && candidate.label !== nativeLabel ? run.engineNotes.filter((n) => n.startsWith(nativeLabel)) : [];
1971
+ await client.addProgress(card.id, provider, `Autonomous background runner claimed task in \`${projectPath}\` and moved card to **In Progress**. **${candidate.label}** is working on it in the project folder — reading, searching, editing and building, as it would in the IDE.` +
1972
+ (passedOver.length > 0
1973
+ ? `\n\n${candidate.label} is standing in for ${nativeLabel}, which cannot run on this machine yet:\n${passedOver.map((n) => `- ${n}`).join('\n')}`
1974
+ : ''));
1936
1975
  log.info('agent.start', { card: card.id, engine: candidate.id, project: projectPath });
1937
1976
  const attempt = await runEngine(candidate, { prompt, projectPath, model });
1938
1977
  log.info('agent.end', { card: card.id, engine: candidate.id, ok: attempt.ok, seconds: Math.round(attempt.durationMs / 1000), failure: attempt.failure });
@@ -1966,11 +2005,11 @@ async function produceChangeWithAgent(run) {
1966
2005
  await revert(change.changed);
1967
2006
  return backToHuman(`I need to change ${foreign.map((f) => `\`${f}\``).join(', ')}, but ${foreign.length === 1 ? 'that file has' : 'those files have'} uncommitted changes in \`${projectPath}\` that are not mine. Committing my edit would also commit that unfinished work, so I stopped and put everything back.\n\n${started.interrupted
1968
2007
  ? 'An earlier run of this card was interrupted before it finished, so these may be its leftovers rather than your work. If you did not make them, discard them (`git checkout -- <file>`); otherwise commit or stash them. Then reply here and I will continue.'
1969
- : 'Commit or stash those changes, then reply here and I will continue.'}`, `Stopped: ${foreign.join(', ')} had uncommitted changes`, foreign);
2008
+ : 'Commit or stash those changes, then reply here and I will continue.'}`, `Stopped: ${foreign.join(', ')} had uncommitted changes`, foreign, [{ label: 'Done, continue', reply: 'I committed or stashed those changes. Please continue.' }]);
1970
2009
  }
1971
2010
  if (!result.ok) {
1972
2011
  await revert(change.changed);
1973
- return backToHuman(`${engine.label} worked on this for ${Math.max(1, Math.round(result.durationMs / 60000))} minute(s) but did not finish: ${result.failure || 'it stopped without a result'}.\n\nI put the files back the way they were. Reply here to have it try again — if the card is large, splitting it into smaller cards helps.`, `${engine.label} did not finish: ${result.failure || 'no result'}`, []);
2012
+ return backToHuman(`${engine.label} worked on this for ${Math.max(1, Math.round(result.durationMs / 60000))} minute(s) but did not finish: ${result.failure || 'it stopped without a result'}.\n\nI put the files back the way they were. Reply here to have it try again — if the card is large, splitting it into smaller cards helps.`, `${engine.label} did not finish: ${result.failure || 'no result'}`, [], TRY_AGAIN);
1974
2013
  }
1975
2014
  let report = parseAgentReport(result.text);
1976
2015
  if (report.status === 'blocked' || change.changed.length === 0) {
@@ -1978,7 +2017,7 @@ async function produceChangeWithAgent(run) {
1978
2017
  const said = report.question || report.summary || proseOf(result.text);
1979
2018
  return backToHuman(report.status === 'blocked' && report.question
1980
2019
  ? report.question
1981
- : `${engine.label} finished without changing any files.${said ? `\n\n${said}` : ''}\n\nIf something is still missing, reply here with what you expected to change.`, `Needs input: ${said.slice(0, 400)}`, []);
2020
+ : `${engine.label} finished without changing any files.${said ? `\n\n${said}` : ''}\n\nIf something is still missing, reply here with what you expected to change.`, `Needs input: ${said.slice(0, 400)}`, [], report.status === 'blocked' && report.question ? report.options : []);
1982
2021
  }
1983
2022
  // 2. The runner's own build gate. The agent built it too, so a failure here is usually this machine's
1984
2023
  // environment — check that before sending the agent after an error that was never its own.
@@ -2024,7 +2063,7 @@ async function produceChangeWithAgent(run) {
2024
2063
  const stillForeign = change.overlapping.filter((f) => !carryOverFiles.includes(f));
2025
2064
  if (!gate.ok || change.changed.length === 0 || stillForeign.length > 0) {
2026
2065
  await revert(change.changed);
2027
- return backToHuman(`${engine.label} made the change, but the project did not build with it, and its attempt to fix that did not get the build green. I reverted everything rather than ship a broken build.\n\n\`\`\`\n${buildErrorExcerpt(gate.errorOutput || '')}\n\`\`\`\n\nYour description was clear — nothing more is needed from you on that. Reply here to have it try again.`, `Reverted: the build failed after ${engine.label}'s change`, []);
2066
+ return backToHuman(`${engine.label} made the change, but the project did not build with it, and its attempt to fix that did not get the build green. I reverted everything rather than ship a broken build.\n\n\`\`\`\n${buildErrorExcerpt(gate.errorOutput || '')}\n\`\`\`\n\nYour description was clear — nothing more is needed from you on that. Reply here to have it try again.`, `Reverted: the build failed after ${engine.label}'s change`, [], TRY_AGAIN);
2028
2067
  }
2029
2068
  }
2030
2069
  }
@@ -2497,7 +2536,7 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
2497
2536
  const changed = modifiedFiles.map((f) => `\`${f}\``).join(', ');
2498
2537
  await client.askQuestion(card.id, provider, deployInfo.errorKind === 'needs_confirmation'
2499
2538
  ? `I made the code change (${changed}). One question before I deploy this project for the first time:\n\n${describeFailureForHuman(deployInfo)}`
2500
- : `I made the code change (${changed}), but the deployment could not be verified, so I kept this card in progress instead of moving it to Testing.\n\n${describeFailureForHuman(deployInfo)}`);
2539
+ : `I made the code change (${changed}), but the deployment could not be verified, so I kept this card in progress instead of moving it to Testing.\n\n${describeFailureForHuman(deployInfo)}`, deployReplyOptions(deployInfo));
2501
2540
  return false;
2502
2541
  }
2503
2542
  if (deployInfo.pending && deployInfo.pendingVerification) {
@@ -2812,7 +2851,7 @@ async function processPendingVerifications(client, projectPaths) {
2812
2851
  attempts: 1,
2813
2852
  last_error: detail,
2814
2853
  });
2815
- await client.askQuestion(p.card_id, p.provider, `The deployment I started for this card did not succeed, so the change is NOT live yet.\n\nWhat failed: ${detail}\n\nFix the failing step and reply **retry**, or tell me the right route with \`deploy: <method or command>\`.`);
2854
+ await client.askQuestion(p.card_id, p.provider, `The deployment I started for this card did not succeed, so the change is NOT live yet.\n\nWhat failed: ${detail}\n\nFix the failing step and reply **retry**, or tell me the right route with \`deploy: <method or command>\`.`, deployReplyOptions({ errorKind: 'ci_failed' }));
2816
2855
  await client.reportDeployment({ card_id: p.card_id, project_path: projectPath, method: 'github_actions', ok: false, error: detail.slice(0, 480), error_kind: 'ci_failed', commit: short });
2817
2856
  }
2818
2857
  else if (ageMs > 90 * 60 * 1000) {
@@ -2879,6 +2918,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
2879
2918
  let holdsWorkerLock = false;
2880
2919
  let journalsRecovered = false;
2881
2920
  let consecutiveFailures = 0;
2921
+ let warnedNoKey = false;
2882
2922
  const autoProvisionSavedBindings = (apiKey, bindings) => {
2883
2923
  for (const b of bindings) {
2884
2924
  const projPath = (b.project_path || '').trim();
@@ -2957,19 +2997,28 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
2957
2997
  try {
2958
2998
  const config = client.loadProjectConfig();
2959
2999
  const apiKey = client.getApiKey();
2960
- if (!config?.board_id || !apiKey) {
3000
+ // The background service is not started inside a project folder: it has no board of its own and
3001
+ // serves every folder linked on this machine. A coding tool's MCP server only pulses for its project.
3002
+ const homeBoardId = config?.board_id || '';
3003
+ if (!apiKey || (!homeBoardId && role !== 'daemon')) {
3004
+ if (role === 'daemon' && !apiKey && !warnedNoKey) {
3005
+ warnedNoKey = true;
3006
+ log.warn('daemon.no_api_key', { hint: 'run `npx -y @agilemonster/mcp-server bootstrap` in a project folder' });
3007
+ }
2961
3008
  return;
2962
3009
  }
3010
+ const homePath = homeBoardId ? config?.project_path || process.cwd() : '';
2963
3011
  const includeDiscovery = pulseCount === 0 || pulseCount % 3 === 0;
2964
3012
  pulseCount++;
2965
3013
  const res = await client.pulse({
2966
- boardId: config.board_id,
3014
+ boardId: homeBoardId || undefined,
2967
3015
  provider: 'any',
2968
- projectName: config.project_name || path.basename(process.cwd()),
2969
- projectPath: config.project_path || process.cwd(),
3016
+ projectName: homeBoardId ? config?.project_name || path.basename(process.cwd()) : undefined,
3017
+ projectPath: homePath || undefined,
2970
3018
  discoveredProjects: includeDiscovery ? client.discoverLocalProjects() : undefined,
2971
3019
  discoveredModels: includeDiscovery ? client.discoverLocalAgentModels() : undefined,
2972
3020
  activeClaims: activeClaims(),
3021
+ machine: await machineReport(role).catch(() => undefined),
2973
3022
  });
2974
3023
  if (consecutiveFailures > 0) {
2975
3024
  log.info('pulse.recovered', { after_failures: consecutiveFailures });
@@ -2979,13 +3028,14 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
2979
3028
  autoProvisionSavedBindings(apiKey, res.saved_bindings);
2980
3029
  }
2981
3030
  const queued = res.queued_cards || [];
2982
- syncIdeTaskRules(config.board_name || 'Board', queued);
3031
+ if (homeBoardId)
3032
+ syncIdeTaskRules(config?.board_name || 'Board', queued);
2983
3033
  for (const card of queued) {
2984
3034
  if (!notifiedCardIds.has(card.id)) {
2985
3035
  notifiedCardIds.add(card.id);
2986
3036
  const worker = card.agent_display_name || card.agent_provider || 'AI Agent';
2987
3037
  const modelTag = card.agent_model && card.agent_model !== 'auto' ? ` [${card.agent_model}]` : '';
2988
- sendDesktopNotification(`Agile Monster -> ${worker}${modelTag}`, `New task queued on ${config.board_name || 'Board'}: "${card.title}"`);
3038
+ sendDesktopNotification(`Agile Monster -> ${worker}${modelTag}`, `New task queued on ${config?.board_name || 'Board'}: "${card.title}"`);
2989
3039
  try {
2990
3040
  if (server) {
2991
3041
  await server.sendResourceUpdated({ uri: 'agilemonster://project/context' });
@@ -3005,7 +3055,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
3005
3055
  const boundQueued = res.all_bound_queued_cards || [];
3006
3056
  for (const qCard of boundQueued) {
3007
3057
  const targetPath = (qCard.bound_project_path || '').trim() ||
3008
- (qCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
3058
+ (homeBoardId && qCard.board_id === homeBoardId ? homePath : '');
3009
3059
  if (targetPath && fs.existsSync(targetPath)) {
3010
3060
  await executeQueuedCardAutonomously(client, qCard, targetPath);
3011
3061
  }
@@ -3016,12 +3066,12 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
3016
3066
  const boundDone = res.all_bound_done_cards || [];
3017
3067
  for (const dCard of boundDone) {
3018
3068
  const targetPath = (dCard.bound_project_path || '').trim() ||
3019
- (dCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
3069
+ (homeBoardId && dCard.board_id === homeBoardId ? homePath : '');
3020
3070
  if (targetPath && fs.existsSync(targetPath)) {
3021
3071
  await deployApprovedCardIfNeeded(client, dCard, targetPath);
3022
3072
  }
3023
3073
  }
3024
- const knownPaths = new Set([config.project_path || process.cwd()]);
3074
+ const knownPaths = new Set(homePath ? [homePath] : []);
3025
3075
  for (const b of res.saved_bindings || []) {
3026
3076
  if (b.project_path)
3027
3077
  knownPaths.add(b.project_path.trim());
@@ -3079,6 +3129,10 @@ export async function runBackgroundPulseDaemon(intervalMs = 20000) {
3079
3129
  log.error('daemon.uncaught_exception', { error: errorMessage(err), stack: err instanceof Error ? err.stack : undefined });
3080
3130
  process.exit(1);
3081
3131
  });
3132
+ // Which agent will work a queued card here, on record before the first card arrives
3133
+ engineStatuses()
3134
+ .then((agents) => log.info('daemon.agents', { agents: agents.map((a) => ({ id: a.id, ready: !a.problem, bin: a.bin, problem: a.problem })) }))
3135
+ .catch(() => { });
3082
3136
  startBuiltInPullPulse(null, client, intervalMs, 'daemon');
3083
3137
  // Watch dist/server.js for updates so launchd reloads new code after a rebuild — but only between jobs,
3084
3138
  // never in the middle of an edit or a deployment.
@@ -0,0 +1,82 @@
1
+ import { ExecOptions, ExecResult } from './runtime.js';
2
+ /**
3
+ * Runs the card runner as a background service that starts with the computer.
4
+ *
5
+ * Without this, cards queued from the board are only worked while an AI coding tool happens to be open in
6
+ * the project folder (its MCP server pulses as a side effect). People do not know that, so their first card
7
+ * sits in "queued" for ever. `bootstrap` installs the service; `service install|status|uninstall` manage it.
8
+ *
9
+ * The service runs its own copy of the package from ~/.agilemonster/runner, so it does not depend on an npx
10
+ * cache that can be cleared or on a project folder that can move, and it updates that copy itself.
11
+ *
12
+ * macOS: a launchd agent. Linux: a systemd user unit. Windows: not yet.
13
+ */
14
+ export declare const SERVICE_LABEL = "com.agilemonster.pulse";
15
+ export declare const SYSTEMD_UNIT = "agilemonster-runner.service";
16
+ export type ServiceKind = 'launchd' | 'systemd';
17
+ export interface ServiceDeps {
18
+ platform: NodeJS.Platform;
19
+ home: string;
20
+ /** Machine-local Agile Monster folder (~/.agilemonster). */
21
+ amHome: string;
22
+ uid: number | null;
23
+ node: string;
24
+ npm: string | null;
25
+ pathEnv: string;
26
+ exec: (command: string, args?: string[], options?: ExecOptions) => Promise<ExecResult>;
27
+ }
28
+ /**
29
+ * The node binary the service should start. `process.execPath` is often a versioned folder (Homebrew's
30
+ * Cellar/node@24/24.21.0_1/…) that disappears with the next upgrade and would leave the service unable to
31
+ * start; the `node` on PATH is a stable link to the same binary, so prefer it when it is the same one.
32
+ */
33
+ export declare function stableNodePath(): string;
34
+ export declare function defaultServiceDeps(): ServiceDeps;
35
+ export declare function runnerDir(amHome: string): string;
36
+ /** The service's own copy of this package's entry point. */
37
+ export declare function runnerEntry(amHome: string): string;
38
+ export declare function serviceKind(platform: NodeJS.Platform): ServiceKind | null;
39
+ export declare function unitPath(kind: ServiceKind, home: string): string;
40
+ export interface UnitOptions {
41
+ node: string;
42
+ entry: string;
43
+ home: string;
44
+ logDir: string;
45
+ pathEnv: string;
46
+ }
47
+ export declare function launchdPlist(o: UnitOptions): string;
48
+ export declare function systemdUnit(o: UnitOptions): string;
49
+ /** The program an existing unit file starts (the script after the node binary), or null when unreadable. */
50
+ export declare function unitTarget(kind: ServiceKind, content: string): string | null;
51
+ export interface ServiceStatus {
52
+ supported: boolean;
53
+ kind: ServiceKind | null;
54
+ unitPath: string | null;
55
+ installed: boolean;
56
+ /** True when the unit starts the copy this module installs (not a hand-made or development setup). */
57
+ managed: boolean;
58
+ target: string | null;
59
+ running: boolean;
60
+ }
61
+ export declare function serviceStatus(deps?: ServiceDeps): Promise<ServiceStatus>;
62
+ export interface ServiceResult {
63
+ ok: boolean;
64
+ /** 'installed' | 'kept' (someone else's setup left alone) | 'unsupported' | 'failed' | 'removed' | 'absent' */
65
+ outcome: 'installed' | 'kept' | 'unsupported' | 'failed' | 'removed' | 'absent';
66
+ message: string;
67
+ }
68
+ export declare function installService(options: {
69
+ force?: boolean;
70
+ hasApiKey: boolean;
71
+ }, deps?: ServiceDeps): Promise<ServiceResult>;
72
+ export declare function uninstallService(options?: {
73
+ force?: boolean;
74
+ }, deps?: ServiceDeps): Promise<ServiceResult>;
75
+ /** npm arguments that install or update the service's own copy of the package. */
76
+ export declare function updateArgs(dir: string, version?: string): string[];
77
+ /**
78
+ * When this process is the service's own copy, the folder to update in place. Null for a copy started by
79
+ * npx, a global install or a source checkout: those update the way they were installed.
80
+ */
81
+ export declare function managedRunnerDir(runningFile: string, amHome?: string): string | null;
82
+ export declare function describeService(deps?: ServiceDeps): Promise<string>;