@masslessai/push-todo 4.0.4 → 4.0.6

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/api.js CHANGED
@@ -147,6 +147,39 @@ export async function markTaskCompleted(taskId, comment = '') {
147
147
  return true;
148
148
  }
149
149
 
150
+ /**
151
+ * Create a new todo.
152
+ *
153
+ * @param {Object} options - Todo creation options
154
+ * @param {string} options.title - Todo title (required)
155
+ * @param {string|null} options.content - Detailed content (optional)
156
+ * @param {boolean} options.backlog - Whether to create as backlog item
157
+ * @returns {Promise<Object>} Created todo with { id, displayNumber, title, createdAt }
158
+ */
159
+ export async function createTodo({ title, content = null, backlog = false }) {
160
+ const response = await apiRequest('create-todo', {
161
+ method: 'POST',
162
+ body: JSON.stringify({
163
+ title,
164
+ normalizedContent: content || null,
165
+ isBacklog: backlog,
166
+ createdByClient: 'cli',
167
+ }),
168
+ });
169
+
170
+ if (!response.ok) {
171
+ const text = await response.text();
172
+ throw new Error(`Failed to create todo: ${text}`);
173
+ }
174
+
175
+ const data = await response.json();
176
+ if (!data.success) {
177
+ throw new Error(data.error || 'Unknown error creating todo');
178
+ }
179
+
180
+ return data.todo;
181
+ }
182
+
150
183
  /**
151
184
  * Queue a task for daemon execution.
152
185
  *
package/lib/cli.js CHANGED
@@ -43,6 +43,7 @@ ${bold('push-todo')} - Voice tasks from Push iOS app for Claude Code
43
43
  ${bold('USAGE:')}
44
44
  push-todo [options] List active tasks
45
45
  push-todo <number> Show specific task
46
+ push-todo create <title> Create a new todo
46
47
  push-todo connect Run connection doctor
47
48
  push-todo search <query> Search tasks
48
49
  push-todo review Review completed tasks
@@ -78,6 +79,8 @@ ${bold('OPTIONS:')}
78
79
  ${bold('EXAMPLES:')}
79
80
  push-todo List active tasks for current project
80
81
  push-todo 427 Show task #427
82
+ push-todo create "Fix auth bug" Create a new todo
83
+ push-todo create "Item" --backlog Create as backlog item
81
84
  push-todo -a List all tasks across projects
82
85
  push-todo --queue 1,2,3 Queue tasks 1, 2, 3 for daemon
83
86
  push-todo search "auth bug" Search for tasks matching "auth bug"
@@ -505,6 +508,38 @@ export async function run(argv) {
505
508
  return requestConfirmation(values, positionals);
506
509
  }
507
510
 
511
+ // Create command - create a new todo
512
+ if (command === 'create') {
513
+ const title = positionals.slice(1).join(' ');
514
+ if (!title) {
515
+ console.error(red('Error: Title is required.'));
516
+ console.error('');
517
+ console.error('Usage:');
518
+ console.error(' push-todo create "Fix the auth bug"');
519
+ console.error(' push-todo create "Fix the auth bug" --backlog');
520
+ console.error(' push-todo create "Fix the auth bug" --content "Detailed description"');
521
+ process.exit(1);
522
+ }
523
+
524
+ try {
525
+ const todo = await api.createTodo({
526
+ title,
527
+ content: values.content || null,
528
+ backlog: values.backlog || false,
529
+ });
530
+
531
+ if (values.json) {
532
+ console.log(JSON.stringify(todo, null, 2));
533
+ } else {
534
+ console.log(green(`Created todo #${todo.displayNumber}: ${todo.title}`));
535
+ }
536
+ } catch (error) {
537
+ console.error(red(`Failed to create todo: ${error.message}`));
538
+ process.exit(1);
539
+ }
540
+ return;
541
+ }
542
+
508
543
  // Cron command - scheduled jobs
509
544
  if (command === 'cron') {
510
545
  const { addJob, removeJob, listJobs } = await import('./cron.js');
package/lib/daemon.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * Ported from: plugins/push-todo/scripts/daemon.py
14
14
  */
15
15
 
16
+ import { randomUUID } from 'crypto';
16
17
  import { spawn, execSync, execFileSync } from 'child_process';
17
18
  import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync, statSync, renameSync } from 'fs';
18
19
  import { homedir, hostname, platform } from 'os';
@@ -547,7 +548,7 @@ async function updateTaskStatus(displayNumber, status, extra = {}, todoId = null
547
548
  }
548
549
  }
549
550
 
