@agilemonster/mcp-server 1.0.16 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/bin/mcp.js +0 -0
- package/bin/orchestrator.js +0 -0
- package/bin/worker.js +0 -0
- package/dist/client.d.ts +21 -4
- package/dist/client.js +119 -101
- package/dist/deploy-memory.d.ts +122 -0
- package/dist/deploy-memory.js +192 -0
- package/dist/deploy-profile.d.ts +151 -0
- package/dist/deploy-profile.js +993 -0
- package/dist/deploy-runner.d.ts +144 -0
- package/dist/deploy-runner.js +1108 -0
- package/dist/guidelines.d.ts +1 -1
- package/dist/guidelines.js +6 -0
- package/dist/ide-config.d.ts +44 -0
- package/dist/ide-config.js +250 -0
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.js +18 -165
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +58 -0
- package/dist/runtime.d.ts +64 -0
- package/dist/runtime.js +293 -0
- package/dist/server.d.ts +16 -3
- package/dist/server.js +1427 -547
- package/dist/types.d.ts +29 -0
- package/package.json +4 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { agileMonsterHome, readJson, writeJsonAtomic } from './runtime.js';
|
|
5
|
+
const HISTORY_LIMIT = 30;
|
|
6
|
+
const CARD_STATE_LIMIT = 200;
|
|
7
|
+
const DEPLOYED_CARDS_LIMIT = 300;
|
|
8
|
+
const STALE_CARD_STATE_MS = 45 * 24 * 60 * 60 * 1000;
|
|
9
|
+
export function memoryFileFor(projectPath) {
|
|
10
|
+
const resolved = path.resolve(projectPath);
|
|
11
|
+
const hash = crypto.createHash('sha1').update(resolved).digest('hex').slice(0, 10);
|
|
12
|
+
const slug = path.basename(resolved).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'project';
|
|
13
|
+
return path.join(agileMonsterHome(), 'projects', `${slug}-${hash}.json`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Recover "how the last deployment worked" from the activity log earlier daemon versions wrote into the
|
|
17
|
+
* project (`.agilemonster/ACTIVITY.md`), so projects that already deployed successfully keep their route
|
|
18
|
+
* instead of starting from a fresh guess.
|
|
19
|
+
*/
|
|
20
|
+
export function parseLastDeploymentFromActivityLog(logText) {
|
|
21
|
+
// The log mixes daemon output with card titles and model-written summaries, so it is only trusted for
|
|
22
|
+
// WHICH of the two routine routes last succeeded — never for a command to run. (A script route found
|
|
23
|
+
// this way would still have to be confirmed by a human.) Headings carry card titles: skip them.
|
|
24
|
+
const lines = logText
|
|
25
|
+
.split(/\r?\n/)
|
|
26
|
+
.filter((l) => l.includes('🚀 **Deployed**') && !l.trimStart().startsWith('#'));
|
|
27
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
28
|
+
const line = lines[i].slice(lines[i].indexOf('🚀 **Deployed**'));
|
|
29
|
+
if (/deployed (?:via|with) `/i.test(line))
|
|
30
|
+
return null; // last route was a script/CLI: needs confirmation, not seeding
|
|
31
|
+
if (/Docker (containers rebuilt|Compose)/i.test(line))
|
|
32
|
+
return { method: 'docker_compose', command: null };
|
|
33
|
+
if (/GitHub Actions (deployment )?passed/i.test(line))
|
|
34
|
+
return { method: 'github_actions', command: null };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function seedFromActivityLog(projectPath) {
|
|
39
|
+
try {
|
|
40
|
+
const file = path.join(projectPath, '.agilemonster', 'ACTIVITY.md');
|
|
41
|
+
if (!fs.existsSync(file))
|
|
42
|
+
return null;
|
|
43
|
+
const stat = fs.statSync(file);
|
|
44
|
+
const fd = fs.openSync(file, 'r');
|
|
45
|
+
const length = Math.min(stat.size, 300 * 1024);
|
|
46
|
+
const buffer = Buffer.alloc(length);
|
|
47
|
+
fs.readSync(fd, buffer, 0, length, stat.size - length);
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
const found = parseLastDeploymentFromActivityLog(buffer.toString('utf8'));
|
|
50
|
+
if (!found)
|
|
51
|
+
return null;
|
|
52
|
+
return { method: found.method, command: found.command, at: stat.mtime.toISOString(), commit: null, card_id: null };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function loadMemory(projectPath) {
|
|
59
|
+
const file = memoryFileFor(projectPath);
|
|
60
|
+
const stored = readJson(file);
|
|
61
|
+
if (!stored && !fs.existsSync(file)) {
|
|
62
|
+
const seeded = seedFromActivityLog(projectPath);
|
|
63
|
+
if (seeded) {
|
|
64
|
+
const memory = {
|
|
65
|
+
project_path: path.resolve(projectPath),
|
|
66
|
+
updated_at: new Date().toISOString(),
|
|
67
|
+
explicit: null,
|
|
68
|
+
last_success: seeded,
|
|
69
|
+
failures: {},
|
|
70
|
+
history: [],
|
|
71
|
+
pending: [],
|
|
72
|
+
cards: {},
|
|
73
|
+
deployed_cards: {},
|
|
74
|
+
};
|
|
75
|
+
saveMemory(memory);
|
|
76
|
+
return memory;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
project_path: path.resolve(projectPath),
|
|
81
|
+
updated_at: stored?.updated_at || new Date(0).toISOString(),
|
|
82
|
+
explicit: stored?.explicit || null,
|
|
83
|
+
last_success: stored?.last_success || null,
|
|
84
|
+
failures: stored?.failures && typeof stored.failures === 'object' ? stored.failures : {},
|
|
85
|
+
history: Array.isArray(stored?.history) ? stored.history : [],
|
|
86
|
+
pending: Array.isArray(stored?.pending) ? stored.pending : [],
|
|
87
|
+
cards: stored?.cards && typeof stored.cards === 'object' ? stored.cards : {},
|
|
88
|
+
deployed_cards: stored?.deployed_cards && typeof stored.deployed_cards === 'object' ? stored.deployed_cards : {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function saveMemory(memory) {
|
|
92
|
+
memory.updated_at = new Date().toISOString();
|
|
93
|
+
// Card state means "edits exist that still have to ship": only drop it when it is clearly abandoned.
|
|
94
|
+
const cardIds = Object.keys(memory.cards);
|
|
95
|
+
for (const id of cardIds) {
|
|
96
|
+
if (Date.now() - (Date.parse(memory.cards[id].at) || 0) > STALE_CARD_STATE_MS)
|
|
97
|
+
delete memory.cards[id];
|
|
98
|
+
}
|
|
99
|
+
const remaining = Object.keys(memory.cards);
|
|
100
|
+
if (remaining.length > CARD_STATE_LIMIT) {
|
|
101
|
+
remaining
|
|
102
|
+
.sort((a, b) => (memory.cards[a].at < memory.cards[b].at ? -1 : 1))
|
|
103
|
+
.slice(0, remaining.length - CARD_STATE_LIMIT)
|
|
104
|
+
.forEach((id) => delete memory.cards[id]);
|
|
105
|
+
}
|
|
106
|
+
const deployedIds = Object.keys(memory.deployed_cards);
|
|
107
|
+
if (deployedIds.length > DEPLOYED_CARDS_LIMIT) {
|
|
108
|
+
deployedIds
|
|
109
|
+
.sort((a, b) => (memory.deployed_cards[a].at < memory.deployed_cards[b].at ? -1 : 1))
|
|
110
|
+
.slice(0, deployedIds.length - DEPLOYED_CARDS_LIMIT)
|
|
111
|
+
.forEach((id) => delete memory.deployed_cards[id]);
|
|
112
|
+
}
|
|
113
|
+
memory.history = memory.history.slice(-HISTORY_LIMIT);
|
|
114
|
+
try {
|
|
115
|
+
writeJsonAtomic(memoryFileFor(memory.project_path), memory);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Memory is an optimization; failing to persist it must never fail a deployment.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Record the outcome of an attempt and update what the project "knows" about its own deployment. */
|
|
122
|
+
export function recordAttempt(projectPath, attempt) {
|
|
123
|
+
const memory = loadMemory(projectPath);
|
|
124
|
+
const at = new Date().toISOString();
|
|
125
|
+
const { live_url: liveUrl, wrong_method: wrongMethod, unverified, ...rest } = attempt;
|
|
126
|
+
memory.history.push({ at, ...rest, error: rest.error ? rest.error.slice(0, 600) : rest.error });
|
|
127
|
+
if (attempt.ok && attempt.card_id) {
|
|
128
|
+
memory.deployed_cards[attempt.card_id] = {
|
|
129
|
+
at,
|
|
130
|
+
commit: attempt.commit || null,
|
|
131
|
+
method: attempt.method,
|
|
132
|
+
verified: !attempt.pending && !unverified,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Only a deployment that was actually verified teaches the project its route
|
|
136
|
+
if (attempt.ok && !attempt.pending && !unverified) {
|
|
137
|
+
memory.last_success = {
|
|
138
|
+
method: attempt.method,
|
|
139
|
+
command: attempt.command || null,
|
|
140
|
+
live_url: liveUrl || memory.last_success?.live_url || null,
|
|
141
|
+
at,
|
|
142
|
+
commit: attempt.commit || null,
|
|
143
|
+
card_id: attempt.card_id || null,
|
|
144
|
+
};
|
|
145
|
+
delete memory.failures[attempt.method];
|
|
146
|
+
}
|
|
147
|
+
else if (!attempt.ok) {
|
|
148
|
+
const prev = memory.failures[attempt.method];
|
|
149
|
+
memory.failures[attempt.method] = {
|
|
150
|
+
count: (prev?.count || 0) + 1,
|
|
151
|
+
last_kind: attempt.kind || null,
|
|
152
|
+
last_error: attempt.error ? attempt.error.slice(0, 400) : null,
|
|
153
|
+
last_at: at,
|
|
154
|
+
wrong_method: Boolean(wrongMethod),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
saveMemory(memory);
|
|
158
|
+
return memory;
|
|
159
|
+
}
|
|
160
|
+
export function setExplicitProfile(projectPath, explicit) {
|
|
161
|
+
const memory = loadMemory(projectPath);
|
|
162
|
+
memory.explicit = { ...explicit, set_at: new Date().toISOString() };
|
|
163
|
+
// A human or IDE agent just told us how to deploy: earlier failures of that method are no longer a reason to avoid it.
|
|
164
|
+
delete memory.failures[explicit.method];
|
|
165
|
+
saveMemory(memory);
|
|
166
|
+
return memory;
|
|
167
|
+
}
|
|
168
|
+
export function setCardState(projectPath, cardId, state) {
|
|
169
|
+
const memory = loadMemory(projectPath);
|
|
170
|
+
if (state)
|
|
171
|
+
memory.cards[cardId] = state;
|
|
172
|
+
else
|
|
173
|
+
delete memory.cards[cardId];
|
|
174
|
+
saveMemory(memory);
|
|
175
|
+
}
|
|
176
|
+
export function getCardState(projectPath, cardId) {
|
|
177
|
+
return loadMemory(projectPath).cards[cardId] || null;
|
|
178
|
+
}
|
|
179
|
+
export function addPendingVerification(projectPath, pending) {
|
|
180
|
+
const memory = loadMemory(projectPath);
|
|
181
|
+
memory.pending = memory.pending.filter((p) => p.card_id !== pending.card_id);
|
|
182
|
+
memory.pending.push(pending);
|
|
183
|
+
saveMemory(memory);
|
|
184
|
+
}
|
|
185
|
+
export function removePendingVerification(projectPath, cardId) {
|
|
186
|
+
const memory = loadMemory(projectPath);
|
|
187
|
+
const next = memory.pending.filter((p) => p.card_id !== cardId);
|
|
188
|
+
if (next.length !== memory.pending.length) {
|
|
189
|
+
memory.pending = next;
|
|
190
|
+
saveMemory(memory);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ProjectMemory } from './deploy-memory.js';
|
|
2
|
+
/**
|
|
3
|
+
* Evidence-based deployment profile resolution.
|
|
4
|
+
*
|
|
5
|
+
* The old detector returned the first deploy-looking file it found (any compose file won), and the
|
|
6
|
+
* server kept that first guess forever. This module instead gathers every piece of evidence a project
|
|
7
|
+
* offers — how the running containers were actually started, which CI workflows really fire on a push
|
|
8
|
+
* to the current branch, deploy scripts, cloud CLI configs, the project's own docs, and what worked
|
|
9
|
+
* last time — ranks the candidates, and says how confident it is and why.
|
|
10
|
+
*
|
|
11
|
+
* Precedence: explicit project file > user-set binding > last verified success > ranked evidence.
|
|
12
|
+
*/
|
|
13
|
+
export type DeployMethod = 'github_actions' | 'docker_compose' | 'deploy_script' | 'cloud_cli' | 'git_push' | 'manual' | 'none';
|
|
14
|
+
export type ProfileSource = 'explicit' | 'user' | 'learned' | 'verified' | 'detected';
|
|
15
|
+
export type Confidence = 'high' | 'medium' | 'low';
|
|
16
|
+
export interface PushTrigger {
|
|
17
|
+
branches: string[] | null;
|
|
18
|
+
branchesIgnore: string[] | null;
|
|
19
|
+
paths: string[] | null;
|
|
20
|
+
pathsIgnore: string[] | null;
|
|
21
|
+
tagsOnly: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface WorkflowInfo {
|
|
24
|
+
file: string;
|
|
25
|
+
name: string;
|
|
26
|
+
isDeploy: boolean;
|
|
27
|
+
events: string[];
|
|
28
|
+
push: PushTrigger | null;
|
|
29
|
+
dispatch: boolean;
|
|
30
|
+
workflowRun: {
|
|
31
|
+
workflows: string[];
|
|
32
|
+
branches: string[] | null;
|
|
33
|
+
} | null;
|
|
34
|
+
}
|
|
35
|
+
export interface ComposeFileInfo {
|
|
36
|
+
rel: string;
|
|
37
|
+
kind: 'default' | 'dev' | 'prod' | 'other';
|
|
38
|
+
hasBuild: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ComposeStack {
|
|
41
|
+
projectName: string;
|
|
42
|
+
workingDir: string;
|
|
43
|
+
configFiles: string[];
|
|
44
|
+
services: string[];
|
|
45
|
+
runningCount: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ComposeTarget {
|
|
48
|
+
projectName: string | null;
|
|
49
|
+
/** Compose files relative to the project root (or absolute when outside it). */
|
|
50
|
+
files: string[];
|
|
51
|
+
running: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface DeployCandidate {
|
|
54
|
+
method: DeployMethod;
|
|
55
|
+
command: string | null;
|
|
56
|
+
score: number;
|
|
57
|
+
evidence: string[];
|
|
58
|
+
compose?: ComposeTarget;
|
|
59
|
+
/** Deploy workflows that this candidate relies on. */
|
|
60
|
+
workflows?: WorkflowInfo[];
|
|
61
|
+
/** The deploy workflow only starts when dispatched (pushing alone does not deploy). */
|
|
62
|
+
dispatchOnly?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface BoundProfile {
|
|
65
|
+
deploy_method?: string | null;
|
|
66
|
+
deploy_command?: string | null;
|
|
67
|
+
live_url?: string | null;
|
|
68
|
+
build_command?: string | null;
|
|
69
|
+
/** 'user' = set by a human, 'verified' = confirmed by a successful deploy, 'auto' = a guess. */
|
|
70
|
+
deploy_source?: string | null;
|
|
71
|
+
}
|
|
72
|
+
export interface GitInfo {
|
|
73
|
+
isRepo: boolean;
|
|
74
|
+
branch: string | null;
|
|
75
|
+
hasRemote: boolean;
|
|
76
|
+
remoteUrl: string | null;
|
|
77
|
+
}
|
|
78
|
+
export interface DeployProfile {
|
|
79
|
+
method: DeployMethod;
|
|
80
|
+
command: string | null;
|
|
81
|
+
source: ProfileSource;
|
|
82
|
+
confidence: Confidence;
|
|
83
|
+
evidence: string[];
|
|
84
|
+
liveUrl: string | null;
|
|
85
|
+
/** 'guessed' URLs are informational: an unreachable guess must never fail a deployment. */
|
|
86
|
+
liveUrlSource: 'explicit' | 'user' | 'learned' | 'guessed' | null;
|
|
87
|
+
buildCommand: string | null;
|
|
88
|
+
compose?: ComposeTarget;
|
|
89
|
+
workflows: WorkflowInfo[];
|
|
90
|
+
dispatchOnly: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The route was only inferred and would run something with production reach that nobody has confirmed
|
|
93
|
+
* (a deploy script, a cloud CLI, a manually-dispatched workflow). Ask once, then remember.
|
|
94
|
+
*/
|
|
95
|
+
needsConfirmation: boolean;
|
|
96
|
+
alternatives: DeployCandidate[];
|
|
97
|
+
git: GitInfo;
|
|
98
|
+
deploymentNotes: string;
|
|
99
|
+
}
|
|
100
|
+
export interface ProfileProbes {
|
|
101
|
+
/** Output of the docker ps probe (see COMPOSE_PS_FORMAT), or null when docker is unavailable. */
|
|
102
|
+
dockerPs?: () => string | null;
|
|
103
|
+
git?: (projectPath: string) => GitInfo;
|
|
104
|
+
}
|
|
105
|
+
export interface ResolveOptions {
|
|
106
|
+
bound?: BoundProfile | null;
|
|
107
|
+
memory?: ProjectMemory | null;
|
|
108
|
+
probes?: ProfileProbes;
|
|
109
|
+
}
|
|
110
|
+
export declare function normalizeMethod(raw: string | null | undefined): DeployMethod | null;
|
|
111
|
+
/**
|
|
112
|
+
* Older versions stored descriptions such as "git push origin HEAD (GitHub Actions CI/CD)" in the command
|
|
113
|
+
* field. Those are not commands. (Real commands may contain parentheses, e.g. `TAG=$(git rev-parse HEAD)`.)
|
|
114
|
+
*/
|
|
115
|
+
export declare function isRunnableCommand(cmd: string | null | undefined): boolean;
|
|
116
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
117
|
+
export declare function parseWorkflow(file: string, text: string): WorkflowInfo;
|
|
118
|
+
export declare function readWorkflows(projectPath: string): WorkflowInfo[];
|
|
119
|
+
/** Does a push to `branch` start this workflow, directly or through a `workflow_run` chain? */
|
|
120
|
+
export declare function workflowRunsOnPush(wf: WorkflowInfo, branch: string, all: WorkflowInfo[], depth?: number): boolean;
|
|
121
|
+
/** With `paths:` filters, a commit that touches none of them does not start the workflow. */
|
|
122
|
+
export declare function pushPathsMatch(push: PushTrigger | null, files: string[]): boolean;
|
|
123
|
+
export declare const COMPOSE_PS_FORMAT = "{{.Label \"com.docker.compose.project\"}}\t{{.Label \"com.docker.compose.project.working_dir\"}}\t{{.Label \"com.docker.compose.project.config_files\"}}\t{{.Label \"com.docker.compose.service\"}}\t{{.State}}";
|
|
124
|
+
export declare function findComposeFiles(projectPath: string): ComposeFileInfo[];
|
|
125
|
+
/** Parse the docker ps probe and keep the compose projects that were started from inside `projectPath`. */
|
|
126
|
+
export declare function parseComposeStacks(psOutput: string, projectPath: string): ComposeStack[];
|
|
127
|
+
export declare function composeCommand(target: ComposeTarget, tail?: string): string;
|
|
128
|
+
/** Arguments for `docker compose …` matching a target (no shell quoting needed). */
|
|
129
|
+
export declare function composeArgs(target: ComposeTarget): string[];
|
|
130
|
+
/** Pull the lines of the project docs that talk about deployment (compact, for the code model and for humans). */
|
|
131
|
+
export declare function extractDeploymentNotes(projectPath: string, maxChars?: number): string;
|
|
132
|
+
export declare function detectBuildCommand(projectPath: string): string | null;
|
|
133
|
+
/** "app.example.co.za" -> "example": the label a project name can be compared with. */
|
|
134
|
+
export declare function registrableLabel(host: string): string;
|
|
135
|
+
/**
|
|
136
|
+
* Best-effort production URL. Returns a URL only when something ties it to this project
|
|
137
|
+
* (an APP_URL-style env value, a doc URL whose host resembles the project/repo name, or a
|
|
138
|
+
* folder literally named like a domain) — never just "the first link in the README".
|
|
139
|
+
*/
|
|
140
|
+
export declare function inferLiveUrl(projectPath: string, name: string, remoteUrl?: string | null): string | null;
|
|
141
|
+
export declare function gatherCandidates(projectPath: string, git: GitInfo, workflows: WorkflowInfo[], dockerPs: string | null): DeployCandidate[];
|
|
142
|
+
export declare function resolveDeployProfile(projectPath: string, options?: ResolveOptions): DeployProfile;
|
|
143
|
+
/**
|
|
144
|
+
* Pushing to the checked-out branch (and whatever pipeline the repo attached to that) and refreshing local
|
|
145
|
+
* containers are routine. Running a deploy script, a cloud CLI, or manually dispatching a workflow reaches
|
|
146
|
+
* production in ways the repository did not automate — so an unconfirmed guess must be confirmed once.
|
|
147
|
+
*/
|
|
148
|
+
export declare function requiresConfirmation(candidate: Pick<DeployCandidate, 'method' | 'dispatchOnly'>): boolean;
|
|
149
|
+
export declare function methodLabel(method: DeployMethod): string;
|
|
150
|
+
/** Compact, human-readable description used in card comments, the code model's context, and MCP tool output. */
|
|
151
|
+
export declare function describeProfile(profile: DeployProfile): string;
|