claude-autopm 3.21.0 → 3.22.1

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.
@@ -0,0 +1,12 @@
1
+ ---
2
+ allowed-tools: Bash, Read
3
+ ---
4
+
5
+ # Diagram List
6
+
7
+ List available project diagrams.
8
+
9
+ ## Usage
10
+ ```bash
11
+ node .claude/scripts/pm/diagram-list.js
12
+ ```
@@ -0,0 +1,85 @@
1
+ ---
2
+ allowed-tools: Bash, Read, Write, Glob, Grep
3
+ ---
4
+
5
+ # Diagram New
6
+
7
+ Create a project architecture diagram by analyzing the codebase.
8
+
9
+ ## Usage
10
+ /pm:diagram-new <name> [--type architecture|modules|data-flow|dependencies]
11
+
12
+ ## Instructions
13
+
14
+ 1. Determine diagram type from --type flag or infer from project:
15
+ - **architecture**: High-level system components and how they connect
16
+ - **modules**: Import/require dependency graph between source files
17
+ - **data-flow**: How data moves through the system (API → service → DB)
18
+ - **dependencies**: package.json dependency tree
19
+
20
+ 2. Analyze the project:
21
+ - Read package.json for dependencies and project type
22
+ - Scan src/ or main source directory for imports/requires
23
+ - Check for docker-compose.yml, Dockerfile
24
+ - Check for API routes (Express, FastAPI, etc.)
25
+ - Check for database models/schemas
26
+ - Check for .env for external service connections
27
+ **NEVER include actual .env values (tokens, passwords, secrets) in diagram or metadata.**
28
+ Only infer service names/types from variable names, not values.
29
+
30
+ 3. Generate Mermaid diagram based on analysis:
31
+ - Use `graph TD` for hierarchical, `graph LR` for flow
32
+ - Group related modules in `subgraph` blocks
33
+ - Use icons: databases [(DB)], services [Service], external{{External}}
34
+ - Color-code using Mermaid classDef:
35
+ ```
36
+ classDef active fill:#d4f8db,stroke:#2f855a
37
+ classDef config fill:#e2e8f0,stroke:#4a5568
38
+ classDef api fill:#ebf4ff,stroke:#2b6cb0
39
+ class FastAPI api
40
+ class PostgreSQL active
41
+ ```
42
+
43
+ 4. Save diagram:
44
+ ```bash
45
+ mkdir -p .claude/pm/diagrams
46
+ ```
47
+ Write Mermaid syntax to `.claude/pm/diagrams/<name>.mmd`
48
+ Write metadata to `.claude/pm/diagrams/<name>.meta.json`:
49
+ ```json
50
+ {"name":"<name>","type":"<type>","created":"<ISO>","updated":"<ISO>","scope":"src/"}
51
+ ```
52
+
53
+ 5. Display the diagram in terminal (show the Mermaid source)
54
+
55
+ ## Example Output
56
+
57
+ For a FastAPI + React project:
58
+ ```mermaid
59
+ graph TD
60
+ subgraph Frontend
61
+ React[React App]
62
+ Redux[Redux Store]
63
+ API_Client[API Client]
64
+ end
65
+
66
+ subgraph Backend
67
+ FastAPI[FastAPI Server]
68
+ Auth[Auth Module]
69
+ Users[Users API]
70
+ ORM[SQLAlchemy ORM]
71
+ end
72
+
73
+ subgraph Data
74
+ PostgreSQL[(PostgreSQL)]
75
+ Redis[(Redis Cache)]
76
+ end
77
+
78
+ React --> API_Client
79
+ API_Client --> FastAPI
80
+ FastAPI --> Auth
81
+ FastAPI --> Users
82
+ Users --> ORM
83
+ ORM --> PostgreSQL
84
+ Auth --> Redis
85
+ ```
@@ -0,0 +1,14 @@
1
+ ---
2
+ allowed-tools: Bash, Read
3
+ ---
4
+
5
+ # Diagram Show
6
+
7
+ Display a project diagram.
8
+
9
+ ## Usage
10
+ ```bash
11
+ node .claude/scripts/pm/diagram-list.js --show <name>
12
+ ```
13
+
14
+ Shows the Mermaid source of the diagram. View rendered version in `/pm:dashboard` Diagrams tab.
@@ -0,0 +1,24 @@
1
+ ---
2
+ allowed-tools: Bash, Read, Write, Glob, Grep
3
+ ---
4
+
5
+ # Diagram Update
6
+
7
+ Update an existing project diagram after code changes.
8
+
9
+ ## Usage
10
+ /pm:diagram-update <name>
11
+
12
+ ## Instructions
13
+
14
+ 1. Read existing diagram from `.claude/pm/diagrams/<name>.mmd`
15
+ 2. Read metadata from `.claude/pm/diagrams/<name>.meta.json` for type and scope
16
+ 3. If either file is missing: `❌ Diagram not found: Run /pm:diagram-new <name>`
17
+ 4. Re-analyze the project (same analysis as diagram-new, scoped to metadata scope)
18
+ 5. Compare with existing diagram — identify:
19
+ - New modules/components added
20
+ - Removed modules
21
+ - Changed connections
22
+ 6. Update the .mmd file with changes
23
+ 7. Update metadata: set `updated` to current timestamp (`date -u +"%Y-%m-%dT%H:%M:%SZ"`)
24
+ 8. Show diff summary: "Added: X, Removed: Y, Updated: Z connections"
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: autopm
3
+ command: node
4
+ args: [".claude/scripts/mcp/autopm-server.js"]
5
+ description: AutoPM project management — issues, epics, PRDs, learnings, checkpoints
6
+ category: project-management
7
+ status: inactive
8
+ ---
9
+
10
+ # AutoPM MCP Server
11
+
12
+ Local MCP server exposing AutoPM project management data. Reuses local providers — same data as slash commands.
13
+
14
+ ## Tools (13)
15
+ - `autopm_list_issues` — list issues (filter by status)
16
+ - `autopm_show_issue` — issue details
17
+ - `autopm_create_issue` — create new issue
18
+ - `autopm_start_issue` — start working (set in_progress)
19
+ - `autopm_close_issue` — close issue
20
+ - `autopm_list_epics` — list epics with progress
21
+ - `autopm_show_epic` — epic with tasks
22
+ - `autopm_list_prds` — list PRDs
23
+ - `autopm_show_prd` — PRD details
24
+ - `autopm_status` — project overview
25
+ - `autopm_learn` — save learning
26
+ - `autopm_recall` — get learnings
27
+ - `autopm_checkpoint` — create checkpoint
28
+
29
+ ## Resources (5)
30
+ - `autopm://config` — config.json
31
+ - `autopm://agents` — agent-registry.xml
32
+ - `autopm://events` — recent events
33
+ - `autopm://learnings` — all learnings
34
+ - `autopm://test-plan` — test plan
35
+
36
+ ## Prompts (3)
37
+ - `autopm_issue_template` — issue XML template
38
+ - `autopm_prd_template` — PRD XML template
39
+ - `autopm_epic_template` — epic XML template
40
+
41
+ ## Enable
42
+ ```bash
43
+ autopm mcp enable autopm
44
+ ```
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AutoPM MCP Server
4
+ *
5
+ * Exposes PM data (issues, epics, PRDs, learnings, config) via Model Context Protocol.
6
+ * Reuses local providers as backend — no data duplication.
7
+ *
8
+ * Usage:
9
+ * node .claude/scripts/mcp/autopm-server.js
10
+ *
11
+ * In .claude/mcp-servers.json:
12
+ * { "autopm": { "command": "node", "args": [".claude/scripts/mcp/autopm-server.js"] } }
13
+ */
14
+
15
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
16
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
17
+ const {
18
+ CallToolRequestSchema,
19
+ ListToolsRequestSchema,
20
+ ListResourcesRequestSchema,
21
+ ReadResourceRequestSchema,
22
+ ListPromptsRequestSchema,
23
+ GetPromptRequestSchema
24
+ } = require('@modelcontextprotocol/sdk/types.js');
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const basePath = process.cwd();
30
+ const settings = { basePath };
31
+
32
+ // Lazy-load providers to avoid errors when files don't exist
33
+ function loadProvider(name) {
34
+ const providerPath = path.join(basePath, '.claude', 'providers', 'local', name + '.js');
35
+ if (fs.existsSync(providerPath)) return require(providerPath);
36
+ // Fallback to autopm path
37
+ const autopmPath = path.join(__dirname, '..', '..', 'providers', 'local', name + '.js');
38
+ if (fs.existsSync(autopmPath)) return require(autopmPath);
39
+ return null;
40
+ }
41
+
42
+ function readFile(relativePath) {
43
+ const fullPath = path.join(basePath, relativePath);
44
+ if (fs.existsSync(fullPath)) return fs.readFileSync(fullPath, 'utf8');
45
+ return null;
46
+ }
47
+
48
+ function readJSON(relativePath) {
49
+ const content = readFile(relativePath);
50
+ if (!content) return null;
51
+ try { return JSON.parse(content); } catch { return null; }
52
+ }
53
+
54
+ // ── Server Setup ──
55
+
56
+ const server = new Server(
57
+ { name: 'autopm', version: '1.0.0' },
58
+ { capabilities: { tools: {}, resources: {}, prompts: {} } }
59
+ );
60
+
61
+ // ── Tools ──
62
+
63
+ const TOOLS = [
64
+ { name: 'autopm_list_issues', description: 'List local issues with optional status filter', inputSchema: { type: 'object', properties: { status: { type: 'string', description: 'Filter: open, in_progress, closed' } } } },
65
+ { name: 'autopm_show_issue', description: 'Show issue details by ID', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Issue number' } }, required: ['id'] } },
66
+ { name: 'autopm_create_issue', description: 'Create a new local issue', inputSchema: { type: 'object', properties: { title: { type: 'string' }, labels: { type: 'array', items: { type: 'string' } }, body: { type: 'string' } }, required: ['title'] } },
67
+ { name: 'autopm_start_issue', description: 'Start working on an issue (set in_progress)', inputSchema: { type: 'object', properties: { id: { type: 'number' }, no_branch: { type: 'boolean' } }, required: ['id'] } },
68
+ { name: 'autopm_close_issue', description: 'Close an issue', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] } },
69
+ { name: 'autopm_list_epics', description: 'List epics with progress', inputSchema: { type: 'object', properties: { status: { type: 'string' } } } },
70
+ { name: 'autopm_show_epic', description: 'Show epic with tasks', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
71
+ { name: 'autopm_list_prds', description: 'List PRDs', inputSchema: { type: 'object', properties: { status: { type: 'string' } } } },
72
+ { name: 'autopm_show_prd', description: 'Show PRD details', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
73
+ { name: 'autopm_status', description: 'Project overview — counts, recent activity', inputSchema: { type: 'object', properties: {} } },
74
+ { name: 'autopm_learn', description: 'Save a project learning', inputSchema: { type: 'object', properties: { learning: { type: 'string' }, tags: { type: 'array', items: { type: 'string' } } }, required: ['learning'] } },
75
+ { name: 'autopm_recall', description: 'Get project learnings', inputSchema: { type: 'object', properties: { tag: { type: 'string' }, limit: { type: 'number' } } } },
76
+ { name: 'autopm_checkpoint', description: 'Create a project checkpoint', inputSchema: { type: 'object', properties: { description: { type: 'string' } }, required: ['description'] } }
77
+ ];
78
+
79
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
80
+
81
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
82
+ const { name, arguments: args } = request.params;
83
+
84
+ try {
85
+ switch (name) {
86
+ case 'autopm_list_issues': {
87
+ const provider = loadProvider('issue-list');
88
+ if (!provider) return text('Issue list provider not available');
89
+ const result = await provider.execute(args || {}, settings);
90
+ return text(JSON.stringify(result, null, 2));
91
+ }
92
+ case 'autopm_show_issue': {
93
+ const provider = loadProvider('issue-show');
94
+ if (!provider) return text('Issue show provider not available');
95
+ const result = await provider.execute(args, settings);
96
+ return text(JSON.stringify(result, null, 2));
97
+ }
98
+ case 'autopm_create_issue': {
99
+ const provider = loadProvider('issue-create');
100
+ if (!provider) return text('Issue create provider not available');
101
+ const result = await provider.execute(args, settings);
102
+ return text(JSON.stringify(result, null, 2));
103
+ }
104
+ case 'autopm_start_issue': {
105
+ const provider = loadProvider('issue-start');
106
+ if (!provider) return text('Issue start provider not available');
107
+ const result = await provider.execute(args, settings);
108
+ return text(JSON.stringify(result, null, 2));
109
+ }
110
+ case 'autopm_close_issue': {
111
+ const provider = loadProvider('issue-close');
112
+ if (!provider) return text('Issue close provider not available');
113
+ const result = await provider.execute(args, settings);
114
+ return text(JSON.stringify(result, null, 2));
115
+ }
116
+ case 'autopm_list_epics': {
117
+ const provider = loadProvider('epic-list');
118
+ if (!provider) return text('Epic list provider not available');
119
+ const result = await provider.execute(args || {}, settings);
120
+ return text(JSON.stringify(result, null, 2));
121
+ }
122
+ case 'autopm_show_epic': {
123
+ const provider = loadProvider('epic-show');
124
+ if (!provider) return text('Epic show provider not available');
125
+ const result = await provider.execute(args, settings);
126
+ return text(JSON.stringify(result, null, 2));
127
+ }
128
+ case 'autopm_list_prds': {
129
+ const provider = loadProvider('prd-list');
130
+ if (!provider) return text('PRD list provider not available');
131
+ const result = await provider.execute(args || {}, settings);
132
+ return text(JSON.stringify(result, null, 2));
133
+ }
134
+ case 'autopm_show_prd': {
135
+ const provider = loadProvider('prd-show');
136
+ if (!provider) return text('PRD show provider not available');
137
+ const result = await provider.execute(args, settings);
138
+ return text(JSON.stringify(result, null, 2));
139
+ }
140
+ case 'autopm_status': {
141
+ const issues = loadProvider('issue-list');
142
+ const epics = loadProvider('epic-list');
143
+ const prds = loadProvider('prd-list');
144
+ const status = {
145
+ issues: issues ? await issues.execute({}, settings) : { count: 0 },
146
+ epics: epics ? await epics.execute({}, settings) : { count: 0 },
147
+ prds: prds ? await prds.execute({}, settings) : { count: 0 }
148
+ };
149
+ // Add recent events
150
+ try {
151
+ const loggerPath = path.join(basePath, '.claude', 'lib', 'event-logger');
152
+ const { readEvents } = require(loggerPath);
153
+ status.recentEvents = readEvents(10, null, basePath);
154
+ } catch { status.recentEvents = []; }
155
+ return text(JSON.stringify(status, null, 2));
156
+ }
157
+ case 'autopm_learn': {
158
+ try {
159
+ const learningsPath = path.join(basePath, '.claude', 'pm', 'learnings.jsonl');
160
+ const dir = path.dirname(learningsPath);
161
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
162
+ const entry = { timestamp: new Date().toISOString(), type: 'learning', learning: args.learning, tags: args.tags || [] };
163
+ fs.appendFileSync(learningsPath, JSON.stringify(entry) + '\n');
164
+ try {
165
+ const loggerPath = path.join(basePath, '.claude', 'lib', 'event-logger');
166
+ const { logEvent } = require(loggerPath);
167
+ logEvent('learning.saved', { learning: args.learning, tags: args.tags || [] }, basePath);
168
+ } catch { /* best effort */ }
169
+ return text(JSON.stringify({ success: true, learning: args.learning }));
170
+ } catch (e) { return text(JSON.stringify({ error: e.message })); }
171
+ }
172
+ case 'autopm_recall': {
173
+ const learningsPath = path.join(basePath, '.claude', 'pm', 'learnings.jsonl');
174
+ if (!fs.existsSync(learningsPath)) return text(JSON.stringify({ learnings: [] }));
175
+ let entries = fs.readFileSync(learningsPath, 'utf8').trim().split('\n')
176
+ .map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
177
+ if (args && args.tag) entries = entries.filter(e => (e.tags || []).includes(args.tag));
178
+ const limit = (args && args.limit) || 20;
179
+ return text(JSON.stringify({ learnings: entries.slice(-limit).reverse() }));
180
+ }
181
+ case 'autopm_checkpoint': {
182
+ try {
183
+ const { execSync } = require('child_process');
184
+ const cpDir = path.join(basePath, '.claude', 'pm', 'checkpoints');
185
+ if (!fs.existsSync(cpDir)) fs.mkdirSync(cpDir, { recursive: true });
186
+ const now = new Date().toISOString();
187
+ let gitInfo = {};
188
+ try {
189
+ gitInfo.branch = execSync('git branch --show-current', { encoding: 'utf8', cwd: basePath }).trim();
190
+ gitInfo.hash = execSync('git rev-parse --short HEAD', { encoding: 'utf8', cwd: basePath }).trim();
191
+ gitInfo.clean = !execSync('git status --porcelain', { encoding: 'utf8', cwd: basePath }).trim();
192
+ } catch { /* not a git repo */ }
193
+ // Count PM artifacts to match CLI checkpoint format
194
+ const counts = {};
195
+ for (const [key, dir] of [['issues', 'issues'], ['epics', 'epics'], ['prds', 'prds']]) {
196
+ const d = path.join(basePath, '.claude', dir);
197
+ try { counts[key] = fs.readdirSync(d).filter(f => f.endsWith('.md')).length; } catch { counts[key] = 0; }
198
+ }
199
+ // Load recent learnings
200
+ let learnings = [];
201
+ try {
202
+ const lPath = path.join(basePath, '.claude', 'pm', 'learnings.jsonl');
203
+ if (fs.existsSync(lPath)) {
204
+ learnings = fs.readFileSync(lPath, 'utf8').trim().split('\n')
205
+ .map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean).slice(-5);
206
+ }
207
+ } catch { /* ignore */ }
208
+ const checkpoint = { timestamp: now, description: args.description, git: gitInfo, counts, learnings, config_snapshot: null };
209
+ fs.writeFileSync(path.join(cpDir, now.replace(/[:.]/g, '-') + '.json'), JSON.stringify(checkpoint, null, 2));
210
+ return text(JSON.stringify({ success: true, checkpoint }));
211
+ } catch (e) { return text(JSON.stringify({ error: e.message })); }
212
+ }
213
+ default:
214
+ return text(`Unknown tool: ${name}`);
215
+ }
216
+ } catch (e) {
217
+ return text(JSON.stringify({ error: e.message }));
218
+ }
219
+ });
220
+
221
+ // ── Resources ──
222
+
223
+ const RESOURCES = [
224
+ { uri: 'autopm://config', name: 'Project Config', description: 'Current .claude/config.json', mimeType: 'application/json' },
225
+ { uri: 'autopm://agents', name: 'Agent Registry', description: 'Loaded agents from agent-registry.xml', mimeType: 'text/xml' },
226
+ { uri: 'autopm://events', name: 'Recent Events', description: 'Last 50 events from events.jsonl', mimeType: 'application/json' },
227
+ { uri: 'autopm://learnings', name: 'Project Learnings', description: 'All learnings from learnings.jsonl', mimeType: 'application/json' },
228
+ { uri: 'autopm://test-plan', name: 'Test Plan', description: 'Current test plan', mimeType: 'text/markdown' }
229
+ ];
230
+
231
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: RESOURCES }));
232
+
233
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
234
+ const { uri } = request.params;
235
+
236
+ switch (uri) {
237
+ case 'autopm://config':
238
+ return resource(uri, readFile('.claude/config.json') || '{}', 'application/json');
239
+ case 'autopm://agents':
240
+ return resource(uri, readFile('.claude/agents/agent-registry.xml') || '<agent-registry/>', 'text/xml');
241
+ case 'autopm://events': {
242
+ try {
243
+ const loggerPath = path.join(basePath, '.claude', 'lib', 'event-logger');
244
+ const { readEvents } = require(loggerPath);
245
+ return resource(uri, JSON.stringify(readEvents(50, null, basePath), null, 2), 'application/json');
246
+ } catch { return resource(uri, '[]', 'application/json'); }
247
+ }
248
+ case 'autopm://learnings': {
249
+ const content = readFile('.claude/pm/learnings.jsonl');
250
+ if (!content) return resource(uri, '[]', 'application/json');
251
+ const entries = content.trim().split('\n').map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
252
+ return resource(uri, JSON.stringify(entries, null, 2), 'application/json');
253
+ }
254
+ case 'autopm://test-plan':
255
+ return resource(uri, readFile('.claude/pm/test-plan.md') || 'No test plan. Run /pm:test-plan to generate.', 'text/markdown');
256
+ default:
257
+ throw new Error(`Unknown resource: ${uri}`);
258
+ }
259
+ });
260
+
261
+ // ── Prompts ──
262
+
263
+ const PROMPTS = [
264
+ { name: 'autopm_issue_template', description: 'Template for creating a new issue' },
265
+ { name: 'autopm_prd_template', description: 'Template for creating a new PRD' },
266
+ { name: 'autopm_epic_template', description: 'Template for creating a new epic' }
267
+ ];
268
+
269
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
270
+
271
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
272
+ const { name } = request.params;
273
+
274
+ try {
275
+ const templateReaderPath = path.join(basePath, '.claude', 'lib', 'template-reader');
276
+ const { readTemplate, generateMarkdown, resolveTemplatePath } = require(templateReaderPath);
277
+
278
+ const templateMap = {
279
+ autopm_issue_template: 'issue.xml',
280
+ autopm_prd_template: 'prd.xml',
281
+ autopm_epic_template: 'epic.xml'
282
+ };
283
+
284
+ const templateFile = templateMap[name];
285
+ if (!templateFile) throw new Error(`Unknown prompt: ${name}`);
286
+
287
+ const templatePath = resolveTemplatePath(templateFile, basePath);
288
+ const template = readTemplate(templatePath);
289
+ const markdown = generateMarkdown(template, {});
290
+
291
+ return {
292
+ messages: [{
293
+ role: 'user',
294
+ content: [{ type: 'text', text: `Use this template to create a new ${templateFile.replace('.xml', '')}:\n\n${markdown}` }]
295
+ }]
296
+ };
297
+ } catch (e) {
298
+ return {
299
+ messages: [{
300
+ role: 'user',
301
+ content: [{ type: 'text', text: `Template not available: ${e.message}. Run autopm install first.` }]
302
+ }]
303
+ };
304
+ }
305
+ });
306
+
307
+ // ── Helpers ──
308
+
309
+ function text(content) {
310
+ return { content: [{ type: 'text', text: content }] };
311
+ }
312
+
313
+ function resource(uri, content, mimeType) {
314
+ return { contents: [{ uri, text: content, mimeType }] };
315
+ }
316
+
317
+ // ── Start ──
318
+
319
+ async function main() {
320
+ const transport = new StdioServerTransport();
321
+ await server.connect(transport);
322
+ console.error('AutoPM MCP Server running on stdio');
323
+ }
324
+
325
+ main().catch(console.error);
@@ -132,8 +132,7 @@ function generateEpicFlowDiagram() {
132
132
  }
