@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/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 { bold, red, cyan, dim } from './utils/colors.js';
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.0.0';
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);