@masslessai/push-todo 3.7.3 → 3.7.5
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/hooks/session-end.js +68 -41
- package/lib/api.js +1 -1
- package/lib/config.js +88 -6
- package/lib/daemon.js +124 -1
- package/lib/fetch.js +49 -3
- package/package.json +1 -1
package/hooks/session-end.js
CHANGED
|
@@ -2,28 +2,29 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Session end hook for Push CLI.
|
|
4
4
|
*
|
|
5
|
-
* Reports
|
|
5
|
+
* Reports session_finished status to Supabase when a Claude Code session ends.
|
|
6
|
+
* Reads the active task from ~/.push/active_task.json (written by fetch.js showTask).
|
|
7
|
+
*
|
|
8
|
+
* This mirrors the daemon's completion flow: daemon sends session_finished
|
|
9
|
+
* when its Claude process exits, this hook does the same for foreground sessions.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
import { existsSync, readFileSync } from 'fs';
|
|
9
|
-
import { homedir } from 'os';
|
|
12
|
+
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
13
|
+
import { homedir, hostname } from 'os';
|
|
10
14
|
import { join } from 'path';
|
|
11
15
|
|
|
12
16
|
const CONFIG_FILE = join(homedir(), '.config', 'push', 'config');
|
|
17
|
+
const MACHINE_ID_FILE = join(homedir(), '.config', 'push', 'machine_id');
|
|
18
|
+
const ACTIVE_TASK_FILE = join(homedir(), '.push', 'active_task.json');
|
|
13
19
|
const API_BASE = 'https://jxuzqcbqhiaxmfitzxlo.supabase.co/functions/v1';
|
|
14
20
|
|
|
15
|
-
/**
|
|
16
|
-
* Get the API key from config.
|
|
17
|
-
*/
|
|
18
21
|
function getApiKey() {
|
|
19
22
|
if (process.env.PUSH_API_KEY) {
|
|
20
23
|
return process.env.PUSH_API_KEY;
|
|
21
24
|
}
|
|
22
|
-
|
|
23
25
|
if (!existsSync(CONFIG_FILE)) {
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
|
-
|
|
27
28
|
try {
|
|
28
29
|
const content = readFileSync(CONFIG_FILE, 'utf8');
|
|
29
30
|
const match = content.match(/^export\s+PUSH_API_KEY\s*=\s*["']?([^"'\n]+)["']?/m);
|
|
@@ -33,65 +34,91 @@ function getApiKey() {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
* Get the machine ID.
|
|
38
|
-
*/
|
|
39
37
|
function getMachineId() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (existsSync(machineIdFile)) {
|
|
38
|
+
if (existsSync(MACHINE_ID_FILE)) {
|
|
43
39
|
try {
|
|
44
|
-
return readFileSync(
|
|
40
|
+
return readFileSync(MACHINE_ID_FILE, 'utf8').trim();
|
|
45
41
|
} catch {
|
|
46
42
|
return null;
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
|
-
|
|
50
45
|
return null;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
function getActiveTask() {
|
|
49
|
+
if (!existsSync(ACTIVE_TASK_FILE)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(ACTIVE_TASK_FILE, 'utf8'));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clearActiveTask() {
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(ACTIVE_TASK_FILE)) {
|
|
62
|
+
unlinkSync(ACTIVE_TASK_FILE);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore cleanup errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function reportSessionFinished(apiKey, activeTask) {
|
|
70
|
+
const machineId = getMachineId();
|
|
71
|
+
const machineName = hostname();
|
|
72
|
+
const startedAt = activeTask.startedAt ? new Date(activeTask.startedAt) : null;
|
|
73
|
+
const now = new Date();
|
|
74
|
+
|
|
75
|
+
const durationMs = startedAt ? now.getTime() - startedAt.getTime() : null;
|
|
76
|
+
const durationStr = durationMs
|
|
77
|
+
? `${Math.floor(durationMs / 60000)}m ${Math.floor((durationMs % 60000) / 1000)}s`
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
const payload = {
|
|
81
|
+
displayNumber: activeTask.displayNumber,
|
|
82
|
+
status: 'session_finished',
|
|
83
|
+
machineId,
|
|
84
|
+
machineName,
|
|
85
|
+
event: {
|
|
86
|
+
type: 'session_finished',
|
|
87
|
+
timestamp: now.toISOString(),
|
|
88
|
+
machineName: machineName || undefined,
|
|
89
|
+
summary: durationStr
|
|
90
|
+
? `Foreground session ended (${durationStr})`
|
|
91
|
+
: 'Foreground session ended',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
57
95
|
try {
|
|
58
|
-
const response = await fetch(`${API_BASE}/
|
|
59
|
-
method: '
|
|
96
|
+
const response = await fetch(`${API_BASE}/update-task-execution`, {
|
|
97
|
+
method: 'PATCH',
|
|
60
98
|
headers: {
|
|
61
99
|
'Authorization': `Bearer ${apiKey}`,
|
|
62
|
-
'Content-Type': 'application/json'
|
|
100
|
+
'Content-Type': 'application/json',
|
|
63
101
|
},
|
|
64
|
-
body: JSON.stringify(
|
|
65
|
-
|
|
66
|
-
exit_reason: exitReason,
|
|
67
|
-
timestamp: new Date().toISOString()
|
|
68
|
-
}),
|
|
69
|
-
signal: AbortSignal.timeout(10000)
|
|
102
|
+
body: JSON.stringify(payload),
|
|
103
|
+
signal: AbortSignal.timeout(10000),
|
|
70
104
|
});
|
|
71
|
-
|
|
72
105
|
return response.ok;
|
|
73
106
|
} catch {
|
|
74
107
|
return false;
|
|
75
108
|
}
|
|
76
109
|
}
|
|
77
110
|
|
|
78
|
-
/**
|
|
79
|
-
* Main entry point.
|
|
80
|
-
*/
|
|
81
111
|
async function main() {
|
|
82
112
|
const apiKey = getApiKey();
|
|
83
|
-
const
|
|
113
|
+
const activeTask = getActiveTask();
|
|
84
114
|
|
|
85
|
-
if (!apiKey || !
|
|
86
|
-
// Not configured
|
|
115
|
+
if (!apiKey || !activeTask) {
|
|
116
|
+
// Not configured or no active task — silent exit
|
|
87
117
|
process.exit(0);
|
|
88
118
|
}
|
|
89
119
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Report to backend
|
|
94
|
-
await reportSessionEnd(apiKey, machineId, exitReason);
|
|
120
|
+
await reportSessionFinished(apiKey, activeTask);
|
|
121
|
+
clearActiveTask();
|
|
95
122
|
|
|
96
123
|
process.exit(0);
|
|
97
124
|
}
|
package/lib/api.js
CHANGED
|
@@ -229,7 +229,7 @@ export async function searchTasks(query, gitRemote = null) {
|
|
|
229
229
|
*/
|
|
230
230
|
export async function updateTaskExecution(payload) {
|
|
231
231
|
const response = await apiRequest('update-task-execution', {
|
|
232
|
-
method: '
|
|
232
|
+
method: 'PATCH',
|
|
233
233
|
body: JSON.stringify(payload)
|
|
234
234
|
});
|
|
235
235
|
|
package/lib/config.js
CHANGED
|
@@ -150,6 +150,48 @@ export function setAutoCommitEnabled(enabled) {
|
|
|
150
150
|
return setConfigValue('AUTO_COMMIT', enabled ? 'true' : 'false');
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Check if auto-merge into main is enabled after daemon task completion.
|
|
155
|
+
* Default: true (merge PR into main after session_finished)
|
|
156
|
+
*
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
export function getAutoMergeEnabled() {
|
|
160
|
+
const value = getConfigValue('AUTO_MERGE', 'true');
|
|
161
|
+
return value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === 'yes';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Set auto-merge setting.
|
|
166
|
+
*
|
|
167
|
+
* @param {boolean} enabled
|
|
168
|
+
* @returns {boolean} True if successful
|
|
169
|
+
*/
|
|
170
|
+
export function setAutoMergeEnabled(enabled) {
|
|
171
|
+
return setConfigValue('AUTO_MERGE', enabled ? 'true' : 'false');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if auto-complete is enabled after daemon task merge.
|
|
176
|
+
* Default: true (mark task as completed after successful merge)
|
|
177
|
+
*
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
export function getAutoCompleteEnabled() {
|
|
181
|
+
const value = getConfigValue('AUTO_COMPLETE', 'true');
|
|
182
|
+
return value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === 'yes';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Set auto-complete setting.
|
|
187
|
+
*
|
|
188
|
+
* @param {boolean} enabled
|
|
189
|
+
* @returns {boolean} True if successful
|
|
190
|
+
*/
|
|
191
|
+
export function setAutoCompleteEnabled(enabled) {
|
|
192
|
+
return setConfigValue('AUTO_COMPLETE', enabled ? 'true' : 'false');
|
|
193
|
+
}
|
|
194
|
+
|
|
153
195
|
/**
|
|
154
196
|
* Get the maximum batch size for queuing tasks.
|
|
155
197
|
* Default: 5
|
|
@@ -226,16 +268,24 @@ export function showSettings() {
|
|
|
226
268
|
console.log();
|
|
227
269
|
|
|
228
270
|
const autoCommit = getAutoCommitEnabled();
|
|
271
|
+
const autoMerge = getAutoMergeEnabled();
|
|
272
|
+
const autoComplete = getAutoCompleteEnabled();
|
|
229
273
|
const batchSize = getMaxBatchSize();
|
|
230
274
|
|
|
231
|
-
console.log(` auto-commit:
|
|
232
|
-
console.log('
|
|
275
|
+
console.log(` auto-commit: ${autoCommit ? 'ON' : 'OFF'}`);
|
|
276
|
+
console.log(' Auto-commit when task completes');
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(` auto-merge: ${autoMerge ? 'ON' : 'OFF'}`);
|
|
279
|
+
console.log(' Merge PR into main after daemon task finishes');
|
|
233
280
|
console.log();
|
|
234
|
-
console.log(`
|
|
235
|
-
console.log('
|
|
281
|
+
console.log(` auto-complete: ${autoComplete ? 'ON' : 'OFF'}`);
|
|
282
|
+
console.log(' Mark task completed after successful merge');
|
|
283
|
+
console.log();
|
|
284
|
+
console.log(` batch-size: ${batchSize}`);
|
|
285
|
+
console.log(' Max tasks for batch queue (1-20)');
|
|
236
286
|
console.log();
|
|
237
287
|
console.log(' Toggle with: push-todo setting <name>');
|
|
238
|
-
console.log(' Example: push-todo setting auto-
|
|
288
|
+
console.log(' Example: push-todo setting auto-merge');
|
|
239
289
|
console.log();
|
|
240
290
|
}
|
|
241
291
|
|
|
@@ -264,6 +314,38 @@ export function toggleSetting(settingName) {
|
|
|
264
314
|
return false;
|
|
265
315
|
}
|
|
266
316
|
|
|
317
|
+
if (normalized === 'auto-merge') {
|
|
318
|
+
const current = getAutoMergeEnabled();
|
|
319
|
+
const newValue = !current;
|
|
320
|
+
if (setAutoMergeEnabled(newValue)) {
|
|
321
|
+
console.log(`Auto-merge is now ${newValue ? 'ON' : 'OFF'}`);
|
|
322
|
+
if (newValue) {
|
|
323
|
+
console.log('PRs will be merged into main after daemon task finishes.');
|
|
324
|
+
} else {
|
|
325
|
+
console.log('PRs will NOT be merged automatically.');
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
console.error('Failed to update setting');
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (normalized === 'auto-complete') {
|
|
334
|
+
const current = getAutoCompleteEnabled();
|
|
335
|
+
const newValue = !current;
|
|
336
|
+
if (setAutoCompleteEnabled(newValue)) {
|
|
337
|
+
console.log(`Auto-complete is now ${newValue ? 'ON' : 'OFF'}`);
|
|
338
|
+
if (newValue) {
|
|
339
|
+
console.log('Tasks will be marked completed after successful merge.');
|
|
340
|
+
} else {
|
|
341
|
+
console.log('Tasks will NOT be marked completed automatically.');
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
console.error('Failed to update setting');
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
267
349
|
if (normalized === 'batch-size') {
|
|
268
350
|
const batchSize = getMaxBatchSize();
|
|
269
351
|
console.log(`Current batch size: ${batchSize}`);
|
|
@@ -272,7 +354,7 @@ export function toggleSetting(settingName) {
|
|
|
272
354
|
}
|
|
273
355
|
|
|
274
356
|
console.error(`Unknown setting: ${settingName}`);
|
|
275
|
-
console.error('Available settings: auto-commit, batch-size');
|
|
357
|
+
console.error('Available settings: auto-commit, auto-merge, auto-complete, batch-size');
|
|
276
358
|
return false;
|
|
277
359
|
}
|
|
278
360
|
|
package/lib/daemon.js
CHANGED
|
@@ -199,6 +199,37 @@ function getVersion() {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
function getConfigValueFromFile(key, defaultValue = '') {
|
|
203
|
+
const fullKey = `PUSH_${key}`;
|
|
204
|
+
if (!existsSync(CONFIG_FILE)) return defaultValue;
|
|
205
|
+
try {
|
|
206
|
+
const content = readFileSync(CONFIG_FILE, 'utf8');
|
|
207
|
+
for (const line of content.split('\n')) {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
if (trimmed.startsWith(`export ${fullKey}=`)) {
|
|
210
|
+
let value = trimmed.split('=')[1] || '';
|
|
211
|
+
value = value.trim();
|
|
212
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
213
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
214
|
+
value = value.slice(1, -1);
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {}
|
|
220
|
+
return defaultValue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getAutoMergeEnabled() {
|
|
224
|
+
const v = getConfigValueFromFile('AUTO_MERGE', 'true');
|
|
225
|
+
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getAutoCompleteEnabled() {
|
|
229
|
+
const v = getConfigValueFromFile('AUTO_COMPLETE', 'true');
|
|
230
|
+
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
231
|
+
}
|
|
232
|
+
|
|
202
233
|
// ==================== E2EE Decryption ====================
|
|
203
234
|
|
|
204
235
|
let decryptTodoField = null;
|
|
@@ -596,6 +627,83 @@ Automated PR from Push daemon for task #${displayNumber}.
|
|
|
596
627
|
}
|
|
597
628
|
}
|
|
598
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Merge a PR into main and update local main branch.
|
|
632
|
+
* Uses `gh pr merge` for clean remote merge, then pulls locally.
|
|
633
|
+
*
|
|
634
|
+
* @returns {boolean} True if merge succeeded
|
|
635
|
+
*/
|
|
636
|
+
function mergePRForTask(displayNumber, prUrl, projectPath) {
|
|
637
|
+
const gitCwd = projectPath || process.cwd();
|
|
638
|
+
|
|
639
|
+
// Extract PR number from URL (e.g., https://github.com/user/repo/pull/42)
|
|
640
|
+
const prMatch = prUrl.match(/\/pull\/(\d+)/);
|
|
641
|
+
if (!prMatch) {
|
|
642
|
+
logError(`Could not extract PR number from: ${prUrl}`);
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
const prNumber = prMatch[1];
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
execFileSync('gh', ['pr', 'merge', prNumber, '--merge', '--delete-branch'], {
|
|
649
|
+
cwd: gitCwd,
|
|
650
|
+
timeout: 60000,
|
|
651
|
+
stdio: 'pipe'
|
|
652
|
+
});
|
|
653
|
+
log(`Merged PR #${prNumber} for task #${displayNumber}`);
|
|
654
|
+
|
|
655
|
+
// Pull main locally so next worktree is up to date
|
|
656
|
+
try {
|
|
657
|
+
execFileSync('git', ['pull', '--ff-only'], {
|
|
658
|
+
cwd: gitCwd,
|
|
659
|
+
timeout: 30000,
|
|
660
|
+
stdio: 'pipe'
|
|
661
|
+
});
|
|
662
|
+
log('Updated local main branch');
|
|
663
|
+
} catch {
|
|
664
|
+
log('Could not pull main (may not be checked out), skipping local update');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return true;
|
|
668
|
+
} catch (e) {
|
|
669
|
+
const stderr = e.stderr?.toString() || e.message || '';
|
|
670
|
+
if (stderr.includes('merge conflict') || stderr.includes('conflict')) {
|
|
671
|
+
logError(`PR #${prNumber} has merge conflicts, skipping auto-merge`);
|
|
672
|
+
} else if (stderr.includes('not found') || stderr.includes('ENOENT')) {
|
|
673
|
+
log('GitHub CLI (gh) not installed, skipping merge');
|
|
674
|
+
} else {
|
|
675
|
+
logError(`Failed to merge PR #${prNumber}: ${stderr.slice(0, 200)}`);
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Mark a task as completed via the todo-status endpoint.
|
|
683
|
+
*/
|
|
684
|
+
async function markTaskAsCompleted(displayNumber, taskId, comment) {
|
|
685
|
+
try {
|
|
686
|
+
const response = await apiRequest('todo-status', {
|
|
687
|
+
method: 'PATCH',
|
|
688
|
+
body: JSON.stringify({
|
|
689
|
+
todoId: taskId,
|
|
690
|
+
isCompleted: true,
|
|
691
|
+
completedAt: new Date().toISOString(),
|
|
692
|
+
completionComment: comment || `Completed by daemon on ${getMachineName()}`
|
|
693
|
+
})
|
|
694
|
+
});
|
|
695
|
+
if (response.ok) {
|
|
696
|
+
log(`Task #${displayNumber} marked as completed`);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
logError(`Failed to mark #${displayNumber} complete: HTTP ${response.status}`);
|
|
700
|
+
return false;
|
|
701
|
+
} catch (error) {
|
|
702
|
+
logError(`Failed to mark #${displayNumber} complete: ${error.message}`);
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
599
707
|
// ==================== Stuck Detection ====================
|
|
600
708
|
|
|
601
709
|
function checkStuckPatterns(displayNumber, line) {
|
|
@@ -1000,12 +1108,27 @@ function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1000
1108
|
);
|
|
1001
1109
|
}
|
|
1002
1110
|
|
|
1111
|
+
// Auto-merge PR into main (configurable, default ON)
|
|
1112
|
+
let merged = false;
|
|
1113
|
+
if (getAutoMergeEnabled() && prUrl) {
|
|
1114
|
+
merged = mergePRForTask(displayNumber, prUrl, projectPath);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Auto-complete task after successful merge (configurable, default ON)
|
|
1118
|
+
const taskId = info.taskId;
|
|
1119
|
+
if (getAutoCompleteEnabled() && merged && taskId) {
|
|
1120
|
+
const comment = semanticSummary
|
|
1121
|
+
? `${semanticSummary} (${durationStr} on ${machineName})`
|
|
1122
|
+
: `Completed in ${durationStr} on ${machineName}`;
|
|
1123
|
+
markTaskAsCompleted(displayNumber, taskId, comment);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1003
1126
|
completedToday.push({
|
|
1004
1127
|
displayNumber,
|
|
1005
1128
|
summary,
|
|
1006
1129
|
completedAt: new Date().toISOString(),
|
|
1007
1130
|
duration,
|
|
1008
|
-
status: 'session_finished',
|
|
1131
|
+
status: merged ? 'completed' : 'session_finished',
|
|
1009
1132
|
prUrl,
|
|
1010
1133
|
sessionId
|
|
1011
1134
|
});
|
package/lib/fetch.js
CHANGED
|
@@ -12,6 +12,9 @@ import { formatTaskForDisplay, formatSearchResult } from './utils/format.js';
|
|
|
12
12
|
import { bold, green, yellow, red, cyan, dim, muted } from './utils/colors.js';
|
|
13
13
|
import { decryptTodoField, isE2EEAvailable } from './encryption.js';
|
|
14
14
|
import { getAutoCommitEnabled, getMaxBatchSize } from './config.js';
|
|
15
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
import { join } from 'path';
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Decrypt encrypted fields in a task object.
|
|
@@ -121,7 +124,7 @@ export async function listTasks(options = {}) {
|
|
|
121
124
|
if (!options.backlog && !options.completed && running.length > 0) {
|
|
122
125
|
console.log(`# ${running.length} Running/Queued Tasks (${scope})\n`);
|
|
123
126
|
for (const task of running) {
|
|
124
|
-
const displayNum = task.displayNumber
|
|
127
|
+
const displayNum = task.displayNumber;
|
|
125
128
|
console.log(`---\n### #${displayNum}\n`);
|
|
126
129
|
console.log(formatTaskForDisplay(task));
|
|
127
130
|
console.log('');
|
|
@@ -134,7 +137,7 @@ export async function listTasks(options = {}) {
|
|
|
134
137
|
|
|
135
138
|
// Show full details for each task (matching Python behavior)
|
|
136
139
|
for (const task of tasksToShow) {
|
|
137
|
-
const displayNum = task.displayNumber
|
|
140
|
+
const displayNum = task.displayNumber;
|
|
138
141
|
console.log(`---\n### #${displayNum}\n`);
|
|
139
142
|
console.log(formatTaskForDisplay(task));
|
|
140
143
|
console.log('');
|
|
@@ -183,6 +186,49 @@ export async function showTask(displayNumber, options = {}) {
|
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
console.log(formatTaskForDisplay(decrypted));
|
|
189
|
+
|
|
190
|
+
// Track active task for session-end hook and send running status to Supabase.
|
|
191
|
+
// Skip if task is already running or queued (daemon owns it).
|
|
192
|
+
const execStatus = decrypted.executionStatus;
|
|
193
|
+
if (execStatus !== 'running' && execStatus !== 'queued') {
|
|
194
|
+
trackActiveTask(decrypted).catch(() => {});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Track active task for foreground execution.
|
|
200
|
+
*
|
|
201
|
+
* Mirrors the daemon's behavior: sends 'running' status to Supabase
|
|
202
|
+
* and writes ~/.push/active_task.json for the session-end hook.
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} task - Task object from API
|
|
205
|
+
*/
|
|
206
|
+
async function trackActiveTask(task) {
|
|
207
|
+
const pushDir = join(homedir(), '.push');
|
|
208
|
+
mkdirSync(pushDir, { recursive: true });
|
|
209
|
+
|
|
210
|
+
// Write active task file for session-end hook to read
|
|
211
|
+
writeFileSync(join(pushDir, 'active_task.json'), JSON.stringify({
|
|
212
|
+
displayNumber: task.displayNumber,
|
|
213
|
+
taskId: task.id,
|
|
214
|
+
startedAt: new Date().toISOString(),
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
// Send running status to Supabase (same as daemon's updateTaskStatus)
|
|
218
|
+
const machineId = getMachineId();
|
|
219
|
+
const machineName = getMachineName();
|
|
220
|
+
await api.updateTaskExecution({
|
|
221
|
+
displayNumber: task.displayNumber,
|
|
222
|
+
status: 'running',
|
|
223
|
+
machineId,
|
|
224
|
+
machineName,
|
|
225
|
+
event: {
|
|
226
|
+
type: 'started',
|
|
227
|
+
timestamp: new Date().toISOString(),
|
|
228
|
+
machineName: machineName || undefined,
|
|
229
|
+
summary: 'Foreground session started',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
186
232
|
}
|
|
187
233
|
|
|
188
234
|
/**
|
|
@@ -432,7 +478,7 @@ export async function runReview(options = {}) {
|
|
|
432
478
|
|
|
433
479
|
// Show full details for each task (matching Python behavior)
|
|
434
480
|
for (const task of decrypted) {
|
|
435
|
-
const displayNum = task.displayNumber
|
|
481
|
+
const displayNum = task.displayNumber;
|
|
436
482
|
console.log(`---\n### #${displayNum}\n`);
|
|
437
483
|
console.log(formatTaskForDisplay(task));
|
|
438
484
|
console.log('');
|