133
133
  } catch {}
134
134
  if (epics.length === 0 && prds.length === 0) {
135
- lines.push(' N[No epics or PRDs found]');
136
- return lines.join('\n');
135
+ return { mermaid: '', hasData: false };
137
136
  }
138
137
  let nodeId = 0;
139
138
  for (const prd of prds) {
@@ -159,7 +158,7 @@ function generateEpicFlowDiagram() {
159
158
  lines.push(` ${eid} --> ${tid}["${t.name} ${t.icon}"]`);
160
159
  }
161
160
  }
162
- return lines.join('\n');
161
+ return { mermaid: lines.join('\n'), hasData: true };
163
162
  }
164
163
 
165
164
  function generatePluginGraph() {
@@ -263,8 +262,10 @@ function getProjectDiagrams() {
263
262
  }
264
263
 
265
264
  function getDiagramsData() {
265
+ const epic = generateEpicFlowDiagram();
266
266
  return {
267
- epicFlow: generateEpicFlowDiagram(),
267
+ epicFlow: epic.mermaid,
268
+ epicFlowHasData: epic.hasData,
268
269
  projectDiagrams: getProjectDiagrams(),
269
270
  pluginGraph: generatePluginGraph(),
270
271
  agentTree: generateAgentTree()
@@ -364,6 +365,42 @@ function renderHTML() {
364
365
  .toast.ok { background: #238636; }
365
366
  .toast.err { background: #da3633; }
366
367
  footer { margin-top: 24px; text-align: center; color: #484f58; font-size: 12px; }
368
+ /* Diagram cards */
369
+ .diagram-card { position: relative; }
370
+ .diagram-card .diagram-toolbar { display: flex; gap: 6px; position: absolute; top: 12px; right: 12px; z-index: 2; }
371
+ .diagram-card .diagram-toolbar button {
372
+ background: #21262d; color: #8b949e; border: 1px solid #30363d;
373
+ padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
374
+ }
375
+ .diagram-card .diagram-toolbar button:hover { background: #30363d; color: #c9d1d9; }
376
+ .diagram-card .mermaid-wrap { overflow: auto; max-height: 400px; cursor: pointer; }
377
+ .diagram-card .mermaid-wrap:hover { outline: 1px solid #30363d; border-radius: 4px; }
378
+ /* Fullscreen modal */
379
+ .diagram-modal {
380
+ display: none; position: fixed; inset: 0; z-index: 1000;
381
+ background: rgba(0,0,0,0.85); justify-content: center; align-items: center;
382
+ }
383
+ .diagram-modal.open { display: flex; }
384
+ .diagram-modal .modal-inner {
385
+ background: #161b22; border: 1px solid #30363d; border-radius: 12px;
386
+ width: 95vw; height: 92vh; display: flex; flex-direction: column; position: relative;
387
+ }
388
+ .diagram-modal .modal-header {
389
+ display: flex; justify-content: space-between; align-items: center;
390
+ padding: 12px 20px; border-bottom: 1px solid #30363d;
391
+ }
392
+ .diagram-modal .modal-header h3 { color: #c9d1d9; font-size: 14px; }
393
+ .diagram-modal .modal-header .modal-actions { display: flex; gap: 8px; }
394
+ .diagram-modal .modal-header button {
395
+ background: #21262d; color: #8b949e; border: 1px solid #30363d;
396
+ padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px;
397
+ }
398
+ .diagram-modal .modal-header button:hover { background: #30363d; color: #c9d1d9; }
399
+ .diagram-modal .modal-body {
400
+ flex: 1; overflow: auto; padding: 20px; display: flex;
401
+ justify-content: center; align-items: flex-start;
402
+ }
403
+ .diagram-modal .modal-body svg { max-width: 100%; height: auto; }
367
404
  </style>
368
405
  </head>
369
406
  <body>
@@ -494,15 +531,33 @@ function renderHTML() {
494
531
 
495
532
  <!-- Diagrams Tab -->
496
533
  <div id="tab-diagrams" class="tab-content">
497
- <div class="grid" style="grid-template-columns: 1fr 1fr;">
498
- <div class="card">
499
- <h3>Epic Flow</h3>
534
+ <div id="diagram-epic-card" class="card diagram-card" style="display:none;">
535
+ <h3>Epic Flow <span style="color:#8b949e;font-size:11px;font-weight:normal;margin-left:8px;">PRD → Epic → Tasks workflow</span></h3>
536
+ <div class="diagram-toolbar">
537
+ <button onclick="downloadMermaid('diagram-epic')">↓ .mmd</button>
538
+ <button onclick="openDiagramModal('diagram-epic','Epic Flow')">⤢ Fullscreen</button>
539
+ </div>
540
+ <div class="mermaid-wrap" onclick="openDiagramModal('diagram-epic','Epic Flow')">
500
541
  <pre class="mermaid" id="diagram-epic"></pre>
501
542
  </div>
502
543
  </div>
503
544
  <div id="project-diagrams"></div>
504
545
  </div>
505
546
 
547
+ <!-- Diagram fullscreen modal -->
548
+ <div class="diagram-modal" id="diagram-modal" role="dialog" aria-modal="true" aria-labelledby="modal-diagram-title" onclick="if(event.target===this)closeDiagramModal()">
549
+ <div class="modal-inner">
550
+ <div class="modal-header">
551
+ <h3 id="modal-diagram-title"></h3>
552
+ <div class="modal-actions">
553
+ <button onclick="downloadMermaid(currentModalDiagram)">↓ Download .mmd</button>
554
+ <button onclick="closeDiagramModal()">✕ Close</button>
555
+ </div>
556
+ </div>
557
+ <div class="modal-body" id="modal-diagram-body"></div>
558
+ </div>
559
+ </div>
560
+
506
561
  <!-- Tests Tab -->
507
562
  <div id="tab-tests" class="tab-content">
508
563
  <div class="card">
@@ -542,26 +597,68 @@ function showTab(name, btn) {
542
597
  }
543
598
 
544
599
  let diagramsRendered = false;
600
+ const diagramSources = {};
601
+ const diagramNames = {};
602
+ let currentModalDiagram = null;
603
+
545
604
  function renderMermaid() {
546
605
  if (diagramsRendered) return;
547
606
  fetch('/api/diagrams', { headers: { 'Authorization': 'Bearer ' + TOKEN } })
548
607
  .then(r => r.json())
549
608
  .then(data => {
550
- document.getElementById('diagram-epic').textContent = data.epicFlow;
609
+ // Epic Flow — only show if there are actual epics/PRDs
610
+ const epicCard = document.getElementById('diagram-epic-card');
611
+ const epicEl = document.getElementById('diagram-epic');
612
+ if (data.epicFlowHasData) {
613
+ epicCard.style.display = '';
614
+ epicEl.className = 'mermaid';
615
+ epicEl.textContent = data.epicFlow;
616
+ diagramSources['diagram-epic'] = data.epicFlow;
617
+ diagramNames['diagram-epic'] = 'epic-flow';
618
+ } else {
619
+ epicCard.style.display = 'none';
620
+ epicEl.className = '';
621
+ epicEl.textContent = '';
622
+ }
551
623
  document.getElementById('diagram-plugins').textContent = data.pluginGraph;
552
624
  document.getElementById('diagram-agents').textContent = data.agentTree;
625
+ diagramSources['diagram-plugins'] = data.pluginGraph;
626
+ diagramSources['diagram-agents'] = data.agentTree;
627
+ diagramNames['diagram-plugins'] = 'plugins';
628
+ diagramNames['diagram-agents'] = 'agents';
553
629
  // Render project diagrams dynamically
554
630
  const container = document.getElementById('project-diagrams');
555
631
  container.innerHTML = '';
556
632
  if (data.projectDiagrams && data.projectDiagrams.length > 0) {
557
- data.projectDiagrams.forEach(d => {
633
+ data.projectDiagrams.forEach((d, i) => {
634
+ const id = 'proj-diagram-' + i;
635
+ diagramSources[id] = d.content;
636
+ diagramNames[id] = d.name;
558
637
  const card = document.createElement('div');
559
- card.className = 'card';
560
- card.innerHTML = '<h3>' + esc(d.name) + '</h3>';
638
+ card.className = 'card diagram-card';
639
+ const h3 = document.createElement('h3');
640
+ h3.textContent = d.name;
641
+ card.appendChild(h3);
642
+ const toolbar = document.createElement('div');
643
+ toolbar.className = 'diagram-toolbar';
644
+ const dlBtn = document.createElement('button');
645
+ dlBtn.textContent = '↓ .mmd';
646
+ dlBtn.onclick = function(e) { e.stopPropagation(); downloadMermaid(id); };
647
+ const fsBtn = document.createElement('button');
648
+ fsBtn.textContent = '⤢ Fullscreen';
649
+ fsBtn.onclick = function(e) { e.stopPropagation(); openDiagramModal(id, d.name); };
650
+ toolbar.appendChild(dlBtn);
651
+ toolbar.appendChild(fsBtn);
652
+ card.appendChild(toolbar);
653
+ const wrap = document.createElement('div');
654
+ wrap.className = 'mermaid-wrap';
655
+ wrap.onclick = function() { openDiagramModal(id, d.name); };
561
656
  const pre = document.createElement('pre');
562
657
  pre.className = 'mermaid';
658
+ pre.id = id;
563
659
  pre.textContent = d.content;
564
- card.appendChild(pre);
660
+ wrap.appendChild(pre);
661
+ card.appendChild(wrap);
565
662
  container.appendChild(card);
566
663
  });
567
664
  } else {
@@ -586,6 +683,59 @@ function renderMermaid() {
586
683
  });
587
684
  }
588
685
 
686
+ function openDiagramModal(diagramId, title) {
687
+ currentModalDiagram = diagramId;
688
+ document.getElementById('modal-diagram-title').textContent = title || diagramId;
689
+ const body = document.getElementById('modal-diagram-body');
690
+ body.textContent = '';
691
+ const source = diagramSources[diagramId];
692
+ if (source) {
693
+ const pre = document.createElement('pre');
694
+ pre.className = 'mermaid';
695
+ pre.textContent = source;
696
+ body.appendChild(pre);
697
+ if (typeof mermaid !== 'undefined' && mermaid.run) {
698
+ mermaid.run({ nodes: body.querySelectorAll('.mermaid') });
699
+ }
700
+ } else {
701
+ body.textContent = 'Diagram not available.';
702
+ }
703
+ const modal = document.getElementById('diagram-modal');
704
+ modal.classList.add('open');
705
+ modalPrevFocus = document.activeElement;
706
+ const closeBtn = modal.querySelector('.modal-actions button:last-child');
707
+ if (closeBtn) closeBtn.focus();
708
+ document.addEventListener('keydown', modalEscHandler);
709
+ }
710
+
711
+ let modalPrevFocus = null;
712
+ function closeDiagramModal() {
713
+ document.getElementById('diagram-modal').classList.remove('open');
714
+ document.removeEventListener('keydown', modalEscHandler);
715
+ currentModalDiagram = null;
716
+ if (modalPrevFocus) { modalPrevFocus.focus(); modalPrevFocus = null; }
717
+ }
718
+
719
+ function modalEscHandler(e) { if (e.key === 'Escape') closeDiagramModal(); }
720
+
721
+ function downloadMermaid(diagramId) {
722
+ const src = diagramSources[diagramId];
723
+ if (!src) return;
724
+ const label = diagramNames[diagramId] || diagramId.replace('proj-diagram-', 'diagram-').replace('diagram-', '');
725
+ const safeName = label.replace(/[^a-zA-Z0-9_-]/g, '_') + '.mmd';
726
+ const blob = new Blob([src], { type: 'text/plain' });
727
+ const a = document.createElement('a');
728
+ const url = URL.createObjectURL(blob);
729
+ a.href = url;
730
+ a.download = safeName;
731
+ a.style.display = 'none';
732
+ document.body.appendChild(a);
733
+ try { a.click(); } finally {
734
+ document.body.removeChild(a);
735
+ setTimeout(function() { URL.revokeObjectURL(url); }, 100);
736
+ }
737
+ }
738
+
589
739
  function renderTestPlan(md) {
590
740
  if (!md) { document.getElementById('test-plan-content').textContent = 'No test plan found. Run /pm:test-plan to generate.'; return; }
591
741
  const lines = md.split('\\n');
@@ -963,7 +1113,22 @@ function addEnvSuggestion(key, val, hint) {
963
1113
  addEnvRow(key, val);
964
1114
  }
965
1115
 
1116
+ // Pause auto-refresh while user is editing MCP or env forms
1117
+ let editingLock = false;
1118
+ document.addEventListener('focusin', function(e) {
1119
+ if (e.target.closest('#mcp-list, #env-list, .mcp-entry, .env-row')) editingLock = true;
1120
+ });
1121
+ document.addEventListener('focusout', function(e) {
1122
+ if (e.target.closest('#mcp-list, #env-list, .mcp-entry, .env-row')) {
1123
+ setTimeout(function() {
1124
+ const active = document.activeElement;
1125
+ if (!active || !active.closest('#mcp-list, #env-list, .mcp-entry, .env-row')) editingLock = false;
1126
+ }, 200);
1127
+ }
1128
+ });
1129
+
966
1130
  async function refresh() {
1131
+ if (editingLock) return;
967
1132
  try {
968
1133
  const data = await api('GET', '/api/status');
969
1134
  loadStatus(data);
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PM Diagram List/Show Script
5
+ *
6
+ * Lists diagrams from .claude/pm/diagrams/*.mmd with metadata.
7
+ * With --show <name>: displays diagram content.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const DIAGRAMS_DIR = path.join('.claude', 'pm', 'diagrams');
14
+
15
+ function findDiagrams() {
16
+ if (!fs.existsSync(DIAGRAMS_DIR)) {
17
+ return [];
18
+ }
19
+ return fs.readdirSync(DIAGRAMS_DIR)
20
+ .filter(f => f.endsWith('.mmd'))
21
+ .map(f => f.replace('.mmd', ''));
22
+ }
23
+
24
+ function loadMeta(name) {
25
+ const metaPath = path.join(DIAGRAMS_DIR, `${name}.meta.json`);
26
+ if (!fs.existsSync(metaPath)) {
27
+ return { name, type: 'unknown', created: '-', updated: '-' };
28
+ }
29
+ try {
30
+ const raw = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
31
+ return {
32
+ name: raw.name || name,
33
+ type: raw.type || 'unknown',
34
+ created: (raw.created || '-').slice(0, 10),
35
+ updated: (raw.updated || '-').slice(0, 10)
36
+ };
37
+ } catch {
38
+ return { name, type: 'unknown', created: '-', updated: '-' };
39
+ }
40
+ }
41
+
42
+ function listDiagrams() {
43
+ const names = findDiagrams();
44
+ if (names.length === 0) {
45
+ console.log('No diagrams found.');
46
+ console.log('Create one with: /pm:diagram-new <name>');
47
+ return;
48
+ }
49
+
50
+ const rows = names.map(loadMeta);
51
+
52
+ console.log('## Project Diagrams\n');
53
+ console.log('| Name | Type | Created | Updated |');
54
+ console.log('|------|------|---------|---------|');
55
+ for (const r of rows) {
56
+ const escapedName = r.name.replace(/\|/g, '\\|');
57
+ const escapedType = (r.type || '').replace(/\|/g, '\\|');
58
+ console.log(`| ${escapedName} | ${escapedType} | ${r.created} | ${r.updated} |`);
59
+ }
60
+ console.log(`\nTotal: ${rows.length} diagram${rows.length === 1 ? '' : 's'}`);
61
+ console.log('View in dashboard: /pm:dashboard → Diagrams tab');
62
+ }
63
+
64
+ function showDiagram(name) {
65
+ const mmdPath = path.join(DIAGRAMS_DIR, `${name}.mmd`);
66
+ if (!fs.existsSync(mmdPath)) {
67
+ console.error(`❌ Diagram "${name}" not found. Run: /pm:diagram-new ${name}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ const content = fs.readFileSync(mmdPath, 'utf8');
72
+ const meta = loadMeta(name);
73
+
74
+ console.log(`## Diagram: ${meta.name} (${meta.type})\n`);
75
+ console.log('```mermaid');
76
+ console.log(content.trim());
77
+ console.log('```');
78
+ }
79
+
80
+ // Parse args
81
+ const args = process.argv.slice(2);
82
+ const showIdx = args.indexOf('--show');
83
+
84
+ if (showIdx !== -1 && args[showIdx + 1]) {
85
+ const name = args[showIdx + 1];
86
+ const safeName = path.basename(name || '');
87
+ if (!safeName || !/^[a-z0-9_-]+$/i.test(safeName)) {
88
+ console.log('Invalid diagram name. Use alphanumeric, hyphens, underscores only.');
89
+ process.exit(1);
90
+ }
91
+ showDiagram(safeName);
92
+ } else {
93
+ listDiagrams();
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-autopm",
3
- "version": "3.21.0",
3
+ "version": "3.22.1",
4
4
  "description": "Autonomous Project Management Framework for Claude Code - Advanced AI-powered development automation",
5
5
  "main": "bin/autopm.js",
6
6
  "workspaces": [
@@ -126,6 +126,7 @@
126
126
  ],
127
127
  "dependencies": {
128
128
  "@anthropic-ai/sdk": "^0.67.0",
129
+ "@modelcontextprotocol/sdk": "^1.29.0",
129
130
  "@octokit/rest": "^22.0.0",
130
131
  "azure-devops-node-api": "^15.1.1",
131
132
  "chalk": "4.1.2",