@masslessai/push-todo 3.0.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/api.js ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Supabase API client for Push CLI.
3
+ *
4
+ * Handles all HTTP requests to the Push backend.
5
+ */
6
+
7
+ import { getApiKey } from './config.js';
8
+
9
+ const API_BASE = 'https://jxuzqcbqhiaxmfitzxlo.supabase.co/functions/v1';
10
+
11
+ /**
12
+ * Make an authenticated API request.
13
+ *
14
+ * @param {string} endpoint - API endpoint (without base URL)
15
+ * @param {Object} options - Fetch options
16
+ * @returns {Promise<Response>}
17
+ */
18
+ async function apiRequest(endpoint, options = {}) {
19
+ const apiKey = getApiKey();
20
+ if (!apiKey) {
21
+ throw new Error('No API key configured. Run "push-todo connect" first.');
22
+ }
23
+
24
+ const url = endpoint.startsWith('http') ? endpoint : `${API_BASE}/${endpoint}`;
25
+
26
+ const response = await fetch(url, {
27
+ ...options,
28
+ headers: {
29
+ 'Authorization': `Bearer ${apiKey}`,
30
+ 'Content-Type': 'application/json',
31
+ ...options.headers
32
+ }
33
+ });
34
+
35
+ return response;
36
+ }
37
+
38
+ /**
39
+ * Fetch tasks from the API.
40
+ *
41
+ * @param {string|null} gitRemote - Git remote to filter by (null for all projects)
42
+ * @param {Object} options - Query options
43
+ * @param {boolean} options.backlogOnly - Only return backlog items
44
+ * @param {boolean} options.includeBacklog - Include backlog items
45
+ * @param {boolean} options.completedOnly - Only return completed items
46
+ * @param {boolean} options.includeCompleted - Include completed items
47
+ * @returns {Promise<Object[]>} Array of todo objects
48
+ */
49
+ export async function fetchTasks(gitRemote, options = {}) {
50
+ const params = new URLSearchParams();
51
+
52
+ if (gitRemote) {
53
+ params.set('git_remote', gitRemote);
54
+ }
55
+ if (options.backlogOnly) {
56
+ params.set('later_only', 'true');
57
+ }
58
+ if (options.includeBacklog) {
59
+ params.set('include_later', 'true');
60
+ }
61
+ if (options.completedOnly) {
62
+ params.set('completed_only', 'true');
63
+ }
64
+ if (options.includeCompleted) {
65
+ params.set('include_completed', 'true');
66
+ }
67
+
68
+ const queryString = params.toString();
69
+ const endpoint = queryString ? `synced-todos?${queryString}` : 'synced-todos';
70
+
71
+ const response = await apiRequest(endpoint);
72
+
73
+ if (!response.ok) {
74
+ if (response.status === 401) {
75
+ throw new Error('Invalid API key. Run "push-todo connect" to re-authenticate.');
76
+ }
77
+ if (response.status === 404) {
78
+ return [];
79
+ }
80
+ const text = await response.text();
81
+ throw new Error(`API error (${response.status}): ${text}`);
82
+ }
83
+
84
+ const data = await response.json();
85
+ return data.todos || [];
86
+ }
87
+
88
+ /**
89
+ * Fetch a specific task by display number.
90
+ *
91
+ * @param {number} displayNumber - The task's display number
92
+ * @returns {Promise<Object|null>} Task object or null if not found
93
+ */
94
+ export async function fetchTaskByNumber(displayNumber) {
95
+ const response = await apiRequest(`synced-todos?display_number=${displayNumber}`);
96
+
97
+ if (!response.ok) {
98
+ if (response.status === 401) {
99
+ throw new Error('Invalid API key. Run "push-todo connect" to re-authenticate.');
100
+ }
101
+ if (response.status === 404) {
102
+ return null;
103
+ }
104
+ const text = await response.text();
105
+ throw new Error(`API error (${response.status}): ${text}`);
106
+ }
107
+
108
+ const data = await response.json();
109
+ const todos = data.todos || [];
110
+ return todos.length > 0 ? todos[0] : null;
111
+ }
112
+
113
+ /**
114
+ * Mark a task as completed.
115
+ *
116
+ * @param {string} taskId - UUID of the task
117
+ * @param {string} comment - Completion comment
118
+ * @returns {Promise<boolean>} True if successful
119
+ */
120
+ export async function markTaskCompleted(taskId, comment = '') {
121
+ const response = await apiRequest('mark-todo-completed', {
122
+ method: 'POST',
123
+ body: JSON.stringify({
124
+ todo_id: taskId,
125
+ completion_comment: comment
126
+ })
127
+ });
128
+
129
+ if (!response.ok) {
130
+ const text = await response.text();
131
+ throw new Error(`Failed to mark task completed: ${text}`);
132
+ }
133
+
134
+ return true;
135
+ }
136
+
137
+ /**
138
+ * Queue a task for daemon execution.
139
+ *
140
+ * @param {number} displayNumber - The task's display number
141
+ * @returns {Promise<boolean>} True if successful
142
+ */
143
+ export async function queueTask(displayNumber) {
144
+ const response = await apiRequest('queue-task', {
145
+ method: 'POST',
146
+ body: JSON.stringify({
147
+ display_number: displayNumber
148
+ })
149
+ });
150
+
151
+ if (!response.ok) {
152
+ const text = await response.text();
153
+ throw new Error(`Failed to queue task: ${text}`);
154
+ }
155
+
156
+ return true;
157
+ }
158
+
159
+ /**
160
+ * Queue multiple tasks for daemon execution.
161
+ *
162
+ * @param {number[]} displayNumbers - Array of display numbers
163
+ * @returns {Promise<Object>} Result with success/failure counts
164
+ */
165
+ export async function queueTasks(displayNumbers) {
166
+ const results = {
167
+ success: [],
168
+ failed: []
169
+ };
170
+
171
+ for (const num of displayNumbers) {
172
+ try {
173
+ await queueTask(num);
174
+ results.success.push(num);
175
+ } catch (error) {
176
+ results.failed.push({ num, error: error.message });
177
+ }
178
+ }
179
+
180
+ return results;
181
+ }
182
+
183
+ /**
184
+ * Search tasks by query.
185
+ *
186
+ * @param {string} query - Search query
187
+ * @param {string|null} gitRemote - Git remote to filter by
188
+ * @returns {Promise<Object[]>} Array of matching tasks
189
+ */
190
+ export async function searchTasks(query, gitRemote = null) {
191
+ const params = new URLSearchParams();
192
+ params.set('query', query);
193
+ if (gitRemote) {
194
+ params.set('git_remote', gitRemote);
195
+ }
196
+
197
+ const response = await apiRequest(`search-todos?${params}`);
198
+
199
+ if (!response.ok) {
200
+ if (response.status === 401) {
201
+ throw new Error('Invalid API key. Run "push-todo connect" to re-authenticate.');
202
+ }
203
+ const text = await response.text();
204
+ throw new Error(`Search failed: ${text}`);
205
+ }
206
+
207
+ const data = await response.json();
208
+ return data.results || [];
209
+ }
210
+
211
+ /**
212
+ * Update task execution status.
213
+ *
214
+ * @param {Object} payload - Execution update payload
215
+ * @returns {Promise<boolean>} True if successful
216
+ */
217
+ export async function updateTaskExecution(payload) {
218
+ const response = await apiRequest('update-task-execution', {
219
+ method: 'POST',
220
+ body: JSON.stringify(payload)
221
+ });
222
+
223
+ if (!response.ok) {
224
+ const text = await response.text();
225
+ throw new Error(`Failed to update execution: ${text}`);
226
+ }
227
+
228
+ return true;
229
+ }
230
+
231
+ /**
232
+ * Validate API key.
233
+ *
234
+ * @returns {Promise<Object>} Validation result with user info
235
+ */
236
+ export async function validateApiKey() {
237
+ try {
238
+ const response = await apiRequest('validate-api-key');
239
+
240
+ if (!response.ok) {
241
+ if (response.status === 401) {
242
+ return { valid: false, reason: 'invalid_key' };
243
+ }
244
+ return { valid: false, reason: 'api_error' };
245
+ }
246
+
247
+ const data = await response.json();
248
+ return {
249
+ valid: true,
250
+ userId: data.user_id,
251
+ email: data.email
252
+ };
253
+ } catch (error) {
254
+ return { valid: false, reason: 'network_error', error: error.message };
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Register a project with the backend.
260
+ *
261
+ * @param {string} gitRemote - Normalized git remote
262
+ * @param {string[]} keywords - Project keywords
263
+ * @param {string} description - Project description
264
+ * @returns {Promise<boolean>} True if successful
265
+ */
266
+ export async function registerProject(gitRemote, keywords = [], description = '') {
267
+ const response = await apiRequest('register-project', {
268
+ method: 'POST',
269
+ body: JSON.stringify({
270
+ git_remote: gitRemote,
271
+ keywords,
272
+ description
273
+ })
274
+ });
275
+
276
+ if (!response.ok) {
277
+ const text = await response.text();
278
+ throw new Error(`Failed to register project: ${text}`);
279
+ }
280
+
281
+ return true;
282
+ }
283
+
284
+ /**
285
+ * Validate machine registration.
286
+ *
287
+ * @param {string} machineId - Machine identifier
288
+ * @returns {Promise<Object>} Validation result
289
+ */
290
+ export async function validateMachine(machineId) {
291
+ const response = await apiRequest('validate-machine', {
292
+ method: 'POST',
293
+ body: JSON.stringify({
294
+ machine_id: machineId
295
+ })
296
+ });
297
+
298
+ if (!response.ok) {
299
+ const text = await response.text();
300
+ throw new Error(`Machine validation failed: ${text}`);
301
+ }
302
+
303
+ const data = await response.json();
304
+ return data;
305
+ }
306
+
307
+ /**
308
+ * Get the current CLI version from the server.
309
+ *
310
+ * @returns {Promise<string>} Latest version string
311
+ */
312
+ export async function getLatestVersion() {
313
+ try {
314
+ const response = await fetch('https://raw.githubusercontent.com/MasslessAI/push-todo-cli/main/npm/push-todo/package.json');
315
+ if (!response.ok) {
316
+ return null;
317
+ }
318
+ const data = await response.json();
319
+ return data.version;
320
+ } catch {
321
+ return null;
322
+ }
323
+ }
324
+
325
+ export { API_BASE };
package/lib/cli.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * CLI argument parsing and command routing for Push CLI.
3
+ *
4
+ * Handles all command-line options and dispatches to appropriate handlers.
5
+ */
6
+
7
+ import { parseArgs } from 'util';
8
+ import * as fetch from './fetch.js';
9
+ import { runConnect } from './connect.js';
10
+ import { startWatch } from './watch.js';
11
+ import { showSettings, toggleSetting } from './config.js';
12
+ import { bold, red, cyan, dim } from './utils/colors.js';
13
+
14
+ const VERSION = '3.0.0';
15
+
16
+ const HELP_TEXT = `
17
+ ${bold('push-todo')} - Voice tasks from Push iOS app for Claude Code
18
+
19
+ ${bold('USAGE:')}
20
+ push-todo [options] List active tasks
21
+ push-todo <number> Show specific task
22
+ push-todo connect Run connection doctor
23
+ push-todo search <query> Search tasks
24
+ push-todo review Review completed tasks
25
+
26
+ ${bold('OPTIONS:')}
27
+ --all-projects, -a List tasks from all projects
28
+ --backlog, -b Only show backlog items
29
+ --include-backlog Include backlog items in listing
30
+ --completed, -c Only show completed items
31
+ --include-completed Include completed items in listing
32
+ --queue <numbers> Queue tasks for daemon (comma-separated)
33
+ --queue-batch Auto-queue a batch of tasks
34
+ --mark-completed <uuid> Mark a task as completed
35
+ --completion-comment <text> Comment for completion
36
+ --search <query> Search tasks
37
+ --status Show connection and daemon status
38
+ --watch, -w Live terminal UI
39
+ --setting [name] Show or toggle settings
40
+ --json Output as JSON
41
+ --version, -v Show version
42
+ --help, -h Show this help
43
+
44
+ ${bold('EXAMPLES:')}
45
+ push-todo List active tasks for current project
46
+ push-todo 427 Show task #427
47
+ push-todo -a List all tasks across projects
48
+ push-todo --queue 1,2,3 Queue tasks 1, 2, 3 for daemon
49
+ push-todo search "auth bug" Search for tasks matching "auth bug"
50
+ push-todo connect Run connection diagnostics
51
+
52
+ ${bold('SETTINGS:')}
53
+ push-todo setting Show all settings
54
+ push-todo setting auto-commit Toggle auto-commit
55
+
56
+ ${dim('Documentation:')} https://pushto.do/docs/cli
57
+ `;
58
+
59
+ const options = {
60
+ 'all-projects': { type: 'boolean', short: 'a' },
61
+ 'backlog': { type: 'boolean', short: 'b' },
62
+ 'include-backlog': { type: 'boolean' },
63
+ 'completed': { type: 'boolean', short: 'c' },
64
+ 'include-completed': { type: 'boolean' },
65
+ 'queue': { type: 'string' },
66
+ 'queue-batch': { type: 'boolean' },
67
+ 'mark-completed': { type: 'string' },
68
+ 'completion-comment': { type: 'string' },
69
+ 'search': { type: 'string' },
70
+ 'status': { type: 'boolean' },
71
+ 'watch': { type: 'boolean', short: 'w' },
72
+ 'setting': { type: 'string' },
73
+ 'json': { type: 'boolean' },
74
+ 'version': { type: 'boolean', short: 'v' },
75
+ 'help': { type: 'boolean', short: 'h' }
76
+ };
77
+
78
+ /**
79
+ * Parse command line arguments.
80
+ *
81
+ * @param {string[]} argv - Command line arguments
82
+ * @returns {Object} Parsed arguments with values and positionals
83
+ */
84
+ function parseArguments(argv) {
85
+ try {
86
+ return parseArgs({
87
+ args: argv,
88
+ options,
89
+ allowPositionals: true
90
+ });
91
+ } catch (error) {
92
+ console.error(red(`Error: ${error.message}`));
93
+ console.log(`Run ${cyan('push-todo --help')} for usage.`);
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Main CLI entry point.
100
+ *
101
+ * @param {string[]} argv - Command line arguments (without node and script path)
102
+ */
103
+ export async function run(argv) {
104
+ const { values, positionals } = parseArguments(argv);
105
+
106
+ // Help
107
+ if (values.help) {
108
+ console.log(HELP_TEXT);
109
+ return;
110
+ }
111
+
112
+ // Version
113
+ if (values.version) {
114
+ console.log(`push-todo ${VERSION}`);
115
+ return;
116
+ }
117
+
118
+ // Get the command (first positional)
119
+ const command = positionals[0];
120
+
121
+ // Connect command
122
+ if (command === 'connect') {
123
+ return runConnect(values);
124
+ }
125
+
126
+ // Review command
127
+ if (command === 'review') {
128
+ return fetch.runReview(values);
129
+ }
130
+
131
+ // Search command (positional form)
132
+ if (command === 'search' && positionals[1]) {
133
+ return fetch.searchTasks(positionals.slice(1).join(' '), values);
134
+ }
135
+
136
+ // Search option
137
+ if (values.search) {
138
+ return fetch.searchTasks(values.search, values);
139
+ }
140
+
141
+ // Watch mode
142
+ if (values.watch) {
143
+ return startWatch(values);
144
+ }
145
+
146
+ // Status
147
+ if (values.status) {
148
+ return fetch.showStatus(values);
149
+ }
150
+
151
+ // Settings
152
+ if ('setting' in values) {
153
+ const settingName = values.setting;
154
+ if (settingName && settingName !== 'true') {
155
+ return toggleSetting(settingName);
156
+ }
157
+ return showSettings();
158
+ }
159
+
160
+ // Queue tasks
161
+ if (values.queue) {
162
+ return fetch.queueForExecution(values.queue);
163
+ }
164
+
165
+ // Queue batch
166
+ if (values['queue-batch']) {
167
+ return fetch.offerBatch(values);
168
+ }
169
+
170
+ // Mark completed
171
+ if (values['mark-completed']) {
172
+ const comment = values['completion-comment'] || '';
173
+ return fetch.markComplete(values['mark-completed'], comment);
174
+ }
175
+
176
+ // Specific task by number
177
+ if (command && /^\d+$/.test(command)) {
178
+ const displayNumber = parseInt(command, 10);
179
+ return fetch.showTask(displayNumber, values);
180
+ }
181
+
182
+ // Unknown command
183
+ if (command && !/^\d+$/.test(command)) {
184
+ console.error(red(`Unknown command: ${command}`));
185
+ console.log(`Run ${cyan('push-todo --help')} for usage.`);
186
+ process.exit(1);
187
+ }
188
+
189
+ // Default: list tasks
190
+ return fetch.listTasks(values);
191
+ }