@masslessai/push-todo 4.2.9 → 4.4.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/SKILL.md CHANGED
@@ -433,6 +433,11 @@ 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 schedule add` | Create a remote schedule (Supabase-backed) |
437
+ | `push-todo schedule list` | List all remote schedules |
438
+ | `push-todo schedule remove <id>` | Remove a schedule |
439
+ | `push-todo schedule enable <id>` | Enable a schedule |
440
+ | `push-todo schedule disable <id>` | Disable a schedule |
436
441
 
437
442
  ## What is Push?
438
443
 
package/lib/api.js CHANGED
@@ -154,9 +154,12 @@ export async function markTaskCompleted(taskId, comment = '') {
154
154
  * @param {string} options.title - Todo title (required)
155
155
  * @param {string|null} options.content - Detailed content (optional)
156
156
  * @param {boolean} options.backlog - Whether to create as backlog item
157
- * @returns {Promise<Object>} Created todo with { id, displayNumber, title, createdAt }
157
+ * @param {string|null} options.reminderDate - ISO8601 reminder date (optional)
158
+ * @param {string|null} options.reminderTimeSource - Time source enum (optional)
159
+ * @param {boolean} options.alarmEnabled - Whether to mark as urgent (optional)
160
+ * @returns {Promise<Object>} Created todo with { id, displayNumber, title, createdAt, reminderDate, reminderEnabled }
158
161
  */
159
- export async function createTodo({ title, content = null, backlog = false }) {
162
+ export async function createTodo({ title, content = null, backlog = false, reminderDate = null, reminderTimeSource = null, alarmEnabled = false }) {
160
163
  const response = await apiRequest('create-todo', {
161
164
  method: 'POST',
162
165
  body: JSON.stringify({
@@ -164,6 +167,10 @@ export async function createTodo({ title, content = null, backlog = false }) {
164
167
  normalizedContent: content || null,
165
168
  isBacklog: backlog,
166
169
  createdByClient: 'cli',
170
+ reminderDate: reminderDate || null,
171
+ reminderEnabled: reminderDate != null,
172
+ reminderTimeSource: reminderTimeSource || null,
173
+ alarmEnabled,
167
174
  }),
168
175
  });
169
176
 
@@ -521,4 +528,4 @@ export async function requestInput(todoId, question, timeoutMs = 300000) {
521
528
  return null; // Timeout
522
529
  }
523
530
 
524
- export { API_BASE };
531
+ export { API_BASE, apiRequest };
package/lib/cli.js CHANGED
@@ -19,6 +19,7 @@ import { install as installLaunchAgent, uninstall as uninstallLaunchAgent, getSt
19
19
  import { getScreenshotPath, screenshotExists, openScreenshot } from './utils/screenshots.js';
20
20
  import { bold, red, cyan, dim, green } from './utils/colors.js';
21
21
  import { getMachineId } from './machine-id.js';
22
+ import { parseReminder } from './reminder-parser.js';
22
23
 
23
24
  const __filename = fileURLToPath(import.meta.url);
24
25
  const __dirname = dirname(__filename);
@@ -45,10 +46,14 @@ ${bold('USAGE:')}
45
46
  push-todo [options] List active tasks
46
47
  push-todo <number> Show specific task
47
48
  push-todo create <title> Create a new todo
49
+ --remind <text> Set reminder ("tomorrow night", "in 2 hours")
50
+ --remind-at <iso> Set reminder at exact ISO8601 time
51
+ --alarm Mark as urgent (bypasses Focus)
48
52
  push-todo connect Run connection doctor
49
53
  push-todo search <query> Search tasks
50
54
  push-todo review Review completed tasks
51
55
  push-todo update Update CLI, check agents, refresh projects
56
+ push-todo schedule Manage remote schedules (add/list/remove)
52
57
 
53
58
  ${bold('OPTIONS:')}
54
59
  --all-projects, -a List tasks from all projects
@@ -91,6 +96,8 @@ ${bold('EXAMPLES:')}
91
96
  push-todo List active tasks for current project
92
97
  push-todo 427 Show task #427
93
98
  push-todo create "Fix auth bug" Create a new todo
99
+ push-todo create "Debug" --remind "tomorrow night"
100
+ push-todo create "Call" --remind "at 3pm" --alarm
94
101
  push-todo create "Item" --backlog Create as backlog item
95
102
  push-todo -a List all tasks across projects
96
103
  push-todo --queue 1,2,3 Queue tasks 1, 2, 3 for daemon
@@ -116,17 +123,20 @@ ${bold('CONFIRM (for daemon skills):')}
116
123
  --metadata <json> Optional JSON metadata for rich rendering
117
124
  --task <number> Display number (auto-detected in daemon)
118
125
 
119
- ${bold('CRON (scheduled jobs):')}
120
- push-todo cron add Add a cron job
121
- --name <name> Job name (required)
122
- --every <interval> Repeat interval: 30m, 1h, 24h, 7d
123
- --at <iso-date> One-shot at specific time
124
- --cron <expression> 5-field cron expression
125
- --notify <message> Send Mac notification
126
- --create-todo <content> Create todo reminder
127
- --health-check <path> Run codebase health check (scope: general|tests|deps)
128
- push-todo cron list List all cron jobs
129
- push-todo cron remove <id> Remove a cron job by ID
126
+ ${bold('SCHEDULE (remote scheduled jobs):')}
127
+ push-todo schedule add Create a schedule
128
+ --name <name> Schedule name (required)
129
+ --every <interval> Repeat interval: 30m, 1h, 24h, 7d
130
+ --at <iso-date> One-shot at specific time
131
+ --cron <expression> 5-field cron expression
132
+ --create-todo <title> Create a new todo each fire
133
+ --queue-todo <todoId> Re-queue an existing todo each fire
134
+ --git-remote <remote> Route created todos to a project
135
+ --content <body> Expanded content for created todos
136
+ push-todo schedule list List all schedules
137
+ push-todo schedule remove <id> Remove a schedule
138
+ push-todo schedule enable <id> Enable a schedule
139
+ push-todo schedule disable <id> Disable a schedule
130
140
 
131
141
  ${bold('SETTINGS:')}
132
142
  push-todo setting Show all settings
@@ -174,22 +184,24 @@ const options = {
174
184
  'store-e2ee-key': { type: 'string' },
175
185
  'description': { type: 'string' },
176
186
  'auto': { type: 'boolean' },
187
+ // Create command options
188
+ 'remind': { type: 'string' },
189
+ 'remind-at': { type: 'string' },
190
+ 'alarm': { type: 'boolean' },
177
191
  // Confirm command options
178
192
  'type': { type: 'string' },
179
193
  'title': { type: 'string' },
180
194
  'content': { type: 'string' },
181
195
  'metadata': { type: 'string' },
182
196
  'task': { type: 'string' },
183
- // Cron command options
197
+ // Schedule command options
184
198
  'name': { type: 'string' },
185
199
  'every': { type: 'string' },
186
200
  'at': { type: 'string' },
187
201
  'cron': { type: 'string' },
188
202
  'create-todo': { type: 'string' },
189
- 'notify': { type: 'string' },
190
- 'queue-execution': { type: 'string' },
191
- 'health-check': { type: 'string' },
192
- 'scope': { type: 'string' },
203
+ 'git-remote': { type: 'string' },
204
+ 'queue-todo': { type: 'string' },
193
205
  // Skill CLI options (Phase 3)
194
206
  'report-progress': { type: 'string' },
195
207
  'phase': { type: 'string' },
@@ -682,22 +694,74 @@ export async function run(argv) {
682
694
  console.error('');
683
695
  console.error('Usage:');
684
696
  console.error(' push-todo create "Fix the auth bug"');
685
- console.error(' push-todo create "Fix the auth bug" --backlog');
686
- console.error(' push-todo create "Fix the auth bug" --content "Detailed description"');
697
+ console.error(' push-todo create "Debug daemon" --remind "tomorrow night"');
698
+ console.error(' push-todo create "Call dentist" --remind "at 3pm" --alarm');
699
+ console.error(' push-todo create "Deploy" --remind-at "2026-03-03T14:00:00"');
687
700
  process.exit(1);
688
701
  }
689
702
 
703
+ // Conflict check
704
+ if (values.remind && values['remind-at']) {
705
+ console.error(red('Error: Use --remind OR --remind-at, not both.'));
706
+ process.exit(1);
707
+ }
708
+
709
+ // Parse reminder
710
+ let reminderDate = null;
711
+ let reminderTimeSource = null;
712
+ const alarmEnabled = values.alarm || false;
713
+
714
+ if (values['remind-at']) {
715
+ const parsed = new Date(values['remind-at']);
716
+ if (isNaN(parsed.getTime())) {
717
+ console.error(red('Error: --remind-at must be a valid ISO8601 date.'));
718
+ console.error(dim(' Example: 2026-03-03T14:00:00'));
719
+ process.exit(1);
720
+ }
721
+ if (parsed <= new Date()) {
722
+ console.error(red('Error: --remind-at must be in the future.'));
723
+ process.exit(1);
724
+ }
725
+ reminderDate = parsed.toISOString();
726
+ reminderTimeSource = 'userSpecified';
727
+ } else if (values.remind) {
728
+ const result = parseReminder(values.remind);
729
+ if (!result.date) {
730
+ console.error(red(`Error: Could not parse reminder: "${values.remind}"`));
731
+ console.error('');
732
+ console.error('Examples:');
733
+ console.error(' --remind "tomorrow"');
734
+ console.error(' --remind "tonight"');
735
+ console.error(' --remind "in 2 hours"');
736
+ console.error(' --remind "next monday at 3pm"');
737
+ process.exit(1);
738
+ }
739
+ reminderDate = result.date.toISOString();
740
+ reminderTimeSource = result.timeSource;
741
+ }
742
+
690
743
  try {
691
744
  const todo = await api.createTodo({
692
745
  title,
693
746
  content: values.content || null,
694
747
  backlog: values.backlog || false,
748
+ reminderDate,
749
+ reminderTimeSource,
750
+ alarmEnabled,
695
751
  });
696
752
 
697
753
  if (values.json) {
698
754
  console.log(JSON.stringify(todo, null, 2));
699
755
  } else {
700
- console.log(green(`Created todo #${todo.displayNumber}: ${todo.title}`));
756
+ let msg = green(`Created todo #${todo.displayNumber}: ${todo.title}`);
757
+ if (reminderDate) {
758
+ const d = new Date(reminderDate);
759
+ msg += `\n ${dim('Reminder:')} ${d.toLocaleString()}`;
760
+ }
761
+ if (alarmEnabled) {
762
+ msg += ` ${dim('(urgent)')}`;
763
+ }
764
+ console.log(msg);
701
765
  }
