@masslessai/push-todo 4.1.2 → 4.1.3
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.
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse hook: auto-complete Push tasks on git commit.
|
|
4
|
+
*
|
|
5
|
+
* Fires after every Bash tool call. Detects "Task #NNNN" patterns in git
|
|
6
|
+
* commit messages and marks the corresponding Push tasks as completed.
|
|
7
|
+
*
|
|
8
|
+
* This closes the workflow gap where work committed directly to master
|
|
9
|
+
* (outside the daemon worktree pipeline) would otherwise leave tasks
|
|
10
|
+
* in session_finished state indefinitely.
|
|
11
|
+
*
|
|
12
|
+
* Guards:
|
|
13
|
+
* - Only fires on successful git commit commands
|
|
14
|
+
* - Skips tasks that are already completed
|
|
15
|
+
* - Skips tasks with execution_status=running or queued (daemon owns them)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, existsSync } from 'fs';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
|
|
22
|
+
const CONFIG_FILE = join(homedir(), '.config', 'push', 'config');
|
|
23
|
+
const API_BASE = 'https://jxuzqcbqhiaxmfitzxlo.supabase.co/functions/v1';
|
|
24
|
+
|
|
25
|
+
function getApiKey() {
|
|
26
|
+
if (process.env.PUSH_API_KEY) {
|
|
27
|
+
return process.env.PUSH_API_KEY;
|
|
28
|
+
}
|
|
29
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(CONFIG_FILE, 'utf8');
|
|
34
|
+
const match = content.match(/^export\s+PUSH_API_KEY\s*=\s*["']?([^"'\n]+)["']?/m);
|
|
35
|
+
return match ? match[1] : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readStdin() {
|
|
42
|
+
if (process.stdin.isTTY) return '';
|
|
43
|
+
const chunks = [];
|
|
44
|
+
for await (const chunk of process.stdin) {
|
|
45
|
+
chunks.push(chunk);
|
|
46
|
+
}
|
|
47
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchTask(apiKey, displayNumber) {
|
|
51
|
+
const response = await fetch(
|
|
52
|
+
`${API_BASE}/synced-todos?display_number=${displayNumber}`,
|
|
53
|
+
{
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${apiKey}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
signal: AbortSignal.timeout(8000),
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
if (!response.ok) return null;
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
const todos = data.todos || [];
|
|
64
|
+
return todos.length > 0 ? todos[0] : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function markCompleted(apiKey, todoId) {
|
|
68
|
+
const response = await fetch(`${API_BASE}/todo-status`, {
|
|
69
|
+
method: 'PATCH',
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${apiKey}`,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
todoId,
|
|
76
|
+
isCompleted: true,
|
|
77
|
+
completedAt: new Date().toISOString(),
|
|
78
|
+
completionComment: 'Completed via direct commit in Claude Code session',
|
|
79
|
+
}),
|
|
80
|
+
signal: AbortSignal.timeout(8000),
|
|
81
|
+
});
|
|
82
|
+
return response.ok;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
const apiKey = getApiKey();
|
|
87
|
+
if (!apiKey) process.exit(0);
|
|
88
|
+
|
|
89
|
+
const stdin = await readStdin();
|
|
90
|
+
if (!stdin.trim()) process.exit(0);
|
|
91
|
+
|
|
92
|
+
let hookData;
|
|
93
|
+
try {
|
|
94
|
+
hookData = JSON.parse(stdin);
|
|
95
|
+
} catch {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Only handle Bash tool calls
|
|
100
|
+
if (hookData.tool_name !== 'Bash') process.exit(0);
|
|
101
|
+
|
|
102
|
+
// Only handle successful calls (skip errored commands)
|
|
103
|
+
if (hookData.tool_response?.isError) process.exit(0);
|
|
104
|
+
|
|
105
|
+
const command = hookData.tool_input?.command || '';
|
|
106
|
+
|
|
107
|
+
// Only handle git commit commands
|
|
108
|
+
if (!command.includes('git commit')) process.exit(0);
|
|
109
|
+
|
|
110
|
+
// Extract all unique Task #NNNN patterns from the commit message
|
|
111
|
+
const taskMatches = [...command.matchAll(/Task\s+#(\d+)/gi)];
|
|
112
|
+
if (taskMatches.length === 0) process.exit(0);
|
|
113
|
+
|
|
114
|
+
const taskNumbers = [...new Set(taskMatches.map((m) => parseInt(m[1], 10)))];
|
|
115
|
+
|
|
116
|
+
for (const num of taskNumbers) {
|
|
117
|
+
try {
|
|
118
|
+
const task = await fetchTask(apiKey, num);
|
|
119
|
+
if (!task) continue;
|
|
120
|
+
|
|
121
|
+
// Skip already completed tasks
|
|
122
|
+
if (task.isCompleted) continue;
|
|
123
|
+
|
|
124
|
+
// Skip tasks the daemon is actively working on
|
|
125
|
+
const status = task.executionStatus;
|
|
126
|
+
if (status === 'running' || status === 'queued') continue;
|
|
127
|
+
|
|
128
|
+
await markCompleted(apiKey, task.id);
|
|
129
|
+
} catch {
|
|
130
|
+
// Never block Claude on hook errors
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main().catch(() => process.exit(0));
|