@promptprojectmanager/mcp-server 4.5.0 → 4.5.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/chunk-POSCT6WJ.js +106 -0
- package/dist/chunk-POSCT6WJ.js.map +1 -0
- package/dist/index.js +360 -0
- package/dist/index.js.map +1 -1
- package/dist/watcher/watcher_daemon.d.ts +2 -0
- package/dist/watcher/watcher_daemon.js +340 -0
- package/dist/watcher/watcher_daemon.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/watcher/watcher_state.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
function getWatcherDir(workingDirectory) {
|
|
7
|
+
const dir = join(workingDirectory, ".ppm", "yolo");
|
|
8
|
+
if (!existsSync(dir)) {
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
function getPidFile(workingDirectory) {
|
|
14
|
+
return join(getWatcherDir(workingDirectory), "watcher.pid");
|
|
15
|
+
}
|
|
16
|
+
function getStatusFile(workingDirectory) {
|
|
17
|
+
return join(getWatcherDir(workingDirectory), "status.json");
|
|
18
|
+
}
|
|
19
|
+
function writePid(workingDirectory, pid) {
|
|
20
|
+
const pidFile = getPidFile(workingDirectory);
|
|
21
|
+
writeFileSync(pidFile, pid.toString(), "utf-8");
|
|
22
|
+
}
|
|
23
|
+
function readPid(workingDirectory) {
|
|
24
|
+
const pidFile = getPidFile(workingDirectory);
|
|
25
|
+
if (!existsSync(pidFile)) {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(pidFile, "utf-8").trim();
|
|
30
|
+
const pid = parseInt(content, 10);
|
|
31
|
+
return isNaN(pid) ? void 0 : pid;
|
|
32
|
+
} catch {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function removePid(workingDirectory) {
|
|
37
|
+
const pidFile = getPidFile(workingDirectory);
|
|
38
|
+
if (existsSync(pidFile)) {
|
|
39
|
+
try {
|
|
40
|
+
unlinkSync(pidFile);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isProcessRunning(pid) {
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isWatcherRunning(workingDirectory) {
|
|
54
|
+
const pid = readPid(workingDirectory);
|
|
55
|
+
if (pid === void 0) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (isProcessRunning(pid)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
removePid(workingDirectory);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function writeStatus(workingDirectory, status) {
|
|
65
|
+
const statusFile = getStatusFile(workingDirectory);
|
|
66
|
+
writeFileSync(statusFile, JSON.stringify(status, null, 2), "utf-8");
|
|
67
|
+
}
|
|
68
|
+
function readStatus(workingDirectory) {
|
|
69
|
+
const statusFile = getStatusFile(workingDirectory);
|
|
70
|
+
if (!existsSync(statusFile)) {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const content = readFileSync(statusFile, "utf-8");
|
|
75
|
+
return JSON.parse(content);
|
|
76
|
+
} catch {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function updateStatus(workingDirectory, updates) {
|
|
81
|
+
const current = readStatus(workingDirectory);
|
|
82
|
+
if (current) {
|
|
83
|
+
writeStatus(workingDirectory, { ...current, ...updates });
|
|
84
|
+
} else if (updates.state && updates.projectSlug && updates.config) {
|
|
85
|
+
writeStatus(workingDirectory, {
|
|
86
|
+
state: updates.state,
|
|
87
|
+
projectSlug: updates.projectSlug,
|
|
88
|
+
ticketsProcessed: updates.ticketsProcessed ?? 0,
|
|
89
|
+
currentlyExecuting: updates.currentlyExecuting ?? [],
|
|
90
|
+
config: updates.config,
|
|
91
|
+
...updates
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
writePid,
|
|
98
|
+
readPid,
|
|
99
|
+
removePid,
|
|
100
|
+
isProcessRunning,
|
|
101
|
+
isWatcherRunning,
|
|
102
|
+
writeStatus,
|
|
103
|
+
readStatus,
|
|
104
|
+
updateStatus
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=chunk-POSCT6WJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/watcher/watcher_state.ts"],"sourcesContent":["/**\n * Watcher State Management\n *\n * Handles file-based state persistence for the YOLO watcher daemon.\n * State is stored in .ppm/yolo/ within the project directory.\n *\n * Files:\n * - watcher.pid: Process ID of running daemon\n * - status.json: Full watcher status\n * - watcher.log: Daemon log output\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from \"fs\";\nimport { join } from \"path\";\nimport type { WatcherStatus } from \"./watcher_types.js\";\n\n// ============================================================================\n// Path Utilities\n// ============================================================================\n\n/**\n * Get the watcher state directory for a project.\n * Creates the directory if it doesn't exist.\n *\n * @param workingDirectory - Project root directory\n * @returns Path to .ppm/yolo/ directory\n */\nexport function getWatcherDir(workingDirectory: string): string {\n const dir = join(workingDirectory, \".ppm\", \"yolo\");\n\n // Ensure directory exists\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n return dir;\n}\n\n/**\n * Get path to the PID file\n */\nexport function getPidFile(workingDirectory: string): string {\n return join(getWatcherDir(workingDirectory), \"watcher.pid\");\n}\n\n/**\n * Get path to the status JSON file\n */\nexport function getStatusFile(workingDirectory: string): string {\n return join(getWatcherDir(workingDirectory), \"status.json\");\n}\n\n/**\n * Get path to the log file\n */\nexport function getLogFile(workingDirectory: string): string {\n return join(getWatcherDir(workingDirectory), \"watcher.log\");\n}\n\n// ============================================================================\n// PID Management\n// ============================================================================\n\n/**\n * Write the daemon's PID to the PID file\n *\n * @param workingDirectory - Project root directory\n * @param pid - Process ID to write\n */\nexport function writePid(workingDirectory: string, pid: number): void {\n const pidFile = getPidFile(workingDirectory);\n writeFileSync(pidFile, pid.toString(), \"utf-8\");\n}\n\n/**\n * Read the daemon's PID from the PID file\n *\n * @param workingDirectory - Project root directory\n * @returns PID if file exists, undefined otherwise\n */\nexport function readPid(workingDirectory: string): number | undefined {\n const pidFile = getPidFile(workingDirectory);\n\n if (!existsSync(pidFile)) {\n return undefined;\n }\n\n try {\n const content = readFileSync(pidFile, \"utf-8\").trim();\n const pid = parseInt(content, 10);\n return isNaN(pid) ? undefined : pid;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Remove the PID file\n *\n * @param workingDirectory - Project root directory\n */\nexport function removePid(workingDirectory: string): void {\n const pidFile = getPidFile(workingDirectory);\n\n if (existsSync(pidFile)) {\n try {\n unlinkSync(pidFile);\n } catch {\n // Ignore errors during cleanup\n }\n }\n}\n\n/**\n * Check if a process with the given PID is running\n *\n * Uses process.kill(pid, 0) which doesn't actually send a signal\n * but throws if the process doesn't exist.\n *\n * @param pid - Process ID to check\n * @returns true if process is running\n */\nexport function isProcessRunning(pid: number): boolean {\n try {\n // Signal 0 doesn't kill the process, just checks if it exists\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if the watcher daemon is currently running\n *\n * Reads the PID file and verifies the process is still alive.\n * Cleans up stale PID file if process is not running.\n *\n * @param workingDirectory - Project root directory\n * @returns true if watcher is running\n */\nexport function isWatcherRunning(workingDirectory: string): boolean {\n const pid = readPid(workingDirectory);\n\n if (pid === undefined) {\n return false;\n }\n\n if (isProcessRunning(pid)) {\n return true;\n }\n\n // Stale PID file - process is not running\n // Clean up the file\n removePid(workingDirectory);\n return false;\n}\n\n// ============================================================================\n// Status Management\n// ============================================================================\n\n/**\n * Write watcher status to the status file\n *\n * @param workingDirectory - Project root directory\n * @param status - Status object to persist\n */\nexport function writeStatus(workingDirectory: string, status: WatcherStatus): void {\n const statusFile = getStatusFile(workingDirectory);\n writeFileSync(statusFile, JSON.stringify(status, null, 2), \"utf-8\");\n}\n\n/**\n * Read watcher status from the status file\n *\n * @param workingDirectory - Project root directory\n * @returns Status object if file exists and is valid, undefined otherwise\n */\nexport function readStatus(workingDirectory: string): WatcherStatus | undefined {\n const statusFile = getStatusFile(workingDirectory);\n\n if (!existsSync(statusFile)) {\n return undefined;\n }\n\n try {\n const content = readFileSync(statusFile, \"utf-8\");\n return JSON.parse(content) as WatcherStatus;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Update specific fields in the status file\n *\n * Reads current status, merges updates, and writes back.\n * Creates new status if none exists.\n *\n * @param workingDirectory - Project root directory\n * @param updates - Partial status updates to apply\n */\nexport function updateStatus(\n workingDirectory: string,\n updates: Partial<WatcherStatus>\n): void {\n const current = readStatus(workingDirectory);\n\n if (current) {\n writeStatus(workingDirectory, { ...current, ...updates });\n } else if (updates.state && updates.projectSlug && updates.config) {\n // Create new status with required fields\n writeStatus(workingDirectory, {\n state: updates.state,\n projectSlug: updates.projectSlug,\n ticketsProcessed: updates.ticketsProcessed ?? 0,\n currentlyExecuting: updates.currentlyExecuting ?? [],\n config: updates.config,\n ...updates,\n });\n }\n}\n\n/**\n * Remove the status file\n *\n * @param workingDirectory - Project root directory\n */\nexport function removeStatus(workingDirectory: string): void {\n const statusFile = getStatusFile(workingDirectory);\n\n if (existsSync(statusFile)) {\n try {\n unlinkSync(statusFile);\n } catch {\n // Ignore errors during cleanup\n }\n }\n}\n\n// ============================================================================\n// Cleanup\n// ============================================================================\n\n/**\n * Clean up all watcher state files\n *\n * Used when stopping the watcher or cleaning up after errors.\n *\n * @param workingDirectory - Project root directory\n */\nexport function cleanupWatcherState(workingDirectory: string): void {\n removePid(workingDirectory);\n // Note: We keep status.json for debugging - it shows last known state\n // The status file's state field is set to 'stopped' instead\n}\n"],"mappings":";;;AAYA,SAAS,YAAY,WAAW,cAAc,eAAe,kBAAkB;AAC/E,SAAS,YAAY;AAcd,SAAS,cAAc,kBAAkC;AAC9D,QAAM,MAAM,KAAK,kBAAkB,QAAQ,MAAM;AAGjD,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AAEA,SAAO;AACT;AAKO,SAAS,WAAW,kBAAkC;AAC3D,SAAO,KAAK,cAAc,gBAAgB,GAAG,aAAa;AAC5D;AAKO,SAAS,cAAc,kBAAkC;AAC9D,SAAO,KAAK,cAAc,gBAAgB,GAAG,aAAa;AAC5D;AAmBO,SAAS,SAAS,kBAA0B,KAAmB;AACpE,QAAM,UAAU,WAAW,gBAAgB;AAC3C,gBAAc,SAAS,IAAI,SAAS,GAAG,OAAO;AAChD;AAQO,SAAS,QAAQ,kBAA8C;AACpE,QAAM,UAAU,WAAW,gBAAgB;AAE3C,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,SAAS,OAAO,EAAE,KAAK;AACpD,UAAM,MAAM,SAAS,SAAS,EAAE;AAChC,WAAO,MAAM,GAAG,IAAI,SAAY;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,UAAU,kBAAgC;AACxD,QAAM,UAAU,WAAW,gBAAgB;AAE3C,MAAI,WAAW,OAAO,GAAG;AACvB,QAAI;AACF,iBAAW,OAAO;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAWO,SAAS,iBAAiB,KAAsB;AACrD,MAAI;AAEF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,iBAAiB,kBAAmC;AAClE,QAAM,MAAM,QAAQ,gBAAgB;AAEpC,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,GAAG,GAAG;AACzB,WAAO;AAAA,EACT;AAIA,YAAU,gBAAgB;AAC1B,SAAO;AACT;AAYO,SAAS,YAAY,kBAA0B,QAA6B;AACjF,QAAM,aAAa,cAAc,gBAAgB;AACjD,gBAAc,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;AACpE;AAQO,SAAS,WAAW,kBAAqD;AAC9E,QAAM,aAAa,cAAc,gBAAgB;AAEjD,MAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,YAAY,OAAO;AAChD,WAAO,KAAK,MAAM,OAAO;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,aACd,kBACA,SACM;AACN,QAAM,UAAU,WAAW,gBAAgB;AAE3C,MAAI,SAAS;AACX,gBAAY,kBAAkB,EAAE,GAAG,SAAS,GAAG,QAAQ,CAAC;AAAA,EAC1D,WAAW,QAAQ,SAAS,QAAQ,eAAe,QAAQ,QAAQ;AAEjE,gBAAY,kBAAkB;AAAA,MAC5B,OAAO,QAAQ;AAAA,MACf,aAAa,QAAQ;AAAA,MACrB,kBAAkB,QAAQ,oBAAoB;AAAA,MAC9C,oBAAoB,QAAQ,sBAAsB,CAAC;AAAA,MACnD,QAAQ,QAAQ;AAAA,MAChB,GAAG;AAAA,IACL,CAAC;AAAA,EACH;AACF;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isProcessRunning,
|
|
4
|
+
isWatcherRunning,
|
|
5
|
+
readPid,
|
|
6
|
+
readStatus,
|
|
7
|
+
removePid,
|
|
8
|
+
writeStatus
|
|
9
|
+
} from "./chunk-POSCT6WJ.js";
|
|
2
10
|
|
|
3
11
|
// src/index.ts
|
|
4
12
|
import minimist from "minimist";
|
|
@@ -90,6 +98,171 @@ function parsePromptsUpdateArgs(args) {
|
|
|
90
98
|
changeLogMessage: typeof parsed?.changeLogMessage === "string" ? parsed.changeLogMessage : void 0
|
|
91
99
|
};
|
|
92
100
|
}
|
|
101
|
+
function parseWatcherStartArgs(args) {
|
|
102
|
+
const parsed = args;
|
|
103
|
+
return {
|
|
104
|
+
pollIntervalMs: typeof parsed?.pollIntervalMs === "number" ? parsed.pollIntervalMs : void 0,
|
|
105
|
+
maxParallel: typeof parsed?.maxParallel === "number" ? parsed.maxParallel : void 0,
|
|
106
|
+
ticketTimeout: typeof parsed?.ticketTimeout === "number" ? parsed.ticketTimeout : void 0,
|
|
107
|
+
enableNotifications: typeof parsed?.enableNotifications === "boolean" ? parsed.enableNotifications : void 0,
|
|
108
|
+
workingDirectory: typeof parsed?.workingDirectory === "string" ? parsed.workingDirectory : void 0
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/watcher/watcher_controller.ts
|
|
113
|
+
import { spawn } from "child_process";
|
|
114
|
+
import { dirname, join } from "path";
|
|
115
|
+
import { fileURLToPath } from "url";
|
|
116
|
+
async function startWatcher(baseConfig, args) {
|
|
117
|
+
const workingDirectory = args?.workingDirectory ?? baseConfig.workingDirectory;
|
|
118
|
+
if (isWatcherRunning(workingDirectory)) {
|
|
119
|
+
const existingPid = readPid(workingDirectory);
|
|
120
|
+
const existingStatus = readStatus(workingDirectory);
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
message: "Watcher is already running",
|
|
124
|
+
pid: existingPid,
|
|
125
|
+
alreadyRunning: true,
|
|
126
|
+
config: existingStatus?.config
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const config = {
|
|
130
|
+
projectSlug: baseConfig.projectSlug,
|
|
131
|
+
projectToken: baseConfig.projectToken,
|
|
132
|
+
convexUrl: baseConfig.convexUrl,
|
|
133
|
+
workingDirectory,
|
|
134
|
+
pollIntervalMs: args?.pollIntervalMs ?? 3e4,
|
|
135
|
+
maxParallel: args?.maxParallel ?? 1,
|
|
136
|
+
ticketTimeout: args?.ticketTimeout ?? 18e5,
|
|
137
|
+
enableNotifications: args?.enableNotifications ?? true
|
|
138
|
+
};
|
|
139
|
+
const configJson = JSON.stringify(config);
|
|
140
|
+
const configBase64 = Buffer.from(configJson).toString("base64");
|
|
141
|
+
const daemonPath = getDaemonPath();
|
|
142
|
+
try {
|
|
143
|
+
const child = spawn(process.execPath, [daemonPath, "--config", configBase64], {
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
146
|
+
cwd: workingDirectory
|
|
147
|
+
});
|
|
148
|
+
child.unref();
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
150
|
+
const pid = readPid(workingDirectory);
|
|
151
|
+
if (pid && isProcessRunning(pid)) {
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
message: "Watcher started successfully",
|
|
155
|
+
pid,
|
|
156
|
+
alreadyRunning: false,
|
|
157
|
+
config: {
|
|
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
|
+
};
|
|
167
|
+
} else {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
message: "Daemon process started but failed to initialize. Check logs in .ppm/yolo/"
|
|
171
|
+
};
|
|
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
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function stopWatcher(workingDirectory) {
|
|
182
|
+
const pid = readPid(workingDirectory);
|
|
183
|
+
if (!pid) {
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
message: "No watcher is running",
|
|
187
|
+
wasRunning: false
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (!isProcessRunning(pid)) {
|
|
191
|
+
removePid(workingDirectory);
|
|
192
|
+
const status = readStatus(workingDirectory);
|
|
193
|
+
if (status) {
|
|
194
|
+
writeStatus(workingDirectory, { ...status, state: "stopped" });
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
message: "Watcher was not running (cleaned up stale PID file)",
|
|
199
|
+
wasRunning: false
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, "SIGTERM");
|
|
204
|
+
const maxWait = 5e3;
|
|
205
|
+
const checkInterval = 100;
|
|
206
|
+
let waited = 0;
|
|
207
|
+
while (waited < maxWait && isProcessRunning(pid)) {
|
|
208
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
209
|
+
waited += checkInterval;
|
|
210
|
+
}
|
|
211
|
+
if (isProcessRunning(pid)) {
|
|
212
|
+
try {
|
|
213
|
+
process.kill(pid, "SIGKILL");
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
removePid(workingDirectory);
|
|
218
|
+
const status = readStatus(workingDirectory);
|
|
219
|
+
if (status) {
|
|
220
|
+
writeStatus(workingDirectory, { ...status, state: "stopped" });
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
message: "Watcher stopped successfully",
|
|
225
|
+
wasRunning: true
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
message: `Failed to stop watcher: ${errorMessage}`,
|
|
232
|
+
wasRunning: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function getWatcherStatus(workingDirectory) {
|
|
237
|
+
const status = readStatus(workingDirectory);
|
|
238
|
+
if (!status) {
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
message: "No watcher has been started for this project"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (status.state === "running" && status.pid) {
|
|
245
|
+
if (!isProcessRunning(status.pid)) {
|
|
246
|
+
const updatedStatus = { ...status, state: "stopped" };
|
|
247
|
+
writeStatus(workingDirectory, updatedStatus);
|
|
248
|
+
removePid(workingDirectory);
|
|
249
|
+
return {
|
|
250
|
+
success: true,
|
|
251
|
+
status: updatedStatus,
|
|
252
|
+
message: "Watcher process has stopped unexpectedly"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
status
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function getDaemonPath() {
|
|
262
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
263
|
+
const currentDir = dirname(currentFilePath);
|
|
264
|
+
return join(currentDir, "watcher_daemon.js");
|
|
265
|
+
}
|
|
93
266
|
|
|
94
267
|
// src/prompt-builder.ts
|
|
95
268
|
var AmbiguousPromptError = class extends Error {
|
|
@@ -231,6 +404,28 @@ async function startServer(config, convexClientRaw) {
|
|
|
231
404
|
slashDescription: `Update a ticket by appending content with timestamp`,
|
|
232
405
|
projectSlug,
|
|
233
406
|
type: "update"
|
|
407
|
+
},
|
|
408
|
+
// YOLO watcher tools - autonomous ticket execution
|
|
409
|
+
{
|
|
410
|
+
name: `tickets_yolo_start`,
|
|
411
|
+
description: `Start the YOLO ticket watcher for "${projectSlug}". Polls for open tickets with yolo flag and spawns Terminal windows with Claude Code to execute them autonomously.`,
|
|
412
|
+
slashDescription: `Start YOLO watcher for autonomous ticket execution`,
|
|
413
|
+
projectSlug,
|
|
414
|
+
type: "yolo_start"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: `tickets_yolo_stop`,
|
|
418
|
+
description: `Stop the YOLO ticket watcher for "${projectSlug}"`,
|
|
419
|
+
slashDescription: `Stop the YOLO watcher daemon`,
|
|
420
|
+
projectSlug,
|
|
421
|
+
type: "yolo_stop"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: `tickets_yolo_status`,
|
|
425
|
+
description: `Get YOLO watcher status for "${projectSlug}"`,
|
|
426
|
+
slashDescription: `Check YOLO watcher status and metrics`,
|
|
427
|
+
projectSlug,
|
|
428
|
+
type: "yolo_status"
|
|
234
429
|
}
|
|
235
430
|
];
|
|
236
431
|
console.error(`[MCP] Registering ${dynamicTicketTools.length} ticket tools...`);
|
|
@@ -543,6 +738,45 @@ This will execute the "code-review" prompt.`;
|
|
|
543
738
|
},
|
|
544
739
|
required: ["ticketSlug", "content"]
|
|
545
740
|
};
|
|
741
|
+
} else if (tt.type === "yolo_start") {
|
|
742
|
+
inputSchema = {
|
|
743
|
+
type: "object",
|
|
744
|
+
properties: {
|
|
745
|
+
pollIntervalMs: {
|
|
746
|
+
type: "number",
|
|
747
|
+
description: "Milliseconds between polls (default: 30000 = 30s)"
|
|
748
|
+
},
|
|
749
|
+
maxParallel: {
|
|
750
|
+
type: "number",
|
|
751
|
+
description: "Maximum concurrent ticket executions (default: 1)"
|
|
752
|
+
},
|
|
753
|
+
ticketTimeout: {
|
|
754
|
+
type: "number",
|
|
755
|
+
description: "Timeout for ticket execution in ms (default: 1800000 = 30 min)"
|
|
756
|
+
},
|
|
757
|
+
enableNotifications: {
|
|
758
|
+
type: "boolean",
|
|
759
|
+
description: "Show macOS notifications (default: true)"
|
|
760
|
+
},
|
|
761
|
+
workingDirectory: {
|
|
762
|
+
type: "string",
|
|
763
|
+
description: "Working directory for Claude Code sessions (default: current working directory)"
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
required: []
|
|
767
|
+
};
|
|
768
|
+
} else if (tt.type === "yolo_stop") {
|
|
769
|
+
inputSchema = {
|
|
770
|
+
type: "object",
|
|
771
|
+
properties: {},
|
|
772
|
+
required: []
|
|
773
|
+
};
|
|
774
|
+
} else if (tt.type === "yolo_status") {
|
|
775
|
+
inputSchema = {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {},
|
|
778
|
+
required: []
|
|
779
|
+
};
|
|
546
780
|
} else {
|
|
547
781
|
inputSchema = {
|
|
548
782
|
type: "object",
|
|
@@ -1272,6 +1506,132 @@ _Ticket content has been appended with your update._`
|
|
|
1272
1506
|
isError: true
|
|
1273
1507
|
};
|
|
1274
1508
|
}
|
|
1509
|
+
} else if (ticketTool.type === "yolo_start") {
|
|
1510
|
+
const args = parseWatcherStartArgs(request.params.arguments);
|
|
1511
|
+
const workingDirectory = args.workingDirectory ?? process.cwd();
|
|
1512
|
+
try {
|
|
1513
|
+
const result = await startWatcher(
|
|
1514
|
+
{
|
|
1515
|
+
projectSlug: ticketTool.projectSlug,
|
|
1516
|
+
projectToken: config.projectToken,
|
|
1517
|
+
convexUrl: config.convexUrl,
|
|
1518
|
+
workingDirectory
|
|
1519
|
+
},
|
|
1520
|
+
args
|
|
1521
|
+
);
|
|
1522
|
+
if (result.success) {
|
|
1523
|
+
const statusLines = [
|
|
1524
|
+
result.alreadyRunning ? `\u2139\uFE0F Watcher is already running (PID: ${result.pid})` : `\u2705 Watcher started (PID: ${result.pid})`
|
|
1525
|
+
];
|
|
1526
|
+
if (result.config) {
|
|
1527
|
+
statusLines.push("");
|
|
1528
|
+
statusLines.push("**Configuration:**");
|
|
1529
|
+
statusLines.push(` Poll Interval: ${result.config.pollIntervalMs / 1e3}s`);
|
|
1530
|
+
statusLines.push(` Max Parallel: ${result.config.maxParallel}`);
|
|
1531
|
+
statusLines.push(` Ticket Timeout: ${result.config.ticketTimeout / 1e3 / 60} min`);
|
|
1532
|
+
statusLines.push(` Notifications: ${result.config.enableNotifications ? "enabled" : "disabled"}`);
|
|
1533
|
+
statusLines.push(` Working Directory: ${result.config.workingDirectory}`);
|
|
1534
|
+
}
|
|
1535
|
+
statusLines.push("");
|
|
1536
|
+
statusLines.push("_The watcher will poll for open tickets with the YOLO flag and spawn Claude Code terminals to execute them._");
|
|
1537
|
+
statusLines.push("_Use `tickets_yolo_status` to check progress, `tickets_yolo_stop` to stop._");
|
|
1538
|
+
return {
|
|
1539
|
+
content: [{ type: "text", text: statusLines.join("\n") }]
|
|
1540
|
+
};
|
|
1541
|
+
} else {
|
|
1542
|
+
return {
|
|
1543
|
+
content: [{ type: "text", text: `\u274C ${result.message}` }],
|
|
1544
|
+
isError: true
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1549
|
+
console.error(`[MCP] tickets_yolo_start error:`, error);
|
|
1550
|
+
return {
|
|
1551
|
+
content: [{ type: "text", text: `Error starting watcher: ${errorMessage}` }],
|
|
1552
|
+
isError: true
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
} else if (ticketTool.type === "yolo_stop") {
|
|
1556
|
+
const workingDirectory = process.cwd();
|
|
1557
|
+
try {
|
|
1558
|
+
const result = await stopWatcher(workingDirectory);
|
|
1559
|
+
if (result.success) {
|
|
1560
|
+
return {
|
|
1561
|
+
content: [{
|
|
1562
|
+
type: "text",
|
|
1563
|
+
text: result.wasRunning ? `\u2705 Watcher stopped successfully` : `\u2139\uFE0F ${result.message}`
|
|
1564
|
+
}]
|
|
1565
|
+
};
|
|
1566
|
+
} else {
|
|
1567
|
+
return {
|
|
1568
|
+
content: [{ type: "text", text: `\u274C ${result.message}` }],
|
|
1569
|
+
isError: true
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1574
|
+
console.error(`[MCP] tickets_yolo_stop error:`, error);
|
|
1575
|
+
return {
|
|
1576
|
+
content: [{ type: "text", text: `Error stopping watcher: ${errorMessage}` }],
|
|
1577
|
+
isError: true
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
} else if (ticketTool.type === "yolo_status") {
|
|
1581
|
+
const workingDirectory = process.cwd();
|
|
1582
|
+
try {
|
|
1583
|
+
const result = getWatcherStatus(workingDirectory);
|
|
1584
|
+
if (!result.status) {
|
|
1585
|
+
return {
|
|
1586
|
+
content: [{ type: "text", text: `\u2139\uFE0F ${result.message ?? "No watcher status available"}` }]
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
const status = result.status;
|
|
1590
|
+
const lines = [
|
|
1591
|
+
`# YOLO Watcher Status: ${status.projectSlug}`,
|
|
1592
|
+
"",
|
|
1593
|
+
`**State:** ${status.state.toUpperCase()}`
|
|
1594
|
+
];
|
|
1595
|
+
if (status.pid) {
|
|
1596
|
+
lines.push(`**PID:** ${status.pid}`);
|
|
1597
|
+
}
|
|
1598
|
+
if (status.startedAt) {
|
|
1599
|
+
lines.push(`**Started:** ${status.startedAt}`);
|
|
1600
|
+
}
|
|
1601
|
+
if (status.lastPollAt) {
|
|
1602
|
+
lines.push(`**Last Poll:** ${status.lastPollAt}`);
|
|
1603
|
+
}
|
|
1604
|
+
lines.push(`**Tickets Processed:** ${status.ticketsProcessed}`);
|
|
1605
|
+
if (status.currentlyExecuting && status.currentlyExecuting.length > 0) {
|
|
1606
|
+
lines.push("");
|
|
1607
|
+
lines.push("**Currently Executing:**");
|
|
1608
|
+
for (const exec of status.currentlyExecuting) {
|
|
1609
|
+
const displayName = exec.ticketNumber ? `#${exec.ticketNumber} ${exec.ticketSlug}` : exec.ticketSlug;
|
|
1610
|
+
lines.push(` \u2022 ${displayName} (started: ${exec.startedAt})`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (status.lastError) {
|
|
1614
|
+
lines.push("");
|
|
1615
|
+
lines.push(`**Last Error:** ${status.lastError}`);
|
|
1616
|
+
}
|
|
1617
|
+
if (status.config) {
|
|
1618
|
+
lines.push("");
|
|
1619
|
+
lines.push("**Configuration:**");
|
|
1620
|
+
lines.push(` Poll Interval: ${status.config.pollIntervalMs / 1e3}s`);
|
|
1621
|
+
lines.push(` Max Parallel: ${status.config.maxParallel}`);
|
|
1622
|
+
lines.push(` Working Directory: ${status.config.workingDirectory}`);
|
|
1623
|
+
}
|
|
1624
|
+
return {
|
|
1625
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1626
|
+
};
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1629
|
+
console.error(`[MCP] tickets_yolo_status error:`, error);
|
|
1630
|
+
return {
|
|
1631
|
+
content: [{ type: "text", text: `Error getting watcher status: ${errorMessage}` }],
|
|
1632
|
+
isError: true
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1275
1635
|
}
|
|
1276
1636
|
}
|
|
1277
1637
|
const promptTool = dynamicPromptTools.find((pt) => pt.name === toolName);
|