@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.
@@ -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', 'node.*daemon\\.js'], {
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
- completedToday.push({
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
- completedToday.push({
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
- completedToday.push({
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
- completedToday.push({
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
- completedToday.push({
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
- completedToday.push({
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "3.10.9",
3
+ "version": "4.0.1",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {