@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 +33 -0
- package/lib/cli.js +35 -0
- package/lib/daemon.js +17 -11
- package/package.json +1 -1
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
|
-
//
|
|
1656
|
-
const
|
|
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
|
|
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(
|