@masslessai/push-todo 4.4.0 → 4.5.1

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/SKILL.md CHANGED
@@ -433,6 +433,12 @@ The `push-todo` CLI supports these commands:
433
433
  | `push-todo --status` | Show connection status |
434
434
  | `push-todo --mark-completed <uuid>` | Mark task as completed |
435
435
  | `push-todo --json` | Output as JSON |
436
+ | `push-todo create <title>` | Create a todo from CLI |
437
+ | `push-todo create <title> --remind <text>` | Create with reminder (e.g., "tomorrow night", "in 2 hours") |
438
+ | `push-todo create <title> --remind-at <iso>` | Create with exact reminder date (ISO8601) |
439
+ | `push-todo create <title> --alarm` | Mark reminder as urgent (bypasses Focus) |
440
+ | `push-todo create <title> --content <text>` | Create with detailed content |
441
+ | `push-todo create <title> --backlog` | Create as backlog item |
436
442
  | `push-todo schedule add` | Create a remote schedule (Supabase-backed) |
437
443
  | `push-todo schedule list` | List all remote schedules |
438
444
  | `push-todo schedule remove <id>` | Remove a schedule |
package/lib/cli.js CHANGED
@@ -132,6 +132,7 @@ ${bold('SCHEDULE (remote scheduled jobs):')}
132
132
  --create-todo <title> Create a new todo each fire
133
133
  --queue-todo <todoId> Re-queue an existing todo each fire
134
134
  --git-remote <remote> Route created todos to a project
135
+ --agent-type <type> Agent: claude-code, openclaw, openai-codex
135
136
  --content <body> Expanded content for created todos
136
137
  push-todo schedule list List all schedules
137
138
  push-todo schedule remove <id> Remove a schedule