702
766
  } catch (error) {
703
767
  console.error(red(`Failed to create todo: ${error.message}`));
@@ -706,99 +770,194 @@ export async function run(argv) {
706
770
  return;
707
771
  }
708
772
 
709
- // Cron command - scheduled jobs
710
- if (command === 'cron') {
711
- const { addJob, removeJob, listJobs } = await import('./cron.js');
773
+ // Schedule command - Supabase-backed unified scheduling
774
+ if (command === 'schedule') {
775
+ const { computeNextRun } = await import('./cron.js');
712
776
  const subCommand = positionals[1];
713
777
 
714
778
  if (subCommand === 'add') {
715
779
  if (!values.name) {
716
- console.error(red('--name is required for cron add'));
780
+ console.error(red('--name is required for schedule add'));
717
781
  process.exit(1);
718
782
  }
719
783
 
720
- // Determine schedule
721
- let schedule;
784
+ // Determine schedule timing
785
+ let scheduleType, scheduleValue;
722
786
  if (values.every) {
723
- schedule = { type: 'every', value: values.every };
787
+ scheduleType = 'every';
788
+ scheduleValue = values.every;
724
789
  } else if (values.at) {
725
- schedule = { type: 'at', value: values.at };
790
+ scheduleType = 'at';
791
+ scheduleValue = values.at;
726
792
  } else if (values.cron) {
727
- schedule = { type: 'cron', value: values.cron };
793
+ scheduleType = 'cron';
794
+ scheduleValue = values.cron;
728
795
  } else {
729
796
  console.error(red('Schedule required: --every, --at, or --cron'));
730
797
  process.exit(1);
731
798
  }
732
799
 
800
+ // Validate schedule and compute next run
801
+ let nextRunAt;
802
+ try {
803
+ nextRunAt = computeNextRun({ type: scheduleType, value: scheduleValue });
804
+ if (!nextRunAt) {
805
+ console.error(red('Schedule has no future run time'));
806
+ process.exit(1);
807
+ }
808
+ } catch (error) {
809
+ console.error(red(`Invalid schedule: ${error.message}`));
810
+ process.exit(1);
811
+ }
812
+
733
813
  // Determine action
734
- let action;
814
+ let actionType, actionTitle, actionContent, gitRemote, todoId;
735
815
  if (values['create-todo']) {
736
- action = { type: 'create-todo', content: values['create-todo'] };
737
- } else if (values.notify) {
738
- action = { type: 'notify', content: values.notify };
739
- } else if (values['queue-execution']) {
740
- action = { type: 'queue-execution', todoId: values['queue-execution'] };
741
- } else if (values['health-check']) {
742
- action = {
743
- type: 'health-check',
744
- projectPath: values['health-check'],
745
- scope: values.scope || 'general',
746
- };
816
+ actionType = 'create-todo';
817
+ actionTitle = values['create-todo'];
818
+ actionContent = values.content || null;
819
+ gitRemote = values['git-remote'] || null;
820
+ } else if (values['queue-todo']) {
821
+ actionType = 'queue-todo';
822
+ todoId = values['queue-todo'];
747
823
  } else {
748
- console.error(red('Action required: --create-todo, --notify, --queue-execution, or --health-check'));
824
+ console.error(red('Action required: --create-todo <title> or --queue-todo <todoId>'));
749
825
  process.exit(1);
750
826
  }
751
827
 
752
828
  try {
753
- const job = addJob({ name: values.name, schedule, action });
754
- console.log(green(`Created cron job: ${job.name} (ID: ${job.id.slice(0, 8)})`));
755
- console.log(dim(`Next run: ${job.nextRunAt}`));
829
+ const response = await api.apiRequest('manage-schedules', {
830
+ method: 'POST',
831
+ body: JSON.stringify({
832
+ name: values.name,
833
+ scheduleType,
834
+ scheduleValue,
835
+ actionType,
836
+ actionTitle,
837
+ actionContent,
838
+ gitRemote,
839
+ todoId,
840
+ nextRunAt,
841
+ }),
842
+ });
843
+
844
+ if (!response.ok) {
845
+ const data = await response.json();
846
+ console.error(red(`Failed to create schedule: ${data.error || response.status}`));
847
+ process.exit(1);
848
+ }
849
+
850
+ const data = await response.json();
851
+ const s = data.schedule;
852
+ console.log(green(`Created schedule: ${s.name} (ID: ${s.id.slice(0, 8)})`));
853
+ console.log(dim(`Next run: ${s.next_run_at}`));
756
854
  } catch (error) {
757
- console.error(red(`Failed to create cron job: ${error.message}`));
855
+ console.error(red(`Failed to create schedule: ${error.message}`));
758
856
  process.exit(1);
759
857
  }
760
858
  return;
761
859
  }
762
860
 
763
861
  if (subCommand === 'list') {
764
- const jobs = listJobs();
765
- if (jobs.length === 0) {
766
- console.log('No cron jobs configured.');
767
- console.log(dim('Add one with: push-todo cron add --name "..." --every "24h" --notify "..."'));
768
- return;
769
- }
770
- console.log(bold('Cron Jobs:'));
771
- for (const job of jobs) {
772
- const status = job.enabled ? green('ON') : dim('OFF');
773
- const schedStr = job.schedule.type === 'every' ? `every ${job.schedule.value}` :
774
- job.schedule.type === 'at' ? `at ${job.schedule.value}` :
775
- `cron: ${job.schedule.value}`;
776
- console.log(` ${status} ${job.name} [${schedStr}] ${job.action.type}: ${job.action.content || job.action.todoId || ''}`);
777
- console.log(dim(` ID: ${job.id.slice(0, 8)} | Next: ${job.nextRunAt || 'N/A'} | Last: ${job.lastRunAt || 'never'}`));
862
+ try {
863
+ const response = await api.apiRequest('manage-schedules', { method: 'GET' });
864
+ if (!response.ok) {
865
+ console.error(red(`Failed to list schedules: HTTP ${response.status}`));
866
+ process.exit(1);
867
+ }
868
+
869
+ const data = await response.json();
870
+ const schedules = data.schedules || [];
871
+
872
+ if (schedules.length === 0) {
873
+ console.log('No schedules configured.');
874
+ console.log(dim('Add one with: push-todo schedule add --name "..." --every "4h" --create-todo "..."'));
875
+ return;
876
+ }
877
+
878
+ console.log(bold('Schedules:'));
879
+ for (const s of schedules) {
880
+ const status = s.enabled ? green('ON') : dim('OFF');
881
+ const schedStr = s.schedule_type === 'every' ? `every ${s.schedule_value}` :
882
+ s.schedule_type === 'at' ? `at ${s.schedule_value}` :
883
+ `cron: ${s.schedule_value}`;
884
+ const actionStr = s.action_type === 'create-todo'
885
+ ? `create-todo: ${s.action_title || ''}`
886
+ : `queue-todo: ${s.todo_id || '?'}`;
887
+ console.log(` ${status} ${s.name} [${schedStr}] → ${actionStr}`);
888
+ console.log(dim(` ID: ${s.id.slice(0, 8)} | Next: ${s.next_run_at || 'N/A'} | Last: ${s.last_run_at || 'never'}`));
889
+ }
890
+ } catch (error) {
891
+ console.error(red(`Failed to list schedules: ${error.message}`));
892
+ process.exit(1);
778
893
  }
779
894
  return;
780
895
  }
781
896
 
782
897
  if (subCommand === 'remove') {
783
- const jobId = positionals[2];
784
- if (!jobId) {
785
- console.error(red('Usage: push-todo cron remove <id>'));
898
+ const scheduleId = positionals[2];
899
+ if (!scheduleId) {
900
+ console.error(red('Usage: push-todo schedule remove <id>'));
786
901
  process.exit(1);
787
902
  }
788
- if (removeJob(jobId)) {
789
- console.log(green(`Removed cron job ${jobId}`));
790
- } else {
791
- console.error(red(`Cron job not found: ${jobId}`));
903
+
904
+ try {
905
+ const response = await api.apiRequest(`manage-schedules?id=${scheduleId}`, {
906
+ method: 'DELETE',
907
+ });
908
+ if (response.ok) {
909
+ console.log(green(`Removed schedule ${scheduleId}`));
910
+ } else {
911
+ const data = await response.json();
912
+ console.error(red(`Failed to remove schedule: ${data.error || response.status}`));
913
+ process.exit(1);
914
+ }
915
+ } catch (error) {
916
+ console.error(red(`Failed to remove schedule: ${error.message}`));
917
+ process.exit(1);
918
+ }
919
+ return;
920
+ }
921
+
922
+ if (subCommand === 'enable' || subCommand === 'disable') {
923
+ const scheduleId = positionals[2];
924
+ if (!scheduleId) {
925
+ console.error(red(`Usage: push-todo schedule ${subCommand} <id>`));
926
+ process.exit(1);
927
+ }
928
+
929
+ const enabled = subCommand === 'enable';
930
+ try {
931
+ const response = await api.apiRequest('manage-schedules', {
932
+ method: 'PATCH',
933
+ body: JSON.stringify({ id: scheduleId, enabled }),
934
+ });
935
+ if (response.ok) {
936
+ console.log(green(`Schedule ${scheduleId} ${enabled ? 'enabled' : 'disabled'}`));
937
+ } else {
938
+ const data = await response.json();
939
+ console.error(red(`Failed to ${subCommand} schedule: ${data.error || response.status}`));
940
+ process.exit(1);
941
+ }
942
+ } catch (error) {
943
+ console.error(red(`Failed to ${subCommand} schedule: ${error.message}`));
792
944
  process.exit(1);
793
945
  }
794
946
  return;
795
947
  }
796
948
 
797
- // Default: show help for cron
798
- console.log(`${bold('Cron Commands:')}
799
- push-todo cron add Add a new scheduled job
800
- push-todo cron list List all jobs
801
- push-todo cron remove Remove a job by ID
949
+ // Default: show help for schedule
950
+ console.log(`${bold('Schedule Commands:')}
951
+ push-todo schedule add Create a new remote schedule
952
+ push-todo schedule list List all schedules
953
+ push-todo schedule remove Remove a schedule by ID
954
+ push-todo schedule enable Enable a schedule
955
+ push-todo schedule disable Disable a schedule
956
+
957
+ ${bold('Examples:')}
958
+ push-todo schedule add --name "Daily standup" --every 24h --create-todo "Write standup update"
959
+ push-todo schedule add --name "Weekly review" --cron "0 9 * * 1" --create-todo "Weekly code review" --git-remote github.com/user/repo
960
+ push-todo schedule add --name "Re-run task" --every 4h --queue-todo <todoId>
802
961
  `);
803
962
  return;
804
963
  }
@@ -110,10 +110,16 @@ export function scanProjectSkills(projectPath) {
110
110
  const content = readFileSync(skillFile, 'utf8');
111
111
  const { frontmatter, body } = parseFrontmatter(content);
112
112
 
113
+ // Parse tools field: comma-separated list of tool names/patterns
114
+ const tools = frontmatter.tools
115
+ ? frontmatter.tools.split(',').map(t => t.trim()).filter(Boolean)
116
+ : [];
117
+
113
118
  skills.push({
114
119
  name: frontmatter.name || entry,
115
120
  description: frontmatter.description || '',
116
121
  requiresConfirmation: body.includes('push-todo confirm'),
122
+ tools,
117
123
  });
118
124
  } catch {
119
125
  // Skip unreadable skill files