@masslessai/push-todo 4.1.1 → 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.
package/hooks/hooks.json CHANGED
@@ -21,6 +21,18 @@
21
21
  }
22
22
  ]
23
23
  }
24
+ ],
25
+ "PostToolUse": [
26
+ {
27
+ "matcher": "Bash",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-task-complete.js",
32
+ "timeout": 15
33
+ }
34
+ ]
35
+ }
24
36
  ]
25
37
  }
26
38
  }
@@ -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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {