@masslessai/push-todo 3.10.9 → 4.0.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.
- package/lib/daemon-health.js +1 -1
- package/lib/daemon.js +98 -8
- package/package.json +1 -1
package/lib/daemon-health.js
CHANGED
|
@@ -125,7 +125,7 @@ function killOrphanedDaemons() {
|
|
|
125
125
|
const trackedPid = existsSync(PID_FILE)
|
|
126
126
|
? parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10)
|
|
127
127
|
: null;
|
|
128
|
-
const output = execFileSync('pgrep', ['-f', '
|
|
128
|
+
const output = execFileSync('pgrep', ['-f', 'push-todo.*daemon\\.js'], {
|
|
129
129
|
encoding: 'utf8',
|
|
130
130
|
timeout: 5000
|
|
131
131
|
}).trim();
|
package/lib/daemon.js
CHANGED
|
@@ -78,6 +78,7 @@ const PID_FILE = join(PUSH_DIR, 'daemon.pid');
|
|
|
78
78
|
const LOG_FILE = join(PUSH_DIR, 'daemon.log');
|
|
79
79
|
const STATUS_FILE = join(PUSH_DIR, 'daemon_status.json');
|
|
80
80
|
const VERSION_FILE = join(PUSH_DIR, 'daemon.version');
|
|
81
|
+
const LOCK_FILE = join(PUSH_DIR, 'daemon.lock');
|
|
81
82
|
const CONFIG_FILE = join(CONFIG_DIR, 'config');
|
|
82
83
|
const MACHINE_ID_FILE = join(CONFIG_DIR, 'machine_id');
|
|
83
84
|
const REGISTRY_FILE = join(CONFIG_DIR, 'projects.json');
|
|
@@ -90,12 +91,31 @@ const LOG_BACKUP_COUNT = 3;
|
|
|
90
91
|
const runningTasks = new Map(); // displayNumber -> taskInfo
|
|
91
92
|
const taskDetails = new Map(); // displayNumber -> details
|
|
92
93
|
const completedToday = [];
|
|
94
|
+
const COMPLETED_TODAY_MAX = 100;
|
|
95
|
+
|
|
96
|
+
function trackCompleted(entry) {
|
|
97
|
+
completedToday.push(entry);
|
|
98
|
+
if (completedToday.length > COMPLETED_TODAY_MAX) {
|
|
99
|
+
completedToday.splice(0, completedToday.length - COMPLETED_TODAY_MAX);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
93
102
|
const taskLastOutput = new Map(); // displayNumber -> timestamp
|
|
94
103
|
const taskStdoutBuffer = new Map(); // displayNumber -> lines[]
|
|
95
104
|
const taskStderrBuffer = new Map(); // displayNumber -> lines[]
|
|
96
105
|
const taskProjectPaths = new Map(); // displayNumber -> projectPath
|
|
97
106
|
let daemonStartTime = null;
|
|
98
107
|
|
|
108
|
+
// ==================== Utilities ====================
|
|
109
|
+
|
|
110
|
+
function parseJsonField(value) {
|
|
111
|
+
if (!value) return [];
|
|
112
|
+
if (Array.isArray(value)) return value;
|
|
113
|
+
if (typeof value === 'string') {
|
|
114
|
+
try { return JSON.parse(value); } catch { return []; }
|
|
115
|
+
}
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
// ==================== Logging ====================
|
|
100
120
|
|
|
101
121
|
function rotateLogs() {
|
|
@@ -126,7 +146,7 @@ function rotateLogs() {
|
|
|
126
146
|
|
|
127
147
|
function log(message, level = 'INFO') {
|
|
128
148
|
const timestamp = new Date().toISOString();
|
|
129
|
-
const line = `[${timestamp}] [${level}] ${message}\n`;
|
|
149
|
+
const line = `[${timestamp}] [PID:${process.pid}] [${level}] ${message}\n`;
|
|
130
150
|
|
|
131
151
|
if (process.env.PUSH_DAEMON !== '1') {
|
|
132
152
|
process.stdout.write(line);
|
|
@@ -141,6 +161,38 @@ function logError(message) {
|
|
|
141
161
|
log(message, 'ERROR');
|
|
142
162
|
}
|
|
143
163
|
|
|
164
|
+
// ==================== Lock File ====================
|
|
165
|
+
|
|
166
|
+
function isProcessRunning(pid) {
|
|
167
|
+
try {
|
|
168
|
+
process.kill(pid, 0);
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function acquireLock() {
|
|
176
|
+
try {
|
|
177
|
+
if (existsSync(LOCK_FILE)) {
|
|
178
|
+
const holderPid = parseInt(readFileSync(LOCK_FILE, 'utf8').trim(), 10);
|
|
179
|
+
if (!isNaN(holderPid) && holderPid !== process.pid && isProcessRunning(holderPid)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// Holder is dead or is us — clean up stale lock
|
|
183
|
+
try { unlinkSync(LOCK_FILE); } catch {}
|
|
184
|
+
}
|
|
185
|
+
writeFileSync(LOCK_FILE, String(process.pid));
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function releaseLock() {
|
|
193
|
+
try { unlinkSync(LOCK_FILE); } catch {}
|
|
194
|
+
}
|
|
195
|
+
|
|
144
196
|
// ==================== Mac Notifications ====================
|
|
145
197
|
|
|
146
198
|
function sendMacNotification(title, message, sound = 'default') {
|
|
@@ -1025,7 +1077,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath, taskId)
|
|
|
1025
1077
|
}
|
|
1026
1078
|
}
|
|
1027
1079
|
|
|
1028
|
-
|
|
1080
|
+
trackCompleted({
|
|
1029
1081
|
displayNumber, summary,
|
|
1030
1082
|
completedAt: new Date().toISOString(),
|
|
1031
1083
|
duration: 0, status, prUrl
|
|
@@ -1062,7 +1114,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath, taskId)
|
|
|
1062
1114
|
}
|
|
1063
1115
|
}
|
|
1064
1116
|
|
|
1065
|
-
|
|
1117
|
+
trackCompleted({
|
|
1066
1118
|
displayNumber, summary,
|
|
1067
1119
|
completedAt: new Date().toISOString(),
|
|
1068
1120
|
duration: 0, status, prUrl
|
|
@@ -1079,7 +1131,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath, taskId)
|
|
|
1079
1131
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
1080
1132
|
summary: executionSummary
|
|
1081
1133
|
});
|
|
1082
|
-
|
|
1134
|
+
trackCompleted({
|
|
1083
1135
|
displayNumber, summary,
|
|
1084
1136
|
completedAt: new Date().toISOString(),
|
|
1085
1137
|
duration: 0, status: 'session_finished', prUrl: newPrUrl
|
|
@@ -1345,10 +1397,37 @@ async function executeTask(task) {
|
|
|
1345
1397
|
|
|
1346
1398
|
taskProjectPaths.set(displayNumber, projectPath);
|
|
1347
1399
|
|
|
1400
|
+
// Build attachment context for the prompt (screenshots, links)
|
|
1401
|
+
let attachmentContext = '';
|
|
1402
|
+
try {
|
|
1403
|
+
const links = parseJsonField(task.linkAttachmentsJson || task.linkAttachments || task.link_attachments);
|
|
1404
|
+
const screenshots = parseJsonField(task.screenshotAttachmentsJson || task.screenshotAttachments || task.screenshot_attachments);
|
|
1405
|
+
const contextApp = task.contextApp || task.context_app || null;
|
|
1406
|
+
|
|
1407
|
+
const parts = [];
|
|
1408
|
+
if (contextApp) {
|
|
1409
|
+
parts.push(`Context app: ${contextApp}`);
|
|
1410
|
+
}
|
|
1411
|
+
if (links.length > 0) {
|
|
1412
|
+
parts.push('Links:\n' + links.map(l => ` - ${l.url}${l.title ? ` (${l.title})` : ''}`).join('\n'));
|
|
1413
|
+
}
|
|
1414
|
+
if (screenshots.length > 0) {
|
|
1415
|
+
parts.push('Screenshots:\n' + screenshots.map(s =>
|
|
1416
|
+
` - ${s.imageFilename || s.image_filename}${s.sourceApp ? ` (from ${s.sourceApp})` : ''}`
|
|
1417
|
+
).join('\n'));
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (parts.length > 0) {
|
|
1421
|
+
attachmentContext = '\n\nAttachments:\n' + parts.join('\n');
|
|
1422
|
+
}
|
|
1423
|
+
} catch {
|
|
1424
|
+
// Non-critical — continue without attachment context
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1348
1427
|
// Build prompt
|
|
1349
1428
|
const prompt = `Work on Push task #${displayNumber}:
|
|
1350
1429
|
|
|
1351
|
-
${content}
|
|
1430
|
+
${content}${attachmentContext}
|
|
1352
1431
|
|
|
1353
1432
|
IMPORTANT:
|
|
1354
1433
|
1. If you need to understand the codebase, start by reading the CLAUDE.md file if it exists.
|
|
@@ -1364,6 +1443,7 @@ IMPORTANT:
|
|
|
1364
1443
|
'Bash(git *)',
|
|
1365
1444
|
'Bash(npm *)', 'Bash(npx *)', 'Bash(yarn *)',
|
|
1366
1445
|
'Bash(python *)', 'Bash(python3 *)', 'Bash(pip *)', 'Bash(pip3 *)',
|
|
1446
|
+
'Bash(push-todo *)',
|
|
1367
1447
|
'Task'
|
|
1368
1448
|
].join(',');
|
|
1369
1449
|
|
|
@@ -1585,7 +1665,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1585
1665
|
}
|
|
1586
1666
|
}
|
|
1587
1667
|
|
|
1588
|
-
|
|
1668
|
+
trackCompleted({
|
|
1589
1669
|
displayNumber,
|
|
1590
1670
|
summary,
|
|
1591
1671
|
completedAt: new Date().toISOString(),
|
|
@@ -1621,7 +1701,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1621
1701
|
);
|
|
1622
1702
|
}
|
|
1623
1703
|
|
|
1624
|
-
|
|
1704
|
+
trackCompleted({
|
|
1625
1705
|
displayNumber,
|
|
1626
1706
|
summary,
|
|
1627
1707
|
completedAt: new Date().toISOString(),
|
|
@@ -1765,7 +1845,7 @@ async function checkTimeouts() {
|
|
|
1765
1845
|
);
|
|
1766
1846
|
}
|
|
1767
1847
|
|
|
1768
|
-
|
|
1848
|
+
trackCompleted({
|
|
1769
1849
|
displayNumber,
|
|
1770
1850
|
summary: info.summary || 'Unknown task',
|
|
1771
1851
|
completedAt: new Date().toISOString(),
|
|
@@ -2043,6 +2123,7 @@ async function cleanup() {
|
|
|
2043
2123
|
}
|
|
2044
2124
|
|
|
2045
2125
|
// Clean up files
|
|
2126
|
+
releaseLock();
|
|
2046
2127
|
try { unlinkSync(PID_FILE); } catch {}
|
|
2047
2128
|
|
|
2048
2129
|
// Update status
|
|
@@ -2072,6 +2153,15 @@ mkdirSync(CONFIG_DIR, { recursive: true });
|
|
|
2072
2153
|
// Rotate logs
|
|
2073
2154
|
rotateLogs();
|
|
2074
2155
|
|
|
2156
|
+
// Acquire lock (prevents zombie daemons from racing)
|
|
2157
|
+
if (!acquireLock()) {
|
|
2158
|
+
const holderPid = parseInt(readFileSync(LOCK_FILE, 'utf8').trim(), 10);
|
|
2159
|
+
// Log to file directly since log() may not work yet
|
|
2160
|
+
const msg = `[${new Date().toISOString()}] [PID:${process.pid}] [INFO] Another daemon (PID ${holderPid}) holds the lock. Exiting.\n`;
|
|
2161
|
+
try { appendFileSync(LOG_FILE, msg); } catch {}
|
|
2162
|
+
process.exit(0);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2075
2165
|
// Write PID file
|
|
2076
2166
|
writeFileSync(PID_FILE, String(process.pid));
|
|
2077
2167
|
|