@masslessai/push-todo 4.0.0 → 4.0.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.
Files changed (2) hide show
  1. package/lib/daemon.js +88 -10
  2. package/package.json +1 -1
package/lib/daemon.js CHANGED
@@ -105,6 +105,17 @@ const taskStderrBuffer = new Map(); // displayNumber -> lines[]
105
105
  const taskProjectPaths = new Map(); // displayNumber -> projectPath
106
106
  let daemonStartTime = null;
107
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
+
108
119
  // ==================== Logging ====================
109
120
 
110
121
  function rotateLogs() {
@@ -1000,6 +1011,24 @@ async function markTaskAsCompleted(displayNumber, taskId, comment) {
1000
1011
  }
1001
1012
  }
1002
1013
 
1014
+ async function hasApprovedConfirmation(displayNumber) {
1015
+ try {
1016
+ const response = await apiRequest(`synced-todos?display_number=${displayNumber}`);
1017
+ if (!response.ok) return false;
1018
+ const data = await response.json();
1019
+ const todos = data.todos || [];
1020
+ if (todos.length === 0) return false;
1021
+ const events = parseJsonField(todos[0].executionEventsJson);
1022
+ for (let i = events.length - 1; i >= 0; i--) {
1023
+ if (events[i].type === 'confirmation_approved') return true;
1024
+ if (events[i].type === 'confirmation_rejected') return false;
1025
+ }
1026
+ return false;
1027
+ } catch {
1028
+ return false;
1029
+ }
1030
+ }
1031
+
1003
1032
  /**
1004
1033
  * Auto-heal: detect if a previous execution already completed work for this task.
1005
1034
  * Checks for existing branch commits and PRs to avoid redundant re-execution.
@@ -1386,10 +1415,37 @@ async function executeTask(task) {
1386
1415
 
1387
1416
  taskProjectPaths.set(displayNumber, projectPath);
1388
1417
 
1418
+ // Build attachment context for the prompt (screenshots, links)
1419
+ let attachmentContext = '';
1420
+ try {
1421
+ const links = parseJsonField(task.linkAttachmentsJson || task.linkAttachments || task.link_attachments);
1422
+ const screenshots = parseJsonField(task.screenshotAttachmentsJson || task.screenshotAttachments || task.screenshot_attachments);
1423
+ const contextApp = task.contextApp || task.context_app || null;
1424
+
1425
+ const parts = [];
1426
+ if (contextApp) {
1427
+ parts.push(`Context app: ${contextApp}`);
1428
+ }
1429
+ if (links.length > 0) {
1430
+ parts.push('Links:\n' + links.map(l => ` - ${l.url}${l.title ? ` (${l.title})` : ''}`).join('\n'));
1431
+ }
1432
+ if (screenshots.length > 0) {
1433
+ parts.push('Screenshots:\n' + screenshots.map(s =>
1434
+ ` - ${s.imageFilename || s.image_filename}${s.sourceApp ? ` (from ${s.sourceApp})` : ''}`
1435
+ ).join('\n'));
1436
+ }
1437
+
1438
+ if (parts.length > 0) {
1439
+ attachmentContext = '\n\nAttachments:\n' + parts.join('\n');
1440
+ }
1441
+ } catch {
1442
+ // Non-critical — continue without attachment context
1443
+ }
1444
+
1389
1445
  // Build prompt
1390
1446
  const prompt = `Work on Push task #${displayNumber}:
1391
1447
 
1392
- ${content}
1448
+ ${content}${attachmentContext}
1393
1449
 
1394
1450
  IMPORTANT:
1395
1451
  1. If you need to understand the codebase, start by reading the CLAUDE.md file if it exists.
@@ -1405,6 +1461,7 @@ IMPORTANT:
1405
1461
  'Bash(git *)',
1406
1462
  'Bash(npm *)', 'Bash(npx *)', 'Bash(yarn *)',
1407
1463
  'Bash(python *)', 'Bash(python3 *)', 'Bash(pip *)', 'Bash(pip3 *)',
1464
+ 'Bash(push-todo *)',
1408
1465
  'Task'
1409
1466
  ].join(',');
1410
1467
 
@@ -1614,15 +1671,36 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1614
1671
  }
1615
1672
  }
1616
1673
 
1617
- // Auto-complete task after successful merge (configurable, default ON)
1674
+ // Auto-complete task (configurable, default ON)
1618
1675
  const taskId = info.taskId;
1619
- if (getAutoCompleteEnabled() && merged && taskId) {
1620
- const comment = semanticSummary
1621
- ? `${semanticSummary} (${durationStr} on ${machineName})`
1622
- : `Completed in ${durationStr} on ${machineName}`;
1623
- const completed = await markTaskAsCompleted(displayNumber, taskId, comment);
1624
- if (!completed) {
1625
- logError(`Task #${displayNumber}: Failed to mark as completed — status is session_finished but not completed`);
1676
+ let autoCompleted = false;
1677
+
1678
+ if (getAutoCompleteEnabled() && taskId) {
1679
+ // Path 1: PR merge (code tasks)
1680
+ if (merged) {
1681
+ const comment = semanticSummary
1682
+ ? `${semanticSummary} (${durationStr} on ${machineName})`
1683
+ : `Completed in ${durationStr} on ${machineName}`;
1684
+ autoCompleted = await markTaskAsCompleted(displayNumber, taskId, comment);
1685
+ if (!autoCompleted) {
1686
+ logError(`Task #${displayNumber}: Failed to mark as completed after merge`);
1687
+ }
1688
+ }
1689
+
1690
+ // Path 2: Confirmation approval (content tasks — tweets, emails, etc.)
1691
+ // If user approved a confirmation AND Claude exited cleanly, the task is done.
1692
+ if (!autoCompleted && !merged) {
1693
+ const approved = await hasApprovedConfirmation(displayNumber);
1694
+ if (approved) {
1695
+ log(`Task #${displayNumber}: user-approved confirmation detected, auto-completing`);
1696
+ const comment = semanticSummary
1697
+ ? `${semanticSummary} (${durationStr} on ${machineName})`
1698
+ : `Confirmed and completed in ${durationStr} on ${machineName}`;
1699
+ autoCompleted = await markTaskAsCompleted(displayNumber, taskId, comment);
1700
+ if (!autoCompleted) {
1701
+ logError(`Task #${displayNumber}: Failed to mark as completed after confirmation`);
1702
+ }
1703
+ }
1626
1704
  }
1627
1705
  }
1628
1706
 
@@ -1631,7 +1709,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1631
1709
  summary,
1632
1710
  completedAt: new Date().toISOString(),
1633
1711
  duration,
1634
- status: merged ? 'completed' : 'session_finished',
1712
+ status: autoCompleted ? 'completed' : 'session_finished',
1635
1713
  prUrl,
1636
1714
  sessionId
1637
1715
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {