@masslessai/push-todo 3.7.2 → 3.7.4
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/fetch.js +49 -3
- package/package.json +3 -3
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/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('');
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@masslessai/push-todo",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.4",
|
|
4
4
|
"description": "Voice tasks from Push iOS app for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"push-todo": "
|
|
7
|
+
"push-todo": "bin/push-todo.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./lib/index.js",
|
|
10
10
|
"exports": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
|
47
|
-
"url": "https://github.com/MasslessAI/push-todo-cli"
|
|
47
|
+
"url": "git+https://github.com/MasslessAI/push-todo-cli.git"
|
|
48
48
|
},
|
|
49
49
|
"homepage": "https://pushto.do",
|
|
50
50
|
"bugs": {
|