@promptprojectmanager/mcp-server 4.6.1 → 4.6.2

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/index.js CHANGED
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getWatcherDir,
4
+ isWatcherRunning,
5
+ readPid,
6
+ readStatus,
7
+ removePid,
8
+ writeStatus
9
+ } from "./chunk-PY22KZ7Z.js";
2
10
 
3
11
  // src/index.ts
4
12
  import minimist from "minimist";
@@ -101,159 +109,168 @@ function parseWatcherStartArgs(args) {
101
109
  };
102
110
  }
103
111
 
104
- // src/watcher/watcher_state.ts
105
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
106
- import { join } from "path";
107
- function getWatcherDir(workingDirectory) {
108
- const dir = join(workingDirectory, ".ppm", "yolo");
109
- if (!existsSync(dir)) {
110
- mkdirSync(dir, { recursive: true });
112
+ // src/watcher/watcher_controller.ts
113
+ import { spawn } from "child_process";
114
+ import { fileURLToPath } from "url";
115
+ import { dirname, join } from "path";
116
+ function startWatcher(baseConfig, args) {
117
+ const workingDirectory = args?.workingDirectory ?? baseConfig.workingDirectory;
118
+ if (isWatcherRunning(workingDirectory)) {
119
+ const existingPid = readPid(workingDirectory);
120
+ const status = readStatus(workingDirectory);
121
+ return {
122
+ success: true,
123
+ message: `Watcher already running (PID: ${existingPid})`,
124
+ pid: existingPid,
125
+ alreadyRunning: true,
126
+ config: status?.config
127
+ };
128
+ }
129
+ const config = {
130
+ projectSlug: baseConfig.projectSlug,
131
+ projectToken: baseConfig.projectToken,
132
+ convexUrl: baseConfig.convexUrl,
133
+ pollIntervalMs: args?.pollIntervalMs ?? 3e4,
134
+ maxParallel: args?.maxParallel ?? 1,
135
+ ticketTimeout: args?.ticketTimeout ?? 18e5,
136
+ enableNotifications: args?.enableNotifications ?? true,
137
+ workingDirectory
138
+ };
139
+ getWatcherDir(workingDirectory);
140
+ const daemonPath = getDaemonPath();
141
+ const configJson = JSON.stringify(config);
142
+ const configB64 = Buffer.from(configJson).toString("base64");
143
+ try {
144
+ const child = spawn("node", [daemonPath, "--config", configB64], {
145
+ detached: true,
146
+ stdio: ["ignore", "ignore", "ignore"],
147
+ cwd: workingDirectory
148
+ });
149
+ child.unref();
150
+ const pid = child.pid;
151
+ if (!pid) {
152
+ return {
153
+ success: false,
154
+ message: "Failed to spawn daemon - no PID returned"
155
+ };
156
+ }
157
+ const configWithoutToken = {
158
+ projectSlug: config.projectSlug,
159
+ convexUrl: config.convexUrl,
160
+ pollIntervalMs: config.pollIntervalMs,
161
+ maxParallel: config.maxParallel,
162
+ ticketTimeout: config.ticketTimeout,
163
+ enableNotifications: config.enableNotifications,
164
+ workingDirectory: config.workingDirectory
165
+ };
166
+ return {
167
+ success: true,
168
+ message: `Watcher started (PID: ${pid})`,
169
+ pid,
170
+ alreadyRunning: false,
171
+ config: configWithoutToken
172
+ };
173
+ } catch (error) {
174
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
175
+ return {
176
+ success: false,
177
+ message: `Failed to start watcher: ${errorMessage}`
178
+ };
111
179
  }
112
- return dir;
113
- }
114
- function getStatusFile(workingDirectory) {
115
- return join(getWatcherDir(workingDirectory), "status.json");
116
180
  }
117
- function writeStatus(workingDirectory, status) {
118
- const statusFile = getStatusFile(workingDirectory);
119
- writeFileSync(statusFile, JSON.stringify(status, null, 2), "utf-8");
181
+ function getDaemonPath() {
182
+ const currentFile = fileURLToPath(import.meta.url);
183
+ const currentDir = dirname(currentFile);
184
+ return join(currentDir, "watcher", "watcher_daemon.js");
120
185
  }
121
- function readStatus(workingDirectory) {
122
- const statusFile = getStatusFile(workingDirectory);
123
- if (!existsSync(statusFile)) {
124
- return void 0;
186
+ function stopWatcher(workingDirectory) {
187
+ const pid = readPid(workingDirectory);
188
+ if (!pid) {
189
+ return {
190
+ success: true,
191
+ message: "No watcher running (no PID file)",
192
+ wasRunning: false
193
+ };
194
+ }
195
+ if (!isWatcherRunning(workingDirectory)) {
196
+ removePid(workingDirectory);
197
+ return {
198
+ success: true,
199
+ message: "Watcher was not running (stale PID file cleaned up)",
200
+ wasRunning: false
201
+ };
125
202
  }
126
203
  try {
127
- const content = readFileSync(statusFile, "utf-8");
128
- return JSON.parse(content);
129
- } catch {
130
- return void 0;
204
+ process.kill(pid, "SIGTERM");
205
+ const status = readStatus(workingDirectory);
206
+ if (status) {
207
+ writeStatus(workingDirectory, {
208
+ ...status,
209
+ state: "stopped"
210
+ });
211
+ }
212
+ removePid(workingDirectory);
213
+ return {
214
+ success: true,
215
+ message: `Watcher stopped (PID: ${pid})`,
216
+ wasRunning: true
217
+ };
218
+ } catch (error) {
219
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
220
+ removePid(workingDirectory);
221
+ return {
222
+ success: false,
223
+ message: `Failed to stop watcher: ${errorMessage}`,
224
+ wasRunning: true
225
+ };
131
226
  }
132
227
  }
133
-
134
- // src/watcher/watcher_orchestrator.ts
135
- function buildOrchestratorInstructions(config, pendingTickets) {
136
- const pollIntervalSec = Math.round(config.pollIntervalMs / 1e3);
137
- return `# \u{1F916} YOLO Orchestrator Active
138
-
139
- You are now the **YOLO Orchestrator** for project \`${config.projectSlug}\`.
140
-
141
- ## \u26A0\uFE0F CRITICAL RULE: STRICT SUB-AGENT MODE
142
-
143
- **You are the ORCHESTRATOR, not the executor.**
144
-
145
- You MUST NEVER execute ticket work directly. ALL ticket execution MUST go through Task sub-agents.
146
-
147
- Your ONLY jobs are:
148
- 1. Poll for YOLO tickets
149
- 2. Claim tickets (transition to "working" status)
150
- 3. Spawn Task sub-agents to execute them
151
- 4. Report results when sub-agents complete
152
-
153
- **FORBIDDEN**: Doing the ticket work yourself (no web searches, no code writing, no file edits for tickets)
154
- **REQUIRED**: Always use the Task tool to spawn a sub-agent for EVERY ticket
155
-
156
- ---
157
-
158
- ## Current Configuration
159
- | Setting | Value |
160
- |---------|-------|
161
- | Poll Interval | ${pollIntervalSec}s |
162
- | Max Parallel | ${config.maxParallel} |
163
- | Ticket Timeout | ${Math.round(config.ticketTimeout / 1e3 / 60)} minutes |
164
- | Working Directory | ${config.workingDirectory} |
165
-
166
- ## Pending YOLO Tickets
167
- ${pendingTickets.length > 0 ? formatPendingTickets(pendingTickets) : "_No tickets with YOLO flag currently in queue._"}
168
-
169
- ${pendingTickets.length > 0 ? `
170
- ## \u{1F4E5} Execute These Tickets NOW
171
-
172
- For EACH pending ticket, you MUST:
173
-
174
- ### Step 1: Claim the ticket
175
- \`\`\`
176
- tickets_work with ticketSlug: "{slug}"
177
- \`\`\`
178
-
179
- ### Step 2: Spawn a Task sub-agent (REQUIRED - DO NOT SKIP)
180
- \`\`\`
181
- Task tool with:
182
- description: "Execute YOLO ticket {slug}"
183
- subagent_type: "general-purpose"
184
- prompt: [use the template below]
185
- run_in_background: ${config.maxParallel > 1 ? "true" : "false"}
186
- \`\`\`
187
-
188
- ### Sub-Agent Prompt Template (copy this exactly):
189
-
190
- \`\`\`
191
- You are executing a YOLO ticket autonomously.
192
-
193
- ## Ticket: {ticketSlug}
194
-
195
- {ticketContent}
196
-
197
- ---
198
-
199
- ## Your Instructions
200
-
201
- 1. Execute the task described above
202
- 2. When complete, close the ticket: tickets_close with ticketSlug: "{ticketSlug}"
203
- 3. If blocked, update the ticket: tickets_update with your findings
204
- 4. Return a brief summary of what you accomplished
205
- \`\`\`
206
-
207
- ### Step 3: Report the result
208
- After each sub-agent completes, report:
209
- - \u2705 Success: what was done
210
- - \u274C Failure: what went wrong
211
-
212
- ---
213
-
214
- **START NOW**: Claim and spawn sub-agents for all ${pendingTickets.length} pending ticket(s) above.
215
- ` : `
216
- ## \u{1F504} No Tickets Ready
217
-
218
- No YOLO tickets are currently pending. Options:
219
- - Say **"poll"** to check for new tickets
220
- - Say **"stop"** to exit orchestrator mode
221
- `}
222
-
223
- ## Commands
224
- | Command | Action |
225
- |---------|--------|
226
- | "poll" / "check" | Refresh the ticket list |
227
- | "status" | Show orchestrator status |
228
- | "stop" / "exit" | Stop the orchestrator |
229
-
230
- ---
231
-
232
- _Orchestrator started at ${(/* @__PURE__ */ new Date()).toISOString()}_`;
233
- }
234
- function formatPendingTickets(tickets) {
235
- return tickets.map((t, i) => {
236
- const num = t.ticketNumber ? `#${t.ticketNumber}` : "";
237
- const title = t.slug;
238
- const preview = t.content.slice(0, 150).replace(/\n/g, " ");
239
- return `### ${i + 1}. ${num} ${title}
240
- > ${preview}${t.content.length > 150 ? "..." : ""}`;
241
- }).join("\n\n");
228
+ function getWatcherStatus(workingDirectory) {
229
+ const status = readStatus(workingDirectory);
230
+ const isRunning = isWatcherRunning(workingDirectory);
231
+ if (status?.state === "running" && !isRunning) {
232
+ const updatedStatus = {
233
+ ...status,
234
+ state: "stopped"
235
+ };
236
+ writeStatus(workingDirectory, updatedStatus);
237
+ return {
238
+ success: true,
239
+ status: updatedStatus,
240
+ formattedStatus: formatStatus(updatedStatus),
241
+ message: "Watcher died unexpectedly - status updated"
242
+ };
243
+ }
244
+ if (!status) {
245
+ return {
246
+ success: true,
247
+ message: "No watcher session found for this project",
248
+ formattedStatus: formatStatus(null)
249
+ };
250
+ }
251
+ return {
252
+ success: true,
253
+ status,
254
+ formattedStatus: formatStatus(status)
255
+ };
242
256
  }
243
- function formatOrchestratorStatus(status, pendingTickets) {
257
+ function formatStatus(status) {
244
258
  if (!status) {
245
- return `# YOLO Orchestrator Status
259
+ return `# YOLO Watcher Status
246
260
 
247
261
  **State**: Not started
248
262
 
249
- _Use \`tickets_yolo_start\` to begin orchestration._`;
263
+ _Use \`tickets_yolo_start\` to begin watching for YOLO tickets._`;
250
264
  }
251
265
  const lines = [
252
- "# YOLO Orchestrator Status",
266
+ "# YOLO Watcher Status",
253
267
  "",
254
268
  `**State**: ${status.state}`,
255
269
  `**Project**: ${status.projectSlug}`
256
270
  ];
271
+ if (status.pid) {
272
+ lines.push(`**Daemon PID**: ${status.pid}`);
273
+ }
257
274
  if (status.startedAt) {
258
275
  lines.push(`**Started**: ${status.startedAt}`);
259
276
  }
@@ -261,27 +278,38 @@ _Use \`tickets_yolo_start\` to begin orchestration._`;
261
278
  lines.push(`**Last Poll**: ${status.lastPollAt}`);
262
279
  }
263
280
  lines.push(`**Tickets Processed**: ${status.ticketsProcessed}`);
281
+ if (status.config) {
282
+ lines.push("");
283
+ lines.push("## Configuration");
284
+ lines.push(`- Poll Interval: ${status.config.pollIntervalMs / 1e3}s`);
285
+ lines.push(`- Max Parallel: ${status.config.maxParallel}`);
286
+ lines.push(`- Ticket Timeout: ${status.config.ticketTimeout / 1e3 / 60} min`);
287
+ lines.push(`- Notifications: ${status.config.enableNotifications ? "enabled" : "disabled"}`);
288
+ }
264
289
  if (status.currentlyExecuting && status.currentlyExecuting.length > 0) {
265
290
  lines.push("");
266
- lines.push("## Currently Executing (Sub-Agents)");
291
+ lines.push("## Currently Executing");
267
292
  for (const exec of status.currentlyExecuting) {
268
293
  const num = exec.ticketNumber ? `#${exec.ticketNumber}` : "";
269
294
  const elapsed = getElapsedTime(exec.startedAt);
270
295
  lines.push(`- **${num} ${exec.ticketSlug}** (${elapsed})`);
271
296
  }
272
297
  }
273
- lines.push("");
274
- lines.push("## Pending YOLO Tickets");
275
- if (pendingTickets.length > 0) {
276
- lines.push("");
277
- lines.push("**Action Required**: Spawn Task sub-agents for these tickets:");
298
+ if (status.completedTickets && status.completedTickets.length > 0) {
278
299
  lines.push("");
279
- for (const ticket of pendingTickets) {
280
- const num = ticket.ticketNumber ? `#${ticket.ticketNumber}` : "";
281
- lines.push(`- **${num} ${ticket.slug}**`);
300
+ lines.push("## Recently Completed");
301
+ for (const completed of status.completedTickets.slice(0, 10)) {
302
+ const num = completed.ticketNumber ? `#${completed.ticketNumber}` : "";
303
+ const emoji = completed.success ? "\u2705" : "\u274C";
304
+ const duration = formatDuration(completed.durationMs);
305
+ const summary = completed.summary ? ` - ${completed.summary.slice(0, 50)}` : "";
306
+ lines.push(`- ${emoji} **${num} ${completed.ticketSlug}** (${duration})${summary}`);
282
307
  }
283
- } else {
284
- lines.push("_No tickets pending_");
308
+ }
309
+ if (status.lastError) {
310
+ lines.push("");
311
+ lines.push("## Last Error");
312
+ lines.push(`\`${status.lastError}\``);
285
313
  }
286
314
  return lines.join("\n");
287
315
  }
@@ -295,87 +323,17 @@ function getElapsedTime(startedAt) {
295
323
  }
296
324
  return `${seconds}s`;
297
325
  }
298
-
299
- // src/watcher/watcher_controller.ts
300
- function startOrchestrator(baseConfig, args, pendingTickets) {
301
- const workingDirectory = args?.workingDirectory ?? baseConfig.workingDirectory;
302
- const orchestratorConfig = {
303
- projectSlug: baseConfig.projectSlug,
304
- pollIntervalMs: args?.pollIntervalMs ?? 3e4,
305
- maxParallel: args?.maxParallel ?? 1,
306
- ticketTimeout: args?.ticketTimeout ?? 18e5,
307
- workingDirectory
308
- };
309
- getWatcherDir(workingDirectory);
310
- const initialStatus = {
311
- state: "running",
312
- projectSlug: baseConfig.projectSlug,
313
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
314
- ticketsProcessed: 0,
315
- currentlyExecuting: [],
316
- config: {
317
- projectSlug: baseConfig.projectSlug,
318
- convexUrl: baseConfig.convexUrl,
319
- pollIntervalMs: orchestratorConfig.pollIntervalMs,
320
- maxParallel: orchestratorConfig.maxParallel,
321
- ticketTimeout: orchestratorConfig.ticketTimeout,
322
- enableNotifications: args?.enableNotifications ?? true,
323
- workingDirectory
324
- }
325
- };
326
- writeStatus(workingDirectory, initialStatus);
327
- const instructions = buildOrchestratorInstructions(orchestratorConfig, pendingTickets);
328
- return {
329
- success: true,
330
- message: "Orchestrator started",
331
- instructions,
332
- config: initialStatus.config,
333
- alreadyRunning: false
334
- };
335
- }
336
- function stopOrchestrator(workingDirectory) {
337
- const status = readStatus(workingDirectory);
338
- if (!status) {
339
- return {
340
- success: true,
341
- message: "No orchestrator session found",
342
- wasRunning: false
343
- };
344
- }
345
- if (status.state !== "running") {
346
- return {
347
- success: true,
348
- message: `Orchestrator was already ${status.state}`,
349
- wasRunning: false
350
- };
326
+ function formatDuration(ms) {
327
+ if (ms < 1e3) {
328
+ return `${ms}ms`;
351
329
  }
352
- writeStatus(workingDirectory, {
353
- ...status,
354
- state: "stopped"
355
- });
356
- return {
357
- success: true,
358
- message: "Orchestrator stopped",
359
- wasRunning: true
360
- };
361
- }
362
- function getOrchestratorStatus(workingDirectory, pendingTickets) {
363
- const status = readStatus(workingDirectory);
364
- const formattedStatus = formatOrchestratorStatus(status ?? null, pendingTickets);
365
- if (!status) {
366
- return {
367
- success: true,
368
- message: "No orchestrator session has been started for this project",
369
- pendingTickets,
370
- formattedStatus
371
- };
330
+ const seconds = Math.floor(ms / 1e3);
331
+ const minutes = Math.floor(seconds / 60);
332
+ if (minutes > 0) {
333
+ const remainingSeconds = seconds % 60;
334
+ return `${minutes}m ${remainingSeconds}s`;
372
335
  }
373
- return {
374
- success: true,
375
- status,
376
- pendingTickets,
377
- formattedStatus
378
- };
336
+ return `${seconds}s`;
379
337
  }
380
338
 
381
339
  // src/prompt-builder.ts
@@ -1624,31 +1582,30 @@ _Ticket content has been appended with your update._`
1624
1582
  const args = parseWatcherStartArgs(request.params.arguments);
1625
1583
  const workingDirectory = args.workingDirectory ?? process.cwd();
1626
1584
  try {
1627
- let pendingTickets = [];
1628
- try {
1629
- pendingTickets = await convexClient.query(
1630
- "mcp_tickets:listYoloTickets",
1631
- {
1632
- projectToken: config.projectToken,
1633
- projectSlug: ticketTool.projectSlug
1634
- }
1635
- );
1636
- } catch (queryError) {
1637
- console.error(`[MCP] Failed to query YOLO tickets:`, queryError);
1638
- }
1639
- const result = startOrchestrator(
1585
+ const result = startWatcher(
1640
1586
  {
1641
1587
  projectSlug: ticketTool.projectSlug,
1642
1588
  projectToken: config.projectToken,
1643
1589
  convexUrl: config.convexUrl,
1644
1590
  workingDirectory
1645
1591
  },
1646
- args,
1647
- pendingTickets
1592
+ args
1648
1593
  );
1649
- if (result.success && result.instructions) {
1594
+ if (result.success) {
1595
+ const configInfo = result.config ? `
1596
+
1597
+ **Configuration:**
1598
+ - Poll Interval: ${result.config.pollIntervalMs / 1e3}s
1599
+ - Max Parallel: ${result.config.maxParallel}
1600
+ - Ticket Timeout: ${result.config.ticketTimeout / 1e3 / 60} min
1601
+ - Working Directory: ${result.config.workingDirectory}` : "";
1602
+ const text = result.alreadyRunning ? `\u2139\uFE0F ${result.message}${configInfo}` : `\u2705 ${result.message}
1603
+
1604
+ The daemon is now polling for YOLO-flagged tickets and will spawn Claude CLI processes to execute them.
1605
+
1606
+ Logs: \`.ppm/yolo/logs/\`${configInfo}`;
1650
1607
  return {
1651
- content: [{ type: "text", text: result.instructions }]
1608
+ content: [{ type: "text", text }]
1652
1609
  };
1653
1610
  } else {
1654
1611
  return {
@@ -1660,19 +1617,21 @@ _Ticket content has been appended with your update._`
1660
1617
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1661
1618
  console.error(`[MCP] tickets_yolo_start error:`, error);
1662
1619
  return {
1663
- content: [{ type: "text", text: `Error starting orchestrator: ${errorMessage}` }],
1620
+ content: [{ type: "text", text: `Error starting watcher daemon: ${errorMessage}` }],
1664
1621
  isError: true
1665
1622
  };
1666
1623
  }
1667
1624
  } else if (ticketTool.type === "yolo_stop") {
1668
1625
  const workingDirectory = process.cwd();
1669
1626
  try {
1670
- const result = stopOrchestrator(workingDirectory);
1627
+ const result = stopWatcher(workingDirectory);
1671
1628
  if (result.success) {
1672
1629
  return {
1673
1630
  content: [{
1674
1631
  type: "text",
1675
- text: result.wasRunning ? `\u2705 Orchestrator stopped. You can stop processing tickets now.` : `\u2139\uFE0F ${result.message}`
1632
+ text: result.wasRunning ? `\u2705 ${result.message}
1633
+
1634
+ The daemon has been stopped. Any running ticket executions will complete.` : `\u2139\uFE0F ${result.message}`
1676
1635
  }]
1677
1636
  };
1678
1637
  } else {
@@ -1685,39 +1644,27 @@ _Ticket content has been appended with your update._`
1685
1644
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1686
1645
  console.error(`[MCP] tickets_yolo_stop error:`, error);
1687
1646
  return {
1688
- content: [{ type: "text", text: `Error stopping orchestrator: ${errorMessage}` }],
1647
+ content: [{ type: "text", text: `Error stopping watcher daemon: ${errorMessage}` }],
1689
1648
  isError: true
1690
1649
  };
1691
1650
  }
1692
1651
  } else if (ticketTool.type === "yolo_status") {
1693
1652
  const workingDirectory = process.cwd();
1694
1653
  try {
1695
- let pendingTickets = [];
1696
- try {
1697
- pendingTickets = await convexClient.query(
1698
- "mcp_tickets:listYoloTickets",
1699
- {
1700
- projectToken: config.projectToken,
1701
- projectSlug: ticketTool.projectSlug
1702
- }
1703
- );
1704
- } catch (queryError) {
1705
- console.error(`[MCP] Failed to query YOLO tickets:`, queryError);
1706
- }
1707
- const result = getOrchestratorStatus(workingDirectory, pendingTickets);
1654
+ const result = getWatcherStatus(workingDirectory);
1708
1655
  if (result.formattedStatus) {
1709
1656
  return {
1710
1657
  content: [{ type: "text", text: result.formattedStatus }]
1711
1658
  };
1712
1659
  }
1713
1660
  return {
1714
- content: [{ type: "text", text: `\u2139\uFE0F ${result.message ?? "No orchestrator status available"}` }]
1661
+ content: [{ type: "text", text: `\u2139\uFE0F ${result.message ?? "No watcher status available"}` }]
1715
1662
  };
1716
1663
  } catch (error) {
1717
1664
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1718
1665
  console.error(`[MCP] tickets_yolo_status error:`, error);
1719
1666
  return {
1720
- content: [{ type: "text", text: `Error getting orchestrator status: ${errorMessage}` }],
1667
+ content: [{ type: "text", text: `Error getting watcher status: ${errorMessage}` }],
1721
1668
  isError: true
1722
1669
  };
1723
1670
  }