550
- async function claimTask(displayNumber, todoId) {
551
+ async function claimTask(displayNumber, todoId, sessionId) {
551
552
  const machineId = getMachineId();
552
553
  const machineName = getMachineName();
553
554
 
@@ -566,6 +567,7 @@ async function claimTask(displayNumber, todoId) {
566
567
  machineId,
567
568
  machineName,
568
569
  branch,
570
+ sessionId, // Pre-assigned session ID for reliable resume
569
571
  atomic: true,
570
572
  event: {
571
573
  type: 'started',
@@ -1450,8 +1452,11 @@ async function executeTask(task) {
1450
1452
  log(`Task #${displayNumber}: Project ${gitRemote} -> ${projectPath}`);
1451
1453
  }
1452
1454
 
1455
+ // Pre-assign session ID so we can store it at claim time (not rely on parsing stdout)
1456
+ const preAssignedSessionId = randomUUID();
1457
+
1453
1458
  // Atomic task claiming - must await to actually check the result
1454
- if (!(await claimTask(displayNumber, taskId))) {
1459
+ if (!(await claimTask(displayNumber, taskId, preAssignedSessionId))) {
1455
1460
  log(`Task #${displayNumber}: claim failed, skipping`);
1456
1461
  return null;
1457
1462
  }
@@ -1537,7 +1542,8 @@ async function executeTask(task) {
1537
1542
  '-p', prompt,
1538
1543
  '--allowedTools', allowedTools,
1539
1544
  '--output-format', 'json',
1540
- '--permission-mode', 'bypassPermissions'
1545
+ '--permission-mode', 'bypassPermissions',
1546
+ '--session-id', preAssignedSessionId
1541
1547
  ];
1542
1548
 
1543
1549
  try {
@@ -1557,7 +1563,8 @@ async function executeTask(task) {
1557
1563
  task,
1558
1564
  displayNumber,
1559
1565
  startTime: Date.now(),
1560
- projectPath
1566
+ projectPath,
1567
+ sessionId: preAssignedSessionId
1561
1568
  };
1562
1569
 
1563
1570
  runningTasks.set(displayNumber, taskInfo);
@@ -1652,17 +1659,16 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1652
1659
 
1653
1660
  log(`Task #${displayNumber} completed with code ${exitCode} (${duration}s)`);
1654
1661
 
1655
- // Extract session ID for both success and failure (needed for AI summary)
1656
- const buffer = taskStdoutBuffer.get(displayNumber) || [];
1657
- const sessionId = extractSessionIdFromStdout(taskInfo.process, buffer);
1662
+ // Session ID: prefer pre-assigned ID (reliable), fall back to stdout extraction
1663
+ const sessionId = taskInfo.sessionId || extractSessionIdFromStdout(taskInfo.process, taskStdoutBuffer.get(displayNumber) || []);
1658
1664
  const worktreePath = getWorktreePath(displayNumber, projectPath);
1659
1665
  const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
1660
1666
  const machineName = getMachineName() || 'Mac';
1661
1667
 
1662
1668
  if (sessionId) {
1663
- log(`Task #${displayNumber} session_id: ${sessionId}`);
1669
+ log(`Task #${displayNumber} session_id: ${sessionId}${taskInfo.sessionId ? ' (pre-assigned)' : ' (from stdout)'}`);
1664
1670
  } else {
1665
- log(`Task #${displayNumber} could not extract session_id`);
1671
+ log(`Task #${displayNumber} could not determine session_id`);
1666
1672
  }
1667
1673
 
1668
1674
  if (exitCode === 0) {
@@ -1798,7 +1804,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
1798
1804
  ? `${failureSummary}\nExit code ${exitCode}. Ran for ${durationStr} on ${machineName}.`
1799
1805
  : `Exit code ${exitCode}: ${stderr.slice(0, 200)}`;
1800
1806
 
1801
- await updateTaskStatus(displayNumber, 'failed', { error: errorMsg }, info.taskId);
1807
+ await updateTaskStatus(displayNumber, 'failed', { error: errorMsg, sessionId }, info.taskId);
1802
1808
 
1803
1809
  if (NOTIFY_ON_FAILURE) {
1804
1810
  sendMacNotification(
@@ -1942,7 +1948,7 @@ async function checkTimeouts() {
1942
1948
  runningTasks.delete(displayNumber);
1943
1949
 
1944
1950
  const timeoutError = `Task timed out after ${duration}s (limit: ${TASK_TIMEOUT_MS / 1000}s)`;
1945
- updateTaskStatus(displayNumber, 'failed', { error: timeoutError });
1951
+ updateTaskStatus(displayNumber, 'failed', { error: timeoutError, sessionId: taskInfo.sessionId });
1946
1952
 
1947
1953
  if (NOTIFY_ON_FAILURE) {
1948
1954
  sendMacNotification(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {