@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/.claude-plugin/plugin.json +5 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/SKILL.md +180 -0
- package/bin/push-todo.js +23 -0
- package/commands/push-todo.md +78 -0
- package/hooks/hooks.json +26 -0
- package/hooks/session-end.js +99 -0
- package/hooks/session-start.js +134 -0
- package/lib/api.js +325 -0
- package/lib/cli.js +191 -0
- package/lib/config.js +279 -0
- package/lib/connect.js +380 -0
- package/lib/encryption.js +201 -0
- package/lib/fetch.js +371 -0
- package/lib/index.js +114 -0
- package/lib/machine-id.js +101 -0
- package/lib/project-registry.js +279 -0
- package/lib/utils/colors.js +126 -0
- package/lib/utils/format.js +253 -0
- package/lib/utils/git.js +149 -0
- package/lib/watch.js +343 -0
- package/natives/KeychainHelper.swift +134 -0
- package/package.json +54 -0
- package/scripts/postinstall.js +136 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2EE Encryption support for Push CLI.
|
|
3
|
+
*
|
|
4
|
+
* Decrypts end-to-end encrypted todo fields using
|
|
5
|
+
* the Swift keychain helper binary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from 'child_process';
|
|
9
|
+
import { createDecipheriv } from 'crypto';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
const HELPER_PATH = join(__dirname, '../bin/push-keychain-helper');
|
|
18
|
+
|
|
19
|
+
// Cached encryption key
|
|
20
|
+
let cachedKey = null;
|
|
21
|
+
let keyCheckDone = false;
|
|
22
|
+
let keyAvailable = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if the keychain helper binary exists.
|
|
26
|
+
*
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function helperExists() {
|
|
30
|
+
return existsSync(HELPER_PATH);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the encryption key from the macOS Keychain.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Buffer|null} The 32-byte encryption key or null
|
|
37
|
+
*/
|
|
38
|
+
export function getEncryptionKey() {
|
|
39
|
+
if (cachedKey !== null) {
|
|
40
|
+
return cachedKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!helperExists()) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = execFileSync(HELPER_PATH, [], {
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
timeout: 5000,
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const keyBase64 = result.trim();
|
|
55
|
+
if (!keyBase64) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cachedKey = Buffer.from(keyBase64, 'base64');
|
|
60
|
+
return cachedKey;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Exit codes:
|
|
63
|
+
// 1 = Key not found in Keychain
|
|
64
|
+
// 2 = iCloud Keychain not available
|
|
65
|
+
// Other = Unexpected error
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Decrypt an AES-256-GCM encrypted value.
|
|
72
|
+
*
|
|
73
|
+
* Format: version (1 byte) + nonce (12 bytes) + ciphertext + tag (16 bytes)
|
|
74
|
+
*
|
|
75
|
+
* @param {Buffer} ciphertext - The encrypted data
|
|
76
|
+
* @returns {string} Decrypted plaintext
|
|
77
|
+
*/
|
|
78
|
+
export function decrypt(ciphertext) {
|
|
79
|
+
const key = getEncryptionKey();
|
|
80
|
+
if (!key) {
|
|
81
|
+
throw new Error('Encryption key not available');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check version byte
|
|
85
|
+
const version = ciphertext[0];
|
|
86
|
+
if (version !== 0) {
|
|
87
|
+
throw new Error(`Unsupported encryption version: ${version}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Extract components
|
|
91
|
+
const nonce = ciphertext.slice(1, 13); // 12 bytes
|
|
92
|
+
const encrypted = ciphertext.slice(13, -16); // Ciphertext without tag
|
|
93
|
+
const authTag = ciphertext.slice(-16); // Last 16 bytes
|
|
94
|
+
|
|
95
|
+
// Decrypt
|
|
96
|
+
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
|
97
|
+
decipher.setAuthTag(authTag);
|
|
98
|
+
|
|
99
|
+
const decrypted = Buffer.concat([
|
|
100
|
+
decipher.update(encrypted),
|
|
101
|
+
decipher.final()
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
return decrypted.toString('utf8');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Decrypt a potentially encrypted todo field.
|
|
109
|
+
*
|
|
110
|
+
* Returns the original value if:
|
|
111
|
+
* - The value is null/undefined
|
|
112
|
+
* - The value is too short to be encrypted
|
|
113
|
+
* - Decryption fails (returns original)
|
|
114
|
+
*
|
|
115
|
+
* @param {string} value - The field value (possibly base64 encoded ciphertext)
|
|
116
|
+
* @returns {string} Decrypted value or original
|
|
117
|
+
*/
|
|
118
|
+
export function decryptTodoField(value) {
|
|
119
|
+
if (!value || typeof value !== 'string') {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Encrypted values are base64 and at least 40+ chars
|
|
124
|
+
// (version + nonce + min ciphertext + tag)
|
|
125
|
+
if (value.length < 40) {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Quick check: if it doesn't look like base64, skip
|
|
130
|
+
if (!/^[A-Za-z0-9+/]+=*$/.test(value)) {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const decoded = Buffer.from(value, 'base64');
|
|
136
|
+
|
|
137
|
+
// Check if it starts with version byte 0
|
|
138
|
+
if (decoded.length < 30 || decoded[0] !== 0) {
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return decrypt(decoded);
|
|
143
|
+
} catch {
|
|
144
|
+
// Decryption failed - return original value
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if E2EE is available on this machine.
|
|
151
|
+
*
|
|
152
|
+
* @returns {[boolean, string]} Tuple of [available, message]
|
|
153
|
+
*/
|
|
154
|
+
export function isE2EEAvailable() {
|
|
155
|
+
if (keyCheckDone) {
|
|
156
|
+
return [keyAvailable, keyAvailable ? 'E2EE available' : 'E2EE not available'];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check platform
|
|
160
|
+
if (process.platform !== 'darwin') {
|
|
161
|
+
keyCheckDone = true;
|
|
162
|
+
keyAvailable = false;
|
|
163
|
+
return [false, 'E2EE requires macOS'];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check helper binary
|
|
167
|
+
if (!helperExists()) {
|
|
168
|
+
keyCheckDone = true;
|
|
169
|
+
keyAvailable = false;
|
|
170
|
+
return [false, 'Keychain helper not installed'];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Try to get the key
|
|
174
|
+
try {
|
|
175
|
+
const key = getEncryptionKey();
|
|
176
|
+
keyCheckDone = true;
|
|
177
|
+
keyAvailable = key !== null;
|
|
178
|
+
|
|
179
|
+
if (keyAvailable) {
|
|
180
|
+
return [true, 'E2EE available'];
|
|
181
|
+
} else {
|
|
182
|
+
return [false, 'Encryption key not in Keychain'];
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
keyCheckDone = true;
|
|
186
|
+
keyAvailable = false;
|
|
187
|
+
return [false, `E2EE check failed: ${error.message}`];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Clear the cached encryption key.
|
|
193
|
+
* Useful for testing or when the key might have changed.
|
|
194
|
+
*/
|
|
195
|
+
export function clearKeyCache() {
|
|
196
|
+
cachedKey = null;
|
|
197
|
+
keyCheckDone = false;
|
|
198
|
+
keyAvailable = false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { HELPER_PATH };
|
package/lib/fetch.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task fetching and display for Push CLI.
|
|
3
|
+
*
|
|
4
|
+
* Main module for listing, viewing, and managing tasks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as api from './api.js';
|
|
8
|
+
import { getMachineId, getMachineName } from './machine-id.js';
|
|
9
|
+
import { getRegistry } from './project-registry.js';
|
|
10
|
+
import { getGitRemote, isGitRepo } from './utils/git.js';
|
|
11
|
+
import { formatTaskForDisplay, formatTaskTable, formatSearchResult, formatBatchOffer } from './utils/format.js';
|
|
12
|
+
import { bold, green, yellow, red, cyan, dim, muted } from './utils/colors.js';
|
|
13
|
+
import { decryptTodoField, isE2EEAvailable } from './encryption.js';
|
|
14
|
+
import { getAutoCommitEnabled, getMaxBatchSize } from './config.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decrypt encrypted fields in a task object.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} task - Task object from API
|
|
20
|
+
* @returns {Object} Task with decrypted fields
|
|
21
|
+
*/
|
|
22
|
+
function decryptTaskFields(task) {
|
|
23
|
+
const decrypted = { ...task };
|
|
24
|
+
|
|
25
|
+
// Fields that may be encrypted
|
|
26
|
+
const encryptedFields = [
|
|
27
|
+
'summary',
|
|
28
|
+
'content',
|
|
29
|
+
'normalizedContent',
|
|
30
|
+
'normalized_content',
|
|
31
|
+
'originalTranscript',
|
|
32
|
+
'original_transcript',
|
|
33
|
+
'transcript'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const field of encryptedFields) {
|
|
37
|
+
if (decrypted[field]) {
|
|
38
|
+
decrypted[field] = decryptTodoField(decrypted[field]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return decrypted;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List tasks for the current project or all projects.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} options - List options
|
|
49
|
+
* @param {boolean} options.allProjects - List tasks from all projects
|
|
50
|
+
* @param {boolean} options.backlog - Only show backlog items
|
|
51
|
+
* @param {boolean} options.includeBacklog - Include backlog items
|
|
52
|
+
* @param {boolean} options.completed - Only show completed items
|
|
53
|
+
* @param {boolean} options.includeCompleted - Include completed items
|
|
54
|
+
* @param {boolean} options.json - Output as JSON
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
export async function listTasks(options = {}) {
|
|
58
|
+
// Determine git remote
|
|
59
|
+
let gitRemote = null;
|
|
60
|
+
if (!options.allProjects) {
|
|
61
|
+
gitRemote = getGitRemote();
|
|
62
|
+
if (!gitRemote && isGitRepo()) {
|
|
63
|
+
console.error(yellow('Warning: In a git repo but no remote configured.'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fetch tasks
|
|
68
|
+
const tasks = await api.fetchTasks(gitRemote, {
|
|
69
|
+
backlogOnly: options.backlog,
|
|
70
|
+
includeBacklog: options.includeBacklog,
|
|
71
|
+
completedOnly: options.completed,
|
|
72
|
+
includeCompleted: options.includeCompleted
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Decrypt if E2EE is available
|
|
76
|
+
const decryptedTasks = tasks.map(decryptTaskFields);
|
|
77
|
+
|
|
78
|
+
// Output
|
|
79
|
+
if (options.json) {
|
|
80
|
+
console.log(JSON.stringify(decryptedTasks, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (decryptedTasks.length === 0) {
|
|
85
|
+
const scope = gitRemote ? `for ${cyan(gitRemote)}` : 'across all projects';
|
|
86
|
+
console.log(`No active tasks ${scope}.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Group by status for display
|
|
91
|
+
const active = decryptedTasks.filter(t => !t.isCompleted && !t.is_completed && !t.isBacklog && !t.is_backlog);
|
|
92
|
+
const backlog = decryptedTasks.filter(t => !t.isCompleted && !t.is_completed && (t.isBacklog || t.is_backlog));
|
|
93
|
+
const completed = decryptedTasks.filter(t => t.isCompleted || t.is_completed);
|
|
94
|
+
|
|
95
|
+
// Header
|
|
96
|
+
const scope = gitRemote ? gitRemote : 'All Projects';
|
|
97
|
+
console.log(bold(`\nPush Tasks - ${scope}\n`));
|
|
98
|
+
|
|
99
|
+
// Active tasks
|
|
100
|
+
if (active.length > 0) {
|
|
101
|
+
console.log(green(`Active (${active.length}):`));
|
|
102
|
+
console.log(formatTaskTable(active));
|
|
103
|
+
console.log('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Backlog tasks (if requested)
|
|
107
|
+
if (backlog.length > 0 && (options.backlog || options.includeBacklog)) {
|
|
108
|
+
console.log(yellow(`Backlog (${backlog.length}):`));
|
|
109
|
+
console.log(formatTaskTable(backlog));
|
|
110
|
+
console.log('');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Completed tasks (if requested)
|
|
114
|
+
if (completed.length > 0 && (options.completed || options.includeCompleted)) {
|
|
115
|
+
console.log(dim(`Completed (${completed.length}):`));
|
|
116
|
+
console.log(formatTaskTable(completed));
|
|
117
|
+
console.log('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Summary
|
|
121
|
+
const total = active.length + (options.includeBacklog ? backlog.length : 0);
|
|
122
|
+
console.log(muted(`Showing ${total} task(s). Use --include-backlog or --completed for more.`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Show a specific task by display number.
|
|
127
|
+
*
|
|
128
|
+
* @param {number} displayNumber - The task's display number
|
|
129
|
+
* @param {Object} options - Display options
|
|
130
|
+
* @param {boolean} options.json - Output as JSON
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
export async function showTask(displayNumber, options = {}) {
|
|
134
|
+
const task = await api.fetchTaskByNumber(displayNumber);
|
|
135
|
+
|
|
136
|
+
if (!task) {
|
|
137
|
+
console.error(red(`Task #${displayNumber} not found.`));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const decrypted = decryptTaskFields(task);
|
|
142
|
+
|
|
143
|
+
if (options.json) {
|
|
144
|
+
console.log(JSON.stringify(decrypted, null, 2));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(formatTaskForDisplay(decrypted));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Mark a task as completed.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} taskId - UUID of the task
|
|
155
|
+
* @param {string} comment - Completion comment
|
|
156
|
+
* @returns {Promise<void>}
|
|
157
|
+
*/
|
|
158
|
+
export async function markComplete(taskId, comment = '') {
|
|
159
|
+
await api.markTaskCompleted(taskId, comment);
|
|
160
|
+
console.log(green(`Task marked as completed.`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Queue tasks for daemon execution.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} numbersStr - Comma-separated display numbers
|
|
167
|
+
* @returns {Promise<void>}
|
|
168
|
+
*/
|
|
169
|
+
export async function queueForExecution(numbersStr) {
|
|
170
|
+
const numbers = numbersStr.split(',').map(n => parseInt(n.trim(), 10)).filter(n => !isNaN(n));
|
|
171
|
+
|
|
172
|
+
if (numbers.length === 0) {
|
|
173
|
+
console.error(red('No valid task numbers provided.'));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const results = await api.queueTasks(numbers);
|
|
178
|
+
|
|
179
|
+
if (results.success.length > 0) {
|
|
180
|
+
console.log(green(`Queued: ${results.success.join(', ')}`));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (results.failed.length > 0) {
|
|
184
|
+
for (const { num, error } of results.failed) {
|
|
185
|
+
console.error(red(`Failed to queue #${num}: ${error}`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Search tasks by query.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} query - Search query
|
|
194
|
+
* @param {Object} options - Search options
|
|
195
|
+
* @param {boolean} options.allProjects - Search all projects
|
|
196
|
+
* @param {boolean} options.json - Output as JSON
|
|
197
|
+
* @returns {Promise<void>}
|
|
198
|
+
*/
|
|
199
|
+
export async function searchTasks(query, options = {}) {
|
|
200
|
+
let gitRemote = null;
|
|
201
|
+
if (!options.allProjects) {
|
|
202
|
+
gitRemote = getGitRemote();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const results = await api.searchTasks(query, gitRemote);
|
|
206
|
+
const decrypted = results.map(decryptTaskFields);
|
|
207
|
+
|
|
208
|
+
if (options.json) {
|
|
209
|
+
console.log(JSON.stringify(decrypted, null, 2));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (decrypted.length === 0) {
|
|
214
|
+
console.log(`No tasks found matching "${query}".`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log(bold(`\nSearch Results for "${query}":\n`));
|
|
219
|
+
for (const result of decrypted) {
|
|
220
|
+
console.log(formatSearchResult(result));
|
|
221
|
+
}
|
|
222
|
+
console.log('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Show status information.
|
|
227
|
+
*
|
|
228
|
+
* @param {Object} options - Status options
|
|
229
|
+
* @param {boolean} options.json - Output as JSON
|
|
230
|
+
* @returns {Promise<void>}
|
|
231
|
+
*/
|
|
232
|
+
export async function showStatus(options = {}) {
|
|
233
|
+
const machineId = getMachineId();
|
|
234
|
+
const machineName = getMachineName();
|
|
235
|
+
const gitRemote = getGitRemote();
|
|
236
|
+
const registry = getRegistry();
|
|
237
|
+
const [e2eeAvailable, e2eeMessage] = isE2EEAvailable();
|
|
238
|
+
const autoCommit = getAutoCommitEnabled();
|
|
239
|
+
const maxBatch = getMaxBatchSize();
|
|
240
|
+
|
|
241
|
+
// Validate API key
|
|
242
|
+
const keyStatus = await api.validateApiKey();
|
|
243
|
+
|
|
244
|
+
const status = {
|
|
245
|
+
machine: {
|
|
246
|
+
id: machineId,
|
|
247
|
+
name: machineName
|
|
248
|
+
},
|
|
249
|
+
project: {
|
|
250
|
+
gitRemote,
|
|
251
|
+
isGitRepo: isGitRepo(),
|
|
252
|
+
isRegistered: gitRemote ? registry.isRegistered(gitRemote) : false
|
|
253
|
+
},
|
|
254
|
+
api: {
|
|
255
|
+
valid: keyStatus.valid,
|
|
256
|
+
email: keyStatus.email || null
|
|
257
|
+
},
|
|
258
|
+
e2ee: {
|
|
259
|
+
available: e2eeAvailable,
|
|
260
|
+
message: e2eeMessage
|
|
261
|
+
},
|
|
262
|
+
settings: {
|
|
263
|
+
autoCommit,
|
|
264
|
+
maxBatchSize: maxBatch
|
|
265
|
+
},
|
|
266
|
+
registeredProjects: registry.projectCount()
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (options.json) {
|
|
270
|
+
console.log(JSON.stringify(status, null, 2));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log(bold('\nPush Status\n'));
|
|
275
|
+
|
|
276
|
+
// Machine
|
|
277
|
+
console.log(`${bold('Machine:')} ${machineName}`);
|
|
278
|
+
console.log(`${bold('Machine ID:')} ${machineId}`);
|
|
279
|
+
console.log('');
|
|
280
|
+
|
|
281
|
+
// API
|
|
282
|
+
if (status.api.valid) {
|
|
283
|
+
console.log(`${bold('API:')} ${green('Connected')} (${status.api.email})`);
|
|
284
|
+
} else {
|
|
285
|
+
console.log(`${bold('API:')} ${red('Not connected')} - run "push-todo connect"`);
|
|
286
|
+
}
|
|
287
|
+
console.log('');
|
|
288
|
+
|
|
289
|
+
// Project
|
|
290
|
+
if (gitRemote) {
|
|
291
|
+
console.log(`${bold('Project:')} ${gitRemote}`);
|
|
292
|
+
console.log(`${bold('Registered:')} ${status.project.isRegistered ? green('Yes') : yellow('No')}`);
|
|
293
|
+
} else if (status.project.isGitRepo) {
|
|
294
|
+
console.log(`${bold('Project:')} ${yellow('No remote configured')}`);
|
|
295
|
+
} else {
|
|
296
|
+
console.log(`${bold('Project:')} ${dim('Not in a git repository')}`);
|
|
297
|
+
}
|
|
298
|
+
console.log('');
|
|
299
|
+
|
|
300
|
+
// E2EE
|
|
301
|
+
console.log(`${bold('E2EE:')} ${e2eeAvailable ? green('Available') : yellow(e2eeMessage)}`);
|
|
302
|
+
console.log('');
|
|
303
|
+
|
|
304
|
+
// Settings
|
|
305
|
+
console.log(`${bold('Auto-commit:')} ${autoCommit ? 'Enabled' : 'Disabled'}`);
|
|
306
|
+
console.log(`${bold('Max batch size:')} ${maxBatch}`);
|
|
307
|
+
console.log(`${bold('Registered projects:')} ${status.registeredProjects}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Offer a batch of tasks for processing.
|
|
312
|
+
*
|
|
313
|
+
* @param {Object} options - Batch options
|
|
314
|
+
* @returns {Promise<void>}
|
|
315
|
+
*/
|
|
316
|
+
export async function offerBatch(options = {}) {
|
|
317
|
+
const gitRemote = options.allProjects ? null : getGitRemote();
|
|
318
|
+
const maxBatch = getMaxBatchSize();
|
|
319
|
+
|
|
320
|
+
const tasks = await api.fetchTasks(gitRemote, {
|
|
321
|
+
includeBacklog: false,
|
|
322
|
+
includeCompleted: false
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const decrypted = tasks.map(decryptTaskFields);
|
|
326
|
+
const active = decrypted.filter(t => !t.isCompleted && !t.is_completed && !t.isBacklog && !t.is_backlog);
|
|
327
|
+
|
|
328
|
+
if (active.length === 0) {
|
|
329
|
+
console.log('No active tasks to offer.');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Take up to maxBatch tasks
|
|
334
|
+
const batch = active.slice(0, maxBatch);
|
|
335
|
+
|
|
336
|
+
if (options.json) {
|
|
337
|
+
console.log(JSON.stringify(batch, null, 2));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(formatBatchOffer(batch));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Run the review flow for completed tasks.
|
|
346
|
+
*
|
|
347
|
+
* @param {Object} options - Review options
|
|
348
|
+
* @returns {Promise<void>}
|
|
349
|
+
*/
|
|
350
|
+
export async function runReview(options = {}) {
|
|
351
|
+
const gitRemote = options.allProjects ? null : getGitRemote();
|
|
352
|
+
|
|
353
|
+
const tasks = await api.fetchTasks(gitRemote, {
|
|
354
|
+
completedOnly: true
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const decrypted = tasks.map(decryptTaskFields);
|
|
358
|
+
|
|
359
|
+
if (decrypted.length === 0) {
|
|
360
|
+
console.log('No completed tasks to review.');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (options.json) {
|
|
365
|
+
console.log(JSON.stringify(decrypted, null, 2));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log(bold(`\nCompleted Tasks for Review (${decrypted.length}):\n`));
|
|
370
|
+
console.log(formatTaskTable(decrypted));
|
|
371
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push CLI - Voice tasks from Push iOS app for Claude Code
|
|
3
|
+
*
|
|
4
|
+
* Main module exports for programmatic usage.
|
|
5
|
+
*
|
|
6
|
+
* @module @masslessai/push-todo
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// CLI entry point
|
|
10
|
+
export { run } from './cli.js';
|
|
11
|
+
|
|
12
|
+
// Task operations
|
|
13
|
+
export {
|
|
14
|
+
listTasks,
|
|
15
|
+
showTask,
|
|
16
|
+
markComplete,
|
|
17
|
+
queueForExecution,
|
|
18
|
+
searchTasks,
|
|
19
|
+
showStatus,
|
|
20
|
+
offerBatch,
|
|
21
|
+
runReview
|
|
22
|
+
} from './fetch.js';
|
|
23
|
+
|
|
24
|
+
// API client
|
|
25
|
+
export {
|
|
26
|
+
fetchTasks,
|
|
27
|
+
fetchTaskByNumber,
|
|
28
|
+
markTaskCompleted,
|
|
29
|
+
queueTask,
|
|
30
|
+
queueTasks,
|
|
31
|
+
searchTasks as apiSearchTasks,
|
|
32
|
+
updateTaskExecution,
|
|
33
|
+
validateApiKey,
|
|
34
|
+
registerProject,
|
|
35
|
+
validateMachine,
|
|
36
|
+
getLatestVersion
|
|
37
|
+
} from './api.js';
|
|
38
|
+
|
|
39
|
+
// Connect and authentication
|
|
40
|
+
export { runConnect } from './connect.js';
|
|
41
|
+
|
|
42
|
+
// Watch/monitor
|
|
43
|
+
export { startWatch } from './watch.js';
|
|
44
|
+
|
|
45
|
+
// Configuration
|
|
46
|
+
export {
|
|
47
|
+
getConfigValue,
|
|
48
|
+
setConfigValue,
|
|
49
|
+
getApiKey,
|
|
50
|
+
saveCredentials,
|
|
51
|
+
getAutoCommitEnabled,
|
|
52
|
+
getMaxBatchSize,
|
|
53
|
+
showSettings,
|
|
54
|
+
toggleSetting
|
|
55
|
+
} from './config.js';
|
|
56
|
+
|
|
57
|
+
// Machine identification
|
|
58
|
+
export {
|
|
59
|
+
getMachineId,
|
|
60
|
+
getMachineName,
|
|
61
|
+
getMachineInfo
|
|
62
|
+
} from './machine-id.js';
|
|
63
|
+
|
|
64
|
+
// Project registry
|
|
65
|
+
export {
|
|
66
|
+
getRegistry,
|
|
67
|
+
resetRegistry,
|
|
68
|
+
ProjectRegistry,
|
|
69
|
+
REGISTRY_FILE
|
|
70
|
+
} from './project-registry.js';
|
|
71
|
+
|
|
72
|
+
// Encryption
|
|
73
|
+
export {
|
|
74
|
+
getEncryptionKey,
|
|
75
|
+
decrypt,
|
|
76
|
+
decryptTodoField,
|
|
77
|
+
isE2EEAvailable
|
|
78
|
+
} from './encryption.js';
|
|
79
|
+
|
|
80
|
+
// Utilities
|
|
81
|
+
export {
|
|
82
|
+
getGitRemote,
|
|
83
|
+
isGitRepo,
|
|
84
|
+
getCurrentBranch,
|
|
85
|
+
getGitRoot,
|
|
86
|
+
getRecentCommits,
|
|
87
|
+
hasUncommittedChanges
|
|
88
|
+
} from './utils/git.js';
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
formatDuration,
|
|
92
|
+
formatDate,
|
|
93
|
+
formatTaskForDisplay,
|
|
94
|
+
formatSearchResult,
|
|
95
|
+
formatTaskTable,
|
|
96
|
+
formatBatchOffer,
|
|
97
|
+
truncate
|
|
98
|
+
} from './utils/format.js';
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
bold,
|
|
102
|
+
dim,
|
|
103
|
+
red,
|
|
104
|
+
green,
|
|
105
|
+
yellow,
|
|
106
|
+
cyan,
|
|
107
|
+
muted,
|
|
108
|
+
codes,
|
|
109
|
+
symbols,
|
|
110
|
+
colorsEnabled
|
|
111
|
+
} from './utils/colors.js';
|
|
112
|
+
|
|
113
|
+
// Version
|
|
114
|
+
export const VERSION = '3.0.0';
|