@@ -201,6 +202,7 @@ const options = {
201
202
  'cron': { type: 'string' },
202
203
  'create-todo': { type: 'string' },
203
204
  'git-remote': { type: 'string' },
205
+ 'agent-type': { type: 'string' },
204
206
  'queue-todo': { type: 'string' },
205
207
  // Skill CLI options (Phase 3)
206
208
  'report-progress': { type: 'string' },
@@ -825,6 +827,9 @@ export async function run(argv) {
825
827
  process.exit(1);
826
828
  }
827
829
 
830
+ // Resolve agent type: explicit flag > auto-detect from caller
831
+ const agentType = values['agent-type'] || null;
832
+
828
833
  try {
829
834
  const response = await api.apiRequest('manage-schedules', {
830
835
  method: 'POST',
@@ -833,6 +838,7 @@ export async function run(argv) {
833
838
  scheduleType,
834
839
  scheduleValue,
835
840
  actionType,
841
+ agentType,
836
842
  actionTitle,
837
843
  actionContent,
838
844
  gitRemote,
@@ -884,7 +890,8 @@ export async function run(argv) {
884
890
  const actionStr = s.action_type === 'create-todo'
885
891
  ? `create-todo: ${s.action_title || ''}`
886
892
  : `queue-todo: ${s.todo_id || '?'}`;
887
- console.log(` ${status} ${s.name} [${schedStr}] ${actionStr}`);
893
+ const agentStr = s.agent_type && s.agent_type !== 'claude-code' ? ` (${s.agent_type})` : '';
894
+ console.log(` ${status} ${s.name} [${schedStr}] → ${actionStr}${agentStr}`);
888
895
  console.log(dim(` ID: ${s.id.slice(0, 8)} | Next: ${s.next_run_at || 'N/A'} | Last: ${s.last_run_at || 'never'}`));
889
896
  }
890
897
  } catch (error) {
@@ -958,6 +965,7 @@ ${bold('Examples:')}
958
965
  push-todo schedule add --name "Daily standup" --every 24h --create-todo "Write standup update"
959
966
  push-todo schedule add --name "Weekly review" --cron "0 9 * * 1" --create-todo "Weekly code review" --git-remote github.com/user/repo
960
967
  push-todo schedule add --name "Re-run task" --every 4h --queue-todo <todoId>
968
+ push-todo schedule add --name "Codex review" --every 12h --create-todo "Review PRs" --git-remote github.com/user/repo --agent-type openai-codex
961
969
  `);
962
970
  return;
963
971
  }
package/lib/cron.js CHANGED
@@ -196,16 +196,21 @@ export function computeNextRun(schedule, fromDate = new Date()) {
196
196
  * @param {Function} [logFn] - Optional log function
197
197
  * @param {Object} [context] - Injected dependencies from daemon
198
198
  * @param {Function} [context.apiRequest] - API request function
199
+ * @param {string} [context.machineId] - This machine's ID for targeted schedule filtering
199
200
  */
200
201
  export async function checkAndRunRemoteSchedules(logFn, context = {}) {
201
202
  const log = logFn || (() => {});
202
203
 
203
204
  if (!context.apiRequest) return;
204
205
 
205
- // 1. Fetch due schedules
206
+ // 1. Fetch due schedules (filtered by machine if available)
206
207
  let schedules;
207
208
  try {
208
- const response = await context.apiRequest('manage-schedules?due=true', {
209
+ let endpoint = 'manage-schedules?due=true';
210
+ if (context.machineId) {
211
+ endpoint += `&machine_id=${encodeURIComponent(context.machineId)}`;
212
+ }
213
+ const response = await context.apiRequest(endpoint, {
209
214
  method: 'GET',
210
215
  });
211
216
  if (!response.ok) {
@@ -226,6 +231,7 @@ export async function checkAndRunRemoteSchedules(logFn, context = {}) {
226
231
  // 2. Fire each due schedule
227
232
  for (const schedule of schedules) {
228
233
  const expectedNextRunAt = schedule.next_run_at;
234
+ let actionSucceeded = false;
229
235
 
230
236
  try {
231
237
  if (schedule.action_type === 'create-todo') {
@@ -238,7 +244,8 @@ export async function checkAndRunRemoteSchedules(logFn, context = {}) {
238
244
  };
239
245
  if (schedule.git_remote) {
240
246
  payload.gitRemote = schedule.git_remote;
241
- payload.actionType = 'claude-code';
247
+ // Use schedule's agent_type instead of hardcoding claude-code
248
+ payload.actionType = schedule.agent_type || 'claude-code';
242
249
  }
243
250
 
244
251
  const todoResponse = await context.apiRequest('create-todo', {
@@ -248,6 +255,7 @@ export async function checkAndRunRemoteSchedules(logFn, context = {}) {
248
255
  if (todoResponse.ok) {
249
256
  const todoData = await todoResponse.json();
250
257
  log(`Schedule "${schedule.name}": created todo #${todoData.todo?.displayNumber || '?'}`);
258
+ actionSucceeded = true;
251
259
  } else {
252
260
  log(`Schedule "${schedule.name}": create-todo failed (HTTP ${todoResponse.status})`);
253
261
  }
@@ -277,6 +285,7 @@ export async function checkAndRunRemoteSchedules(logFn, context = {}) {
277
285
  });
278
286
  if (queueResponse.ok) {
279
287
  log(`Schedule "${schedule.name}": queued todo ${schedule.todo_id}`);
288
+ actionSucceeded = true;
280
289
  } else {
281
290
  log(`Schedule "${schedule.name}": queue-todo failed (HTTP ${queueResponse.status})`);
282
291
  }
@@ -285,7 +294,12 @@ export async function checkAndRunRemoteSchedules(logFn, context = {}) {
285
294
  log(`Schedule "${schedule.name}": execution error: ${error.message}`);
286
295
  }
287
296
 
288
- // 3. Advance next_run_at (with optimistic lock)
297
+ // 3. Advance next_run_at only if action succeeded (prevents silent lost fires)
298
+ if (!actionSucceeded) {
299
+ log(`Schedule "${schedule.name}": action failed, will retry next cycle`);
300
+ continue;
301
+ }
302
+
289
303
  const now = new Date();
290
304
  const scheduleConfig = { type: schedule.schedule_type, value: schedule.schedule_value };
291
305
 
package/lib/daemon.js CHANGED
@@ -550,85 +550,6 @@ async function fetchQueuedTasks() {
550
550
  }
551
551
  }
552
552
 
553
- // ==================== Scheduled Reminder Bridge ====================
554
-
555
- async function fetchScheduledTodos() {
556
- try {
557
- const machineId = getMachineId();
558
- const params = new URLSearchParams();
559
- params.set('scheduled_before', new Date().toISOString());
560
- if (machineId) {
561
- params.set('machine_id', machineId);
562
- }
563
-
564
- // Include registered git_remotes so backend can scope to this machine's projects
565
- const projects = getListedProjects();
566
- const gitRemotes = Object.keys(projects);
567
- const headers = {};
568
- if (machineId && gitRemotes.length > 0) {
569
- headers['X-Machine-Id'] = machineId;
570
- headers['X-Git-Remotes'] = gitRemotes.join(',');
571
- }
572
-
573
- const response = await apiRequest(`synced-todos?${params}`, { headers });
574
- if (!response.ok) return [];
575
-
576
- const data = await response.json();
577
- return data.todos || [];
578
- } catch (error) {
579
- logError(`Failed to fetch scheduled todos: ${error.message}`);
580
- return [];
581
- }
582
- }
583
-
584
- async function checkAndQueueScheduledTodos() {
585
- const scheduledTodos = await fetchScheduledTodos();
586
- if (scheduledTodos.length === 0) return;
587
-
588
- const registeredProjects = getListedProjects();
589
-
590
- for (const todo of scheduledTodos) {
591
- const dn = todo.displayNumber || todo.display_number;
592
- const todoId = todo.id;
593
-
594
- // Skip tasks already in an execution state (running, queued, completed, failed, etc.)
595
- const execStatus = todo.executionStatus || todo.execution_status;
596
- if (execStatus && execStatus !== 'none') {
597
- continue;
598
- }
599
-
600
- // Skip completed tasks
601
- if (todo.isCompleted || todo.is_completed) {
602
- continue;
603
- }
604
-
605
- // Skip tasks whose project is not registered on this machine
606
- const gitRemote = todo.gitRemote || todo.git_remote;
607
- if (gitRemote && Object.keys(registeredProjects).length > 0) {
608
- if (!(gitRemote in registeredProjects)) {
609
- log(`Schedule skipped #${dn}: project ${gitRemote} not registered on this machine`);
610
- continue;
611
- }
612
- }
613
-
614
- log(`Schedule triggered for #${dn} (reminder_date: ${todo.reminderDate || todo.reminder_date})`);
615
-
616
- try {
617
- await updateTaskStatus(dn, 'queued', {
618
- event: {
619
- type: 'scheduled_trigger',
620
- timestamp: new Date().toISOString(),
621
- summary: 'Auto-queued: reminder time reached',
622
- }
623
- }, todoId);
624
-
625
- log(`Queued #${dn} via schedule trigger`);
626
- } catch (error) {
627
- logError(`Failed to queue scheduled todo #${dn}: ${error.message}`);
628
- }
629
- }
630
- }
631
-
632
553
  // ==================== Task Status Updates ====================
633
554
 
634
555
  async function updateTaskStatus(displayNumber, status, extra = {}, todoId = null) {
@@ -3239,18 +3160,11 @@ async function mainLoop() {
3239
3160
  try {
3240
3161
  await checkTimeouts();
3241
3162
 
3242
- // Schedule bridge: auto-queue todos whose reminder time has arrived
3243
- try {
3244
- await checkAndQueueScheduledTodos();
3245
- } catch (error) {
3246
- logError(`Scheduled todo check error: ${error.message}`);
3247
- }
3248
-
3249
3163
  await pollAndExecute();
3250
3164
 
3251
3165
  // Remote schedules from Supabase
3252
3166
  try {
3253
- await checkAndRunRemoteSchedules(log, { apiRequest });
3167
+ await checkAndRunRemoteSchedules(log, { apiRequest, machineId: getMachineId() });
3254
3168
  } catch (error) {
3255
3169
  logError(`Remote schedules error: ${error.message}`);
3256
3170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.4.0",
3
+ "version": "4.5.1",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "hooks/",
20
20
  "natives/",
21
21
  "scripts/",
22
+ "skills/",
22
23
  "SKILL.md",
23
24
  "LICENSE"
24
25
  ],
@@ -6,10 +6,11 @@
6
6
  * 1. Claude Code - symlink to ~/.claude/skills/ (gives clean /push-todo command)
7
7
  * 2. OpenAI Codex - AGENTS.md in ~/.codex/
8
8
  * 3. OpenClaw - SKILL.md in ~/.openclaw/skills/ (legacy: ~/.clawdbot/)
9
- * 4. Downloads native keychain helper binary (macOS)
9
+ * 4. Bundled skills - auto-discovers bundled skills and creates per-skill symlinks
10
+ * 5. Downloads native keychain helper binary (macOS)
10
11
  */
11
12
 
12
- import { createWriteStream, existsSync, mkdirSync, unlinkSync, readFileSync, writeFileSync, symlinkSync, lstatSync, readlinkSync, rmSync, appendFileSync } from 'fs';
13
+ import { createWriteStream, existsSync, mkdirSync, unlinkSync, readFileSync, writeFileSync, symlinkSync, lstatSync, readlinkSync, rmSync, appendFileSync, readdirSync } from 'fs';
13
14
  import { chmod, stat } from 'fs/promises';
14
15
  import { pipeline } from 'stream/promises';
15
16
  import { join, dirname } from 'path';
@@ -301,6 +302,68 @@ function setupOpenClaw() {
301
302
  }
302
303
  }
303
304
 
305
+ /**
306
+ * Auto-discover bundled skills and create per-skill symlinks.
307
+ * Each bundled skill gets: <targetDir>/push-<name> -> PACKAGE_ROOT/skills/<name>
308
+ *
309
+ * Claude Code scans one level deep (~/.claude/skills/X/SKILL.md), but bundled
310
+ * skills are two levels deep from the root push-todo symlink. Per-skill symlinks
311
+ * give each bundled skill its own top-level entry for independent discovery.
312
+ *
313
+ * @param {string} targetSkillsDir - e.g. ~/.claude/skills/
314
+ * @param {string} clientLabel - e.g. "Claude Code"
315
+ * @returns {string[]} names of skills that were symlinked
316
+ */
317
+ function setupBundledSkills(targetSkillsDir, clientLabel) {
318
+ const bundledDir = join(PACKAGE_ROOT, 'skills');
319
+ if (!existsSync(bundledDir)) return [];
320
+
321
+ let entries;
322
+ try {
323
+ entries = readdirSync(bundledDir);
324
+ } catch {
325
+ return [];
326
+ }
327
+
328
+ const installed = [];
329
+
330
+ for (const name of entries) {
331
+ const skillSource = join(bundledDir, name);
332
+ const skillMd = join(skillSource, 'SKILL.md');
333
+
334
+ // Only process directories containing SKILL.md
335
+ if (!existsSync(skillMd)) continue;
336
+
337
+ // Namespace with push- prefix (skip if already prefixed)
338
+ const linkName = name.startsWith('push-') ? name : `push-${name}`;
339
+ const linkPath = join(targetSkillsDir, linkName);
340
+
341
+ try {
342
+ if (existsSync(linkPath)) {
343
+ const stats = lstatSync(linkPath);
344
+ if (stats.isSymbolicLink()) {
345
+ const currentTarget = readlinkSync(linkPath);
346
+ if (currentTarget === skillSource) {
347
+ installed.push(linkName);
348
+ continue; // Already correct
349
+ }
350
+ unlinkSync(linkPath);
351
+ } else {
352
+ rmSync(linkPath, { recursive: true });
353
+ }
354
+ }
355
+
356
+ symlinkSync(skillSource, linkPath);
357
+ installed.push(linkName);
358
+ console.log(`[push-todo] ${clientLabel}: Bundled skill installed: ${linkName}`);
359
+ } catch (err) {
360
+ console.log(`[push-todo] ${clientLabel}: Failed to install bundled skill ${linkName}: ${err.message}`);
361
+ }
362
+ }
363
+
364
+ return installed;
365
+ }
366
+
304
367
  /**
305
368
  * Download a file from URL to destination.
306
369
  *
@@ -356,15 +419,36 @@ async function main() {
356
419
  // Step 3: Set up Claude Code skill symlink
357
420
  console.log('[push-todo] Setting up Claude Code skill...');
358
421
  const claudeSuccess = setupClaudeSkill();
422
+ if (claudeSuccess) {
423
+ const bundled = setupBundledSkills(SKILL_DIR, 'Claude Code');
424
+ if (bundled.length > 0) {
425
+ console.log(`[push-todo] Claude Code: ${bundled.length} bundled skill(s) installed`);
426
+ }
427
+ }
359
428
  console.log('');
360
429
 
361
430
  // Step 4: Set up OpenAI Codex (if installed)
362
431
  const codexSuccess = setupCodex();
363
- if (codexSuccess) console.log('');
432
+ if (codexSuccess) {
433
+ const codexSkillsDir = join(CODEX_DIR, 'skills');
434
+ const bundled = setupBundledSkills(codexSkillsDir, 'Codex');
435
+ if (bundled.length > 0) {
436
+ console.log(`[push-todo] Codex: ${bundled.length} bundled skill(s) installed`);
437
+ }
438
+ console.log('');
439
+ }
364
440
 
365
441
  // Step 5: Set up OpenClaw (if installed — formerly Clawdbot)
366
442
  const openclawSuccess = setupOpenClaw();
367
- if (openclawSuccess) console.log('');
443
+ if (openclawSuccess) {
444
+ const clawDir = existsSync(OPENCLAW_DIR) ? OPENCLAW_DIR : OPENCLAW_LEGACY_DIR;
445
+ const clawSkillsDir = join(clawDir, 'skills');
446
+ const bundled = setupBundledSkills(clawSkillsDir, 'OpenClaw');
447
+ if (bundled.length > 0) {
448
+ console.log(`[push-todo] OpenClaw: ${bundled.length} bundled skill(s) installed`);
449
+ }
450
+ console.log('');
451
+ }
368
452
 
369
453
  // Track which clients were set up
370
454
  const clients = [];