@masslessai/push-todo 3.0.0 → 3.2.0
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/.claude-plugin/plugin.json +1 -1
- package/bin/push-keychain-helper +0 -0
- package/hooks/session-end.js +1 -1
- package/hooks/session-start.js +61 -4
- package/lib/api.js +59 -16
- package/lib/certainty.js +434 -0
- package/lib/cli.js +310 -4
- package/lib/connect.js +1120 -200
- package/lib/daemon-health.js +193 -0
- package/lib/daemon.js +1369 -0
- package/lib/fetch.js +16 -1
- package/lib/utils/git.js +43 -0
- package/lib/utils/screenshots.js +65 -0
- package/lib/watch.js +13 -2
- package/natives/KeychainHelper.swift +310 -93
- package/package.json +2 -1
- package/scripts/postinstall.js +306 -14
- package/scripts/preuninstall.js +66 -0
package/lib/cli.js
CHANGED
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { parseArgs } from 'util';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
8
9
|
import * as fetch from './fetch.js';
|
|
10
|
+
import * as api from './api.js';
|
|
9
11
|
import { runConnect } from './connect.js';
|
|
10
12
|
import { startWatch } from './watch.js';
|
|
11
|
-
import { showSettings, toggleSetting } from './config.js';
|
|
12
|
-
import {
|
|
13
|
+
import { showSettings, toggleSetting, setMaxBatchSize } from './config.js';
|
|
14
|
+
import { ensureDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './daemon-health.js';
|
|
15
|
+
import { getScreenshotPath, screenshotExists, openScreenshot } from './utils/screenshots.js';
|
|
16
|
+
import { bold, red, cyan, dim, green } from './utils/colors.js';
|
|
13
17
|
|
|
14
|
-
const VERSION = '3.
|
|
18
|
+
const VERSION = '3.2.0';
|
|
15
19
|
|
|
16
20
|
const HELP_TEXT = `
|
|
17
21
|
${bold('push-todo')} - Voice tasks from Push iOS app for Claude Code
|
|
@@ -36,7 +40,17 @@ ${bold('OPTIONS:')}
|
|
|
36
40
|
--search <query> Search tasks
|
|
37
41
|
--status Show connection and daemon status
|
|
38
42
|
--watch, -w Live terminal UI
|
|
43
|
+
--follow, -f With --watch: exit when all tasks complete
|
|
39
44
|
--setting [name] Show or toggle settings
|
|
45
|
+
--resume <number> Resume Claude session for a completed task
|
|
46
|
+
--view-screenshot <idx> Open screenshot for viewing (index or filename)
|
|
47
|
+
--learn-vocabulary <uuid> Contribute vocabulary for a task
|
|
48
|
+
--keywords <terms> Comma-separated vocabulary terms (with --learn-vocabulary)
|
|
49
|
+
--set-batch-size <N> Set max tasks for batch queue (1-20)
|
|
50
|
+
--daemon-status Show daemon status
|
|
51
|
+
--daemon-start Start daemon manually
|
|
52
|
+
--daemon-stop Stop daemon
|
|
53
|
+
--commands Show available user commands
|
|
40
54
|
--json Output as JSON
|
|
41
55
|
--version, -v Show version
|
|
42
56
|
--help, -h Show this help
|
|
@@ -49,6 +63,17 @@ ${bold('EXAMPLES:')}
|
|
|
49
63
|
push-todo search "auth bug" Search for tasks matching "auth bug"
|
|
50
64
|
push-todo connect Run connection diagnostics
|
|
51
65
|
|
|
66
|
+
${bold('CONNECT OPTIONS:')}
|
|
67
|
+
--reauth Force re-authentication
|
|
68
|
+
--client <type> Client type (claude-code, openai-codex, clawdbot)
|
|
69
|
+
--check-version Check for updates (JSON output)
|
|
70
|
+
--update Update to latest version
|
|
71
|
+
--validate-key Validate API key (JSON output)
|
|
72
|
+
--validate-machine Validate machine registration (JSON output)
|
|
73
|
+
--validate-project Validate project registration (JSON output)
|
|
74
|
+
--store-e2ee-key <key> Import E2EE encryption key
|
|
75
|
+
--description <text> Project description (with connect)
|
|
76
|
+
|
|
52
77
|
${bold('SETTINGS:')}
|
|
53
78
|
push-todo setting Show all settings
|
|
54
79
|
push-todo setting auto-commit Toggle auto-commit
|
|
@@ -69,10 +94,30 @@ const options = {
|
|
|
69
94
|
'search': { type: 'string' },
|
|
70
95
|
'status': { type: 'boolean' },
|
|
71
96
|
'watch': { type: 'boolean', short: 'w' },
|
|
97
|
+
'follow': { type: 'boolean', short: 'f' },
|
|
72
98
|
'setting': { type: 'string' },
|
|
99
|
+
'resume': { type: 'string' },
|
|
100
|
+
'view-screenshot': { type: 'string' },
|
|
101
|
+
'learn-vocabulary': { type: 'string' },
|
|
102
|
+
'keywords': { type: 'string' },
|
|
103
|
+
'set-batch-size': { type: 'string' },
|
|
104
|
+
'daemon-status': { type: 'boolean' },
|
|
105
|
+
'daemon-start': { type: 'boolean' },
|
|
106
|
+
'daemon-stop': { type: 'boolean' },
|
|
107
|
+
'commands': { type: 'boolean' },
|
|
73
108
|
'json': { type: 'boolean' },
|
|
74
109
|
'version': { type: 'boolean', short: 'v' },
|
|
75
|
-
'help': { type: 'boolean', short: 'h' }
|
|
110
|
+
'help': { type: 'boolean', short: 'h' },
|
|
111
|
+
// Connect options
|
|
112
|
+
'reauth': { type: 'boolean' },
|
|
113
|
+
'client': { type: 'string' },
|
|
114
|
+
'check-version': { type: 'boolean' },
|
|
115
|
+
'update': { type: 'boolean' },
|
|
116
|
+
'validate-key': { type: 'boolean' },
|
|
117
|
+
'validate-machine': { type: 'boolean' },
|
|
118
|
+
'validate-project': { type: 'boolean' },
|
|
119
|
+
'store-e2ee-key': { type: 'string' },
|
|
120
|
+
'description': { type: 'string' }
|
|
76
121
|
};
|
|
77
122
|
|
|
78
123
|
/**
|
|
@@ -115,6 +160,258 @@ export async function run(argv) {
|
|
|
115
160
|
return;
|
|
116
161
|
}
|
|
117
162
|
|
|
163
|
+
// Daemon management commands (don't auto-start daemon for these)
|
|
164
|
+
if (values['daemon-status']) {
|
|
165
|
+
const status = getDaemonStatus();
|
|
166
|
+
if (status.running) {
|
|
167
|
+
console.log(`${bold('Daemon:')} Running (PID: ${status.pid})`);
|
|
168
|
+
if (status.uptime) console.log(`${dim('Uptime:')} ${status.uptime}`);
|
|
169
|
+
if (status.version) console.log(`${dim('Version:')} ${status.version}`);
|
|
170
|
+
if (status.runningTasks?.length > 0) {
|
|
171
|
+
console.log(`\n${bold('Running Tasks:')}`);
|
|
172
|
+
for (const t of status.runningTasks) {
|
|
173
|
+
console.log(` #${t.displayNumber} - ${t.summary}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (status.completedToday?.length > 0) {
|
|
177
|
+
console.log(`\n${bold('Completed Today:')} ${status.completedToday.length} tasks`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
console.log(`${bold('Daemon:')} Not running`);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (values['daemon-start']) {
|
|
186
|
+
const status = getDaemonStatus();
|
|
187
|
+
if (status.running) {
|
|
188
|
+
console.log(`Daemon already running (PID: ${status.pid})`);
|
|
189
|
+
} else {
|
|
190
|
+
const success = startDaemon();
|
|
191
|
+
if (success) {
|
|
192
|
+
console.log('Daemon started');
|
|
193
|
+
} else {
|
|
194
|
+
console.error(red('Failed to start daemon'));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (values['daemon-stop']) {
|
|
202
|
+
const status = getDaemonStatus();
|
|
203
|
+
if (!status.running) {
|
|
204
|
+
console.log('Daemon is not running');
|
|
205
|
+
} else {
|
|
206
|
+
const success = stopDaemon();
|
|
207
|
+
if (success) {
|
|
208
|
+
console.log('Daemon stopped');
|
|
209
|
+
} else {
|
|
210
|
+
console.error(red('Failed to stop daemon'));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Handle --commands (simple user help)
|
|
218
|
+
if (values.commands) {
|
|
219
|
+
console.log(`
|
|
220
|
+
${bold('Push Voice Tasks - Commands')}
|
|
221
|
+
${'='.repeat(40)}
|
|
222
|
+
|
|
223
|
+
/push-todo Show your active tasks
|
|
224
|
+
/push-todo 427 Work on task #427
|
|
225
|
+
/push-todo search X Search tasks for 'X'
|
|
226
|
+
/push-todo connect Setup or fix problems
|
|
227
|
+
/push-todo review Check completed work
|
|
228
|
+
/push-todo status Show connection status
|
|
229
|
+
/push-todo watch Live monitor daemon tasks
|
|
230
|
+
/push-todo setting View/toggle settings
|
|
231
|
+
|
|
232
|
+
${dim('Options:')}
|
|
233
|
+
--all-projects See tasks from all projects
|
|
234
|
+
--backlog See deferred tasks only
|
|
235
|
+
--search "query" Search active & completed tasks
|
|
236
|
+
`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle --set-batch-size
|
|
241
|
+
if (values['set-batch-size']) {
|
|
242
|
+
const size = parseInt(values['set-batch-size'], 10);
|
|
243
|
+
if (isNaN(size) || size < 1 || size > 20) {
|
|
244
|
+
console.error(red('Batch size must be between 1 and 20'));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
if (setMaxBatchSize(size)) {
|
|
248
|
+
console.log(`Max batch size set to ${size}`);
|
|
249
|
+
} else {
|
|
250
|
+
console.error(red('Failed to set batch size'));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Handle --resume (resume Claude session for a completed task)
|
|
257
|
+
if (values.resume) {
|
|
258
|
+
const taskNum = values.resume.replace(/^#/, '');
|
|
259
|
+
const displayNumber = parseInt(taskNum, 10);
|
|
260
|
+
if (isNaN(displayNumber)) {
|
|
261
|
+
console.error(red(`Invalid task number: ${values.resume}`));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fetch task to get session_id
|
|
266
|
+
const task = await fetch.getTaskByNumber(displayNumber);
|
|
267
|
+
if (!task) {
|
|
268
|
+
console.error(red(`Task #${displayNumber} not found`));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const sessionId = task.execution_session_id || task.executionSessionId;
|
|
273
|
+
if (!sessionId) {
|
|
274
|
+
const executionStatus = task.execution_status || task.executionStatus;
|
|
275
|
+
if (executionStatus) {
|
|
276
|
+
console.log(`Task #${displayNumber} has execution status '${executionStatus}' but no session ID.`);
|
|
277
|
+
console.log('Session ID is captured when daemon completes a task.');
|
|
278
|
+
console.log();
|
|
279
|
+
console.log('Possible reasons:');
|
|
280
|
+
console.log(' - Task was completed before session capture was added');
|
|
281
|
+
console.log(' - Task was completed manually (not by daemon)');
|
|
282
|
+
console.log(" - Daemon couldn't extract session ID from Claude's output");
|
|
283
|
+
} else {
|
|
284
|
+
console.log(`Task #${displayNumber} was not executed by the daemon.`);
|
|
285
|
+
console.log('Session resume is only available for tasks completed by the Push daemon.');
|
|
286
|
+
}
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Launch claude --resume with the session ID
|
|
291
|
+
console.log(`Resuming session for task #${displayNumber}...`);
|
|
292
|
+
console.log(`Session ID: ${sessionId}`);
|
|
293
|
+
console.log();
|
|
294
|
+
|
|
295
|
+
// Use spawn with stdio: 'inherit' to give control to Claude
|
|
296
|
+
const child = spawn('claude', ['--resume', sessionId], {
|
|
297
|
+
stdio: 'inherit',
|
|
298
|
+
shell: true
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
child.on('error', (error) => {
|
|
302
|
+
if (error.code === 'ENOENT') {
|
|
303
|
+
console.error(red("Error: 'claude' command not found. Is Claude Code installed?"));
|
|
304
|
+
} else {
|
|
305
|
+
console.error(red(`Error launching Claude: ${error.message}`));
|
|
306
|
+
}
|
|
307
|
+
process.exit(1);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
child.on('close', (code) => {
|
|
311
|
+
process.exit(code || 0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle --view-screenshot (open screenshot for viewing)
|
|
318
|
+
if (values['view-screenshot']) {
|
|
319
|
+
// Get the first positional as task number (if provided)
|
|
320
|
+
const taskNum = positionals[0]?.replace(/^#/, '');
|
|
321
|
+
|
|
322
|
+
if (taskNum && /^\d+$/.test(taskNum)) {
|
|
323
|
+
// Task number provided - get task's screenshots
|
|
324
|
+
const displayNumber = parseInt(taskNum, 10);
|
|
325
|
+
const task = await fetch.getTaskByNumber(displayNumber);
|
|
326
|
+
|
|
327
|
+
if (!task) {
|
|
328
|
+
console.error(red(`Task #${displayNumber} not found`));
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const screenshots = task.screenshot_attachments || task.screenshotAttachments || [];
|
|
333
|
+
if (screenshots.length === 0) {
|
|
334
|
+
console.error(red(`Task #${displayNumber} has no screenshot attachments`));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Try to parse as index
|
|
339
|
+
let filename;
|
|
340
|
+
const idx = parseInt(values['view-screenshot'], 10);
|
|
341
|
+
if (!isNaN(idx)) {
|
|
342
|
+
if (idx < 0 || idx >= screenshots.length) {
|
|
343
|
+
console.error(red(`Screenshot index ${idx} out of range (0-${screenshots.length - 1})`));
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
filename = screenshots[idx].imageFilename || screenshots[idx].image_filename;
|
|
347
|
+
} else {
|
|
348
|
+
// Not an index, treat as filename
|
|
349
|
+
filename = values['view-screenshot'];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const filepath = getScreenshotPath(filename);
|
|
353
|
+
try {
|
|
354
|
+
await openScreenshot(filepath);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error(red(error.message));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
// No task number, treat arg as filename
|
|
361
|
+
const filepath = getScreenshotPath(values['view-screenshot']);
|
|
362
|
+
try {
|
|
363
|
+
await openScreenshot(filepath);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error(red(error.message));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Handle --learn-vocabulary (contribute vocabulary terms)
|
|
373
|
+
if (values['learn-vocabulary']) {
|
|
374
|
+
if (!values.keywords) {
|
|
375
|
+
console.error(red('--keywords required with --learn-vocabulary'));
|
|
376
|
+
console.error("Example: --learn-vocabulary TASK_ID --keywords 'realtime,sync,websocket'");
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Parse comma-separated keywords
|
|
381
|
+
const keywords = values.keywords.split(',').map(k => k.trim()).filter(Boolean);
|
|
382
|
+
if (keywords.length === 0) {
|
|
383
|
+
console.error(red('No valid keywords provided'));
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const result = await api.learnVocabulary(values['learn-vocabulary'], keywords);
|
|
389
|
+
|
|
390
|
+
if (values.json) {
|
|
391
|
+
console.log(JSON.stringify(result, null, 2));
|
|
392
|
+
} else {
|
|
393
|
+
const added = result.keywords_added || [];
|
|
394
|
+
const dupes = result.keywords_duplicate || [];
|
|
395
|
+
const total = result.total_keywords || 0;
|
|
396
|
+
|
|
397
|
+
if (added.length > 0) {
|
|
398
|
+
console.log(green(`Added ${added.length} new terms: ${added.join(', ')}`));
|
|
399
|
+
}
|
|
400
|
+
if (dupes.length > 0) {
|
|
401
|
+
console.log(dim(`Already known: ${dupes.join(', ')}`));
|
|
402
|
+
}
|
|
403
|
+
console.log(`Total vocabulary: ${total} terms`);
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(red(`Failed to learn vocabulary: ${error.message}`));
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Auto-start daemon on every command (self-healing behavior)
|
|
413
|
+
ensureDaemonRunning();
|
|
414
|
+
|
|
118
415
|
// Get the command (first positional)
|
|
119
416
|
const command = positionals[0];
|
|
120
417
|
|
|
@@ -128,6 +425,15 @@ export async function run(argv) {
|
|
|
128
425
|
return fetch.runReview(values);
|
|
129
426
|
}
|
|
130
427
|
|
|
428
|
+
// Setting command (positional form: push-todo setting [name])
|
|
429
|
+
if (command === 'setting') {
|
|
430
|
+
const settingName = positionals[1];
|
|
431
|
+
if (settingName) {
|
|
432
|
+
return toggleSetting(settingName);
|
|
433
|
+
}
|
|
434
|
+
return showSettings();
|
|
435
|
+
}
|
|
436
|
+
|
|
131
437
|
// Search command (positional form)
|
|
132
438
|
if (command === 'search' && positionals[1]) {
|
|
133
439
|
return fetch.searchTasks(positionals.slice(1).join(' '), values);
|