@iloom/cli 0.3.4 → 0.4.1

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.
Files changed (181) hide show
  1. package/README.md +13 -3
  2. package/dist/{BranchNamingService-A77VI6AI.js → BranchNamingService-GCCWB3LK.js} +4 -3
  3. package/dist/ClaudeContextManager-DK77227F.js +16 -0
  4. package/dist/ClaudeService-W3SA7HVG.js +15 -0
  5. package/dist/GitHubService-RPM27GWD.js +12 -0
  6. package/dist/{LoomLauncher-ZV3ZZIBA.js → LoomLauncher-S3YGJRJQ.js} +43 -27
  7. package/dist/LoomLauncher-S3YGJRJQ.js.map +1 -0
  8. package/dist/PromptTemplateManager-2TDZAUC6.js +9 -0
  9. package/dist/README.md +13 -3
  10. package/dist/{SettingsManager-I2LRCW2A.js → SettingsManager-FJFU6JJD.js} +7 -3
  11. package/dist/SettingsMigrationManager-EH3J2TCN.js +10 -0
  12. package/dist/{chunk-5Q3NDNNV.js → chunk-2W2FBL5G.js} +153 -6
  13. package/dist/chunk-2W2FBL5G.js.map +1 -0
  14. package/dist/{chunk-OXAM2WVC.js → chunk-55TB3FSG.js} +21 -1
  15. package/dist/chunk-55TB3FSG.js.map +1 -0
  16. package/dist/chunk-6UIGZD2N.js +20 -0
  17. package/dist/chunk-6UIGZD2N.js.map +1 -0
  18. package/dist/{chunk-RIEO2WML.js → chunk-74VMN2KC.js} +26 -2
  19. package/dist/chunk-74VMN2KC.js.map +1 -0
  20. package/dist/{chunk-2MAIX45J.js → chunk-BIIQHEXJ.js} +104 -43
  21. package/dist/chunk-BIIQHEXJ.js.map +1 -0
  22. package/dist/{chunk-UAN4A3YU.js → chunk-G6CIIJLT.js} +11 -11
  23. package/dist/{chunk-DLHA5VQ3.js → chunk-HD5SUKI2.js} +36 -179
  24. package/dist/chunk-HD5SUKI2.js.map +1 -0
  25. package/dist/{chunk-2IJEMXOB.js → chunk-IARWMDAX.js} +427 -428
  26. package/dist/chunk-IARWMDAX.js.map +1 -0
  27. package/dist/chunk-IJ7IGJT3.js +192 -0
  28. package/dist/chunk-IJ7IGJT3.js.map +1 -0
  29. package/dist/{chunk-2CXREBLZ.js → chunk-JC5HXN75.js} +8 -6
  30. package/dist/chunk-JC5HXN75.js.map +1 -0
  31. package/dist/{chunk-3RUPPQRG.js → chunk-KO2FOMHL.js} +43 -2
  32. package/dist/{chunk-3RUPPQRG.js.map → chunk-KO2FOMHL.js.map} +1 -1
  33. package/dist/{chunk-4XIDC3NF.js → chunk-MD6HA5IK.js} +2 -2
  34. package/dist/{chunk-OC4H6HJD.js → chunk-O7WHXLCB.js} +2 -2
  35. package/dist/{chunk-M7JJCX53.js → chunk-OEGECBFS.js} +20 -20
  36. package/dist/chunk-OEGECBFS.js.map +1 -0
  37. package/dist/{chunk-MKWYLDFK.js → chunk-OF7BNW4D.js} +43 -3
  38. package/dist/chunk-OF7BNW4D.js.map +1 -0
  39. package/dist/{chunk-PGPI5LR4.js → chunk-POI7KLBH.js} +7 -21
  40. package/dist/chunk-POI7KLBH.js.map +1 -0
  41. package/dist/{chunk-PA6Q6AWM.js → chunk-PSFVTBM7.js} +2 -2
  42. package/dist/chunk-QHA67Q7A.js +281 -0
  43. package/dist/chunk-QHA67Q7A.js.map +1 -0
  44. package/dist/{chunk-SUOXY5WJ.js → chunk-QIUJPPJQ.js} +5 -5
  45. package/dist/chunk-QIUJPPJQ.js.map +1 -0
  46. package/dist/{chunk-ZM3CFL5L.js → chunk-QRBOPFAA.js} +3 -3
  47. package/dist/{chunk-OYF4VIFI.js → chunk-RUC7OULH.js} +147 -22
  48. package/dist/chunk-RUC7OULH.js.map +1 -0
  49. package/dist/{chunk-CE26YH2U.js → chunk-SJ2GZ6RF.js} +48 -50
  50. package/dist/chunk-SJ2GZ6RF.js.map +1 -0
  51. package/dist/{chunk-SSCQCCJ7.js → chunk-THF25ICZ.js} +2 -2
  52. package/dist/chunk-TMZAVPGF.js +667 -0
  53. package/dist/chunk-TMZAVPGF.js.map +1 -0
  54. package/dist/{chunk-5VK4NRSF.js → chunk-UNXRACJ7.js} +35 -36
  55. package/dist/chunk-UNXRACJ7.js.map +1 -0
  56. package/dist/{chunk-AKUJXDNW.js → chunk-UPUAQYAW.js} +3 -3
  57. package/dist/{chunk-GEHQXLEI.js → chunk-UYVWLISQ.js} +18 -35
  58. package/dist/chunk-UYVWLISQ.js.map +1 -0
  59. package/dist/{chunk-OSCLCMDG.js → chunk-UYWAESOT.js} +3 -3
  60. package/dist/{chunk-RW54ZMBM.js → chunk-VAYGNQTE.js} +2 -2
  61. package/dist/{chunk-ZT3YZB4K.js → chunk-VBFDVGAE.js} +12 -12
  62. package/dist/chunk-VBFDVGAE.js.map +1 -0
  63. package/dist/{chunk-IFB4Z76W.js → chunk-VTXCGKV5.js} +13 -12
  64. package/dist/chunk-VTXCGKV5.js.map +1 -0
  65. package/dist/{chunk-CDZERT7Z.js → chunk-VWNS6DH5.js} +48 -4
  66. package/dist/chunk-VWNS6DH5.js.map +1 -0
  67. package/dist/{chunk-CFFQ2Z7A.js → chunk-WUQQNE63.js} +2 -2
  68. package/dist/{chunk-UJL4HI2R.js → chunk-Z5NXYJIG.js} +20 -2
  69. package/dist/chunk-Z5NXYJIG.js.map +1 -0
  70. package/dist/{claude-W52VKI6L.js → claude-ACVXNB6N.js} +8 -5
  71. package/dist/{cleanup-H4VXU3C3.js → cleanup-KDLVTT7M.js} +133 -122
  72. package/dist/cleanup-KDLVTT7M.js.map +1 -0
  73. package/dist/cli.js +953 -430
  74. package/dist/cli.js.map +1 -1
  75. package/dist/{color-F7RU6B6Z.js → color-ZPIIUADB.js} +3 -3
  76. package/dist/{contribute-Y7IQV5QY.js → contribute-HY372S6F.js} +8 -6
  77. package/dist/{contribute-Y7IQV5QY.js.map → contribute-HY372S6F.js.map} +1 -1
  78. package/dist/dev-server-JCJGQ3PV.js +298 -0
  79. package/dist/dev-server-JCJGQ3PV.js.map +1 -0
  80. package/dist/{feedback-XTUCKJNT.js → feedback-7PVBQNLJ.js} +13 -12
  81. package/dist/{feedback-XTUCKJNT.js.map → feedback-7PVBQNLJ.js.map} +1 -1
  82. package/dist/{git-IYA53VIC.js → git-4BVOOOOV.js} +16 -4
  83. package/dist/hooks/iloom-hook.js +258 -0
  84. package/dist/{ignite-T74RYXCA.js → ignite-3B264M7K.js} +245 -39
  85. package/dist/ignite-3B264M7K.js.map +1 -0
  86. package/dist/index.d.ts +461 -124
  87. package/dist/index.js +743 -210
  88. package/dist/index.js.map +1 -1
  89. package/dist/init-LBA6NUK2.js +21 -0
  90. package/dist/{installation-detector-VARGFFRZ.js → installation-detector-6R6YOFVZ.js} +3 -3
  91. package/dist/mcp/issue-management-server.js +2 -1
  92. package/dist/mcp/issue-management-server.js.map +1 -1
  93. package/dist/neon-helpers-L5CXQ5CT.js +11 -0
  94. package/dist/{open-UMXANW5S.js → open-OGCV32Z4.js} +15 -13
  95. package/dist/{open-UMXANW5S.js.map → open-OGCV32Z4.js.map} +1 -1
  96. package/dist/projects-P55273AB.js +73 -0
  97. package/dist/projects-P55273AB.js.map +1 -0
  98. package/dist/{prompt-QALMYTVC.js → prompt-A7GGRHSY.js} +3 -3
  99. package/dist/prompts/init-prompt.txt +49 -0
  100. package/dist/prompts/issue-prompt.txt +110 -8
  101. package/dist/prompts/regular-prompt.txt +90 -0
  102. package/dist/prompts/session-summary-prompt.txt +82 -0
  103. package/dist/{rebase-VJ2VKR6R.js → rebase-4T5FQHNH.js} +11 -9
  104. package/dist/{rebase-VJ2VKR6R.js.map → rebase-4T5FQHNH.js.map} +1 -1
  105. package/dist/{remote-VUNCQZ6J.js → remote-73TZ2ADI.js} +3 -3
  106. package/dist/{run-MJYY4PUT.js → run-HNOP6WE2.js} +15 -13
  107. package/dist/{run-MJYY4PUT.js.map → run-HNOP6WE2.js.map} +1 -1
  108. package/dist/schema/settings.schema.json +49 -0
  109. package/dist/shell-DE3HKJSM.js +240 -0
  110. package/dist/shell-DE3HKJSM.js.map +1 -0
  111. package/dist/summary-GDT7DTRI.js +244 -0
  112. package/dist/summary-GDT7DTRI.js.map +1 -0
  113. package/dist/{test-git-IT5EWQ5C.js → test-git-YMAE57UP.js} +6 -4
  114. package/dist/{test-git-IT5EWQ5C.js.map → test-git-YMAE57UP.js.map} +1 -1
  115. package/dist/{test-prefix-NPWDPUUH.js → test-prefix-YCKL6CMT.js} +6 -4
  116. package/dist/{test-prefix-NPWDPUUH.js.map → test-prefix-YCKL6CMT.js.map} +1 -1
  117. package/dist/{test-tabs-PRMRSHKI.js → test-tabs-3SCJWRKT.js} +4 -4
  118. package/dist/{test-webserver-DAHONWCS.js → test-webserver-VPNLAFZ3.js} +2 -2
  119. package/dist/{update-4TDDUR5K.js → update-LETF5ASC.js} +4 -4
  120. package/dist/{update-notifier-QEX3CJHA.js → update-notifier-H55ZK7NU.js} +3 -3
  121. package/package.json +6 -6
  122. package/dist/ClaudeContextManager-BN7RE5ZQ.js +0 -15
  123. package/dist/ClaudeService-DLYLJUPA.js +0 -14
  124. package/dist/GitHubService-FZHHBOFG.js +0 -11
  125. package/dist/LoomLauncher-ZV3ZZIBA.js.map +0 -1
  126. package/dist/PromptTemplateManager-6HH3PVXV.js +0 -9
  127. package/dist/SettingsMigrationManager-TJ7UWZG5.js +0 -10
  128. package/dist/chunk-2CXREBLZ.js.map +0 -1
  129. package/dist/chunk-2IJEMXOB.js.map +0 -1
  130. package/dist/chunk-2MAIX45J.js.map +0 -1
  131. package/dist/chunk-5Q3NDNNV.js.map +0 -1
  132. package/dist/chunk-5VK4NRSF.js.map +0 -1
  133. package/dist/chunk-CDZERT7Z.js.map +0 -1
  134. package/dist/chunk-CE26YH2U.js.map +0 -1
  135. package/dist/chunk-DLHA5VQ3.js.map +0 -1
  136. package/dist/chunk-GEHQXLEI.js.map +0 -1
  137. package/dist/chunk-IFB4Z76W.js.map +0 -1
  138. package/dist/chunk-M7JJCX53.js.map +0 -1
  139. package/dist/chunk-MKWYLDFK.js.map +0 -1
  140. package/dist/chunk-OXAM2WVC.js.map +0 -1
  141. package/dist/chunk-OYF4VIFI.js.map +0 -1
  142. package/dist/chunk-PGPI5LR4.js.map +0 -1
  143. package/dist/chunk-RIEO2WML.js.map +0 -1
  144. package/dist/chunk-SUOXY5WJ.js.map +0 -1
  145. package/dist/chunk-UJL4HI2R.js.map +0 -1
  146. package/dist/chunk-ZT3YZB4K.js.map +0 -1
  147. package/dist/cleanup-H4VXU3C3.js.map +0 -1
  148. package/dist/ignite-T74RYXCA.js.map +0 -1
  149. package/dist/init-4FHTAM3F.js +0 -19
  150. package/dist/logger-MKYH4UDV.js +0 -12
  151. package/dist/neon-helpers-77PBPGJ5.js +0 -10
  152. package/dist/update-notifier-QEX3CJHA.js.map +0 -1
  153. /package/dist/{BranchNamingService-A77VI6AI.js.map → BranchNamingService-GCCWB3LK.js.map} +0 -0
  154. /package/dist/{ClaudeContextManager-BN7RE5ZQ.js.map → ClaudeContextManager-DK77227F.js.map} +0 -0
  155. /package/dist/{ClaudeService-DLYLJUPA.js.map → ClaudeService-W3SA7HVG.js.map} +0 -0
  156. /package/dist/{GitHubService-FZHHBOFG.js.map → GitHubService-RPM27GWD.js.map} +0 -0
  157. /package/dist/{PromptTemplateManager-6HH3PVXV.js.map → PromptTemplateManager-2TDZAUC6.js.map} +0 -0
  158. /package/dist/{SettingsManager-I2LRCW2A.js.map → SettingsManager-FJFU6JJD.js.map} +0 -0
  159. /package/dist/{SettingsMigrationManager-TJ7UWZG5.js.map → SettingsMigrationManager-EH3J2TCN.js.map} +0 -0
  160. /package/dist/{chunk-UAN4A3YU.js.map → chunk-G6CIIJLT.js.map} +0 -0
  161. /package/dist/{chunk-4XIDC3NF.js.map → chunk-MD6HA5IK.js.map} +0 -0
  162. /package/dist/{chunk-OC4H6HJD.js.map → chunk-O7WHXLCB.js.map} +0 -0
  163. /package/dist/{chunk-PA6Q6AWM.js.map → chunk-PSFVTBM7.js.map} +0 -0
  164. /package/dist/{chunk-ZM3CFL5L.js.map → chunk-QRBOPFAA.js.map} +0 -0
  165. /package/dist/{chunk-SSCQCCJ7.js.map → chunk-THF25ICZ.js.map} +0 -0
  166. /package/dist/{chunk-AKUJXDNW.js.map → chunk-UPUAQYAW.js.map} +0 -0
  167. /package/dist/{chunk-OSCLCMDG.js.map → chunk-UYWAESOT.js.map} +0 -0
  168. /package/dist/{chunk-RW54ZMBM.js.map → chunk-VAYGNQTE.js.map} +0 -0
  169. /package/dist/{chunk-CFFQ2Z7A.js.map → chunk-WUQQNE63.js.map} +0 -0
  170. /package/dist/{claude-W52VKI6L.js.map → claude-ACVXNB6N.js.map} +0 -0
  171. /package/dist/{color-F7RU6B6Z.js.map → color-ZPIIUADB.js.map} +0 -0
  172. /package/dist/{git-IYA53VIC.js.map → git-4BVOOOOV.js.map} +0 -0
  173. /package/dist/{init-4FHTAM3F.js.map → init-LBA6NUK2.js.map} +0 -0
  174. /package/dist/{installation-detector-VARGFFRZ.js.map → installation-detector-6R6YOFVZ.js.map} +0 -0
  175. /package/dist/{logger-MKYH4UDV.js.map → neon-helpers-L5CXQ5CT.js.map} +0 -0
  176. /package/dist/{neon-helpers-77PBPGJ5.js.map → prompt-A7GGRHSY.js.map} +0 -0
  177. /package/dist/{prompt-QALMYTVC.js.map → remote-73TZ2ADI.js.map} +0 -0
  178. /package/dist/{test-tabs-PRMRSHKI.js.map → test-tabs-3SCJWRKT.js.map} +0 -0
  179. /package/dist/{test-webserver-DAHONWCS.js.map → test-webserver-VPNLAFZ3.js.map} +0 -0
  180. /package/dist/{update-4TDDUR5K.js.map → update-LETF5ASC.js.map} +0 -0
  181. /package/dist/{remote-VUNCQZ6J.js.map → update-notifier-H55ZK7NU.js.map} +0 -0
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-env node */
3
+ /* eslint-disable no-undef */
4
+ /**
5
+ * iloom-hook.js - Claude Code hook script for iloom-vscode integration
6
+ *
7
+ * This script is called by Claude Code on various events and broadcasts
8
+ * relevant session state changes to all iloom-vscode extension instances
9
+ * via Unix sockets.
10
+ *
11
+ * Events we handle:
12
+ * - Stop → waiting_for_input (Claude finished turn)
13
+ * - PermissionRequest → waiting_for_approval (needs permission)
14
+ * - PreToolUse → working (tool about to execute)
15
+ * - PostToolUse → working (tool finished, clears approval)
16
+ * - SessionEnd → ended (clear notifications)
17
+ * - Notification(idle_prompt) → idle_reminder (60s reminder)
18
+ * - Notification(elicitation_dialog) → tool_input_needed (MCP tool question)
19
+ *
20
+ * Events we skip (exit without broadcasting):
21
+ * - SessionStart - user just launched, they know
22
+ * - SubagentStop - subagent done but main agent may continue
23
+ * - Notification(permission_prompt) - redundant with PermissionRequest
24
+ * - Notification(auth_success) - user just logged in
25
+ * - Any other notification types
26
+ *
27
+ * This is purely a notification mechanism - it does NOT participate in
28
+ * permission approval/denial. Claude Code handles permission prompts in
29
+ * the terminal as normal.
30
+ *
31
+ * Debug logging: Set ILOOM_HOOK_DEBUG=1 to enable logging to /tmp/iloom-hook.log
32
+ */
33
+
34
+ const fs = require('fs');
35
+ const net = require('net');
36
+ const path = require('path');
37
+
38
+ // Debug logging - writes to /tmp/iloom-hook.log
39
+ // Set ILOOM_HOOK_DEBUG=0 to disable (enabled by default for now)
40
+ const DEBUG = process.env.ILOOM_HOOK_DEBUG !== '0';
41
+ const LOG_FILE = '/tmp/iloom-hook.log';
42
+
43
+ function debug(message, data = {}) {
44
+ if (!DEBUG) return;
45
+
46
+ const timestamp = new Date().toISOString();
47
+ const dataStr = Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : '';
48
+ const logLine = `[${timestamp}] ${message}${dataStr}\n`;
49
+
50
+ try {
51
+ fs.appendFileSync(LOG_FILE, logLine);
52
+ } catch {
53
+ // Ignore logging errors
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Read JSON from stdin until EOF
59
+ * @returns {Promise<object>} Parsed JSON data from Claude Code
60
+ */
61
+ async function readStdin() {
62
+ return new Promise((resolve, reject) => {
63
+ let data = '';
64
+
65
+ process.stdin.setEncoding('utf8');
66
+ process.stdin.on('data', (chunk) => {
67
+ data += chunk;
68
+ });
69
+
70
+ process.stdin.on('end', () => {
71
+ try {
72
+ const parsed = JSON.parse(data);
73
+ resolve(parsed);
74
+ } catch (error) {
75
+ reject(new Error(`Failed to parse stdin JSON: ${error.message}`));
76
+ }
77
+ });
78
+
79
+ process.stdin.on('error', reject);
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Find all iloom sockets in /tmp
85
+ *
86
+ * @returns {string[]} Array of socket paths
87
+ */
88
+ function findAllIloomSockets() {
89
+ try {
90
+ const tmpDir = '/tmp';
91
+ const files = fs.readdirSync(tmpDir);
92
+ const sockets = files
93
+ .filter(file => file.startsWith('iloom-') && file.endsWith('.sock'))
94
+ .map(file => path.join(tmpDir, file))
95
+ .filter(socketPath => {
96
+ // Verify it's actually a socket
97
+ try {
98
+ const stat = fs.statSync(socketPath);
99
+ return stat.isSocket();
100
+ } catch {
101
+ return false;
102
+ }
103
+ });
104
+
105
+ return sockets;
106
+ } catch (error) {
107
+ debug('Error finding iloom sockets', { error: error.message });
108
+ return [];
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Map hook event name to session status
114
+ *
115
+ * @param {string} eventName - The hook_event_name from Claude Code
116
+ * @param {string|undefined} notificationType - notification_type for Notification events
117
+ * @returns {string|null} Status string for iloom-vscode, or null if event should be skipped
118
+ */
119
+ function mapEventToStatus(eventName, notificationType) {
120
+ switch (eventName) {
121
+ case 'Stop':
122
+ return 'waiting_for_input';
123
+
124
+ case 'PermissionRequest':
125
+ return 'waiting_for_approval';
126
+
127
+ case 'PreToolUse':
128
+ case 'PostToolUse':
129
+ return 'working';
130
+
131
+ case 'SessionEnd':
132
+ return 'ended';
133
+
134
+ case 'Notification':
135
+ if (notificationType === 'idle_prompt') {
136
+ return 'idle_reminder';
137
+ }
138
+ if (notificationType === 'elicitation_dialog') {
139
+ return 'tool_input_needed';
140
+ }
141
+ // Other notification types - not relevant, skip
142
+ return null;
143
+
144
+ default:
145
+ // Other events (SessionStart, SubagentStop, etc.) - not relevant, skip
146
+ return null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Send status to a single socket (fire and forget)
152
+ *
153
+ * @param {string} socketPath - Path to Unix socket
154
+ * @param {string} status - Session status
155
+ * @param {object} hookData - Full hook data from Claude Code
156
+ * @returns {Promise<void>}
157
+ */
158
+ async function sendStatus(socketPath, status, hookData) {
159
+ return new Promise((resolve) => {
160
+ const client = net.createConnection(socketPath, () => {
161
+ const message = JSON.stringify({
162
+ type: 'session_status',
163
+ status,
164
+ session_id: hookData.session_id,
165
+ hook_event_name: hookData.hook_event_name,
166
+ cwd: hookData.cwd,
167
+ tool_name: hookData.tool_name,
168
+ tool_input: hookData.tool_input,
169
+ notification_type: hookData.notification_type,
170
+ timestamp: new Date().toISOString()
171
+ });
172
+
173
+ client.write(message + '\n');
174
+ // Fire and forget - close connection immediately after sending
175
+ client.end();
176
+ resolve();
177
+ });
178
+
179
+ // Handle connection errors silently
180
+ client.on('error', () => {
181
+ resolve();
182
+ });
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Broadcast status to all iloom sockets (fire and forget)
188
+ * Each VSCode instance can filter messages by cwd if needed
189
+ *
190
+ * @param {string[]} socketPaths - Array of socket paths to broadcast to
191
+ * @param {string} status - Session status
192
+ * @param {object} hookData - Full hook data from Claude Code
193
+ */
194
+ async function broadcastStatus(socketPaths, status, hookData) {
195
+ debug('Broadcasting to all sockets', { count: socketPaths.length, socketPaths });
196
+
197
+ const promises = socketPaths.map(socketPath => {
198
+ return sendStatus(socketPath, status, hookData).catch(error => {
199
+ debug('Broadcast failed for socket', { socketPath, error: error.message });
200
+ return null;
201
+ });
202
+ });
203
+
204
+ // Wait for all broadcasts to complete (or fail)
205
+ await Promise.allSettled(promises);
206
+ debug('Broadcast completed');
207
+ }
208
+
209
+ /**
210
+ * Main entry point
211
+ */
212
+ async function main() {
213
+ debug('Hook invoked', { ILOOM: process.env.ILOOM, ILOOM_HOOK_DEBUG: process.env.ILOOM_HOOK_DEBUG });
214
+
215
+ // Only run when launched via iloom (ILOOM=1)
216
+ // This prevents the hook from doing anything when Claude is run directly
217
+ if (process.env.ILOOM !== '1') {
218
+ debug('ILOOM not set, exiting silently');
219
+ process.exit(0);
220
+ }
221
+
222
+ try {
223
+ const hookData = await readStdin();
224
+ const { hook_event_name, cwd, notification_type, session_id } = hookData;
225
+
226
+ debug('Received hook event', { hook_event_name, cwd, notification_type, session_id, tool_name: hookData.tool_name });
227
+
228
+ const status = mapEventToStatus(hook_event_name, notification_type);
229
+ debug('Mapped event to status', { status });
230
+
231
+ // Skip events we don't care about
232
+ if (status === null) {
233
+ debug('Event not relevant, skipping');
234
+ process.exit(0);
235
+ }
236
+
237
+ // Find all iloom sockets for broadcasting
238
+ const allSockets = findAllIloomSockets();
239
+ debug('Found iloom sockets', { count: allSockets.length, sockets: allSockets });
240
+
241
+ // If no sockets exist, exit silently (no VSCode extensions running)
242
+ if (allSockets.length === 0) {
243
+ debug('No iloom sockets found, exiting');
244
+ process.exit(0);
245
+ }
246
+
247
+ // Broadcast status to all sockets (fire and forget)
248
+ // All events including PermissionRequest are just notifications
249
+ await broadcastStatus(allSockets, status, hookData);
250
+
251
+ debug('Hook completed successfully');
252
+ } catch (error) {
253
+ debug('Hook error', { error: error.message, stack: error.stack });
254
+ // Silent failure - don't interrupt Claude
255
+ }
256
+ }
257
+
258
+ main().catch(() => process.exit(0));
@@ -3,59 +3,261 @@ import {
3
3
  FirstRunManager,
4
4
  IssueTrackerFactory,
5
5
  generateIssueManagementMcpConfig
6
- } from "./chunk-DLHA5VQ3.js";
6
+ } from "./chunk-HD5SUKI2.js";
7
+ import "./chunk-QHA67Q7A.js";
7
8
  import {
8
9
  AgentManager
9
- } from "./chunk-OC4H6HJD.js";
10
+ } from "./chunk-O7WHXLCB.js";
10
11
  import {
11
12
  GitWorktreeManager
12
- } from "./chunk-2CXREBLZ.js";
13
- import {
14
- PromptTemplateManager
15
- } from "./chunk-RIEO2WML.js";
13
+ } from "./chunk-JC5HXN75.js";
16
14
  import {
17
15
  extractSettingsOverrides
18
16
  } from "./chunk-GYCR2LOU.js";
19
17
  import {
20
18
  extractIssueNumber
21
- } from "./chunk-5Q3NDNNV.js";
19
+ } from "./chunk-2W2FBL5G.js";
20
+ import {
21
+ MetadataManager
22
+ } from "./chunk-IJ7IGJT3.js";
22
23
  import {
23
24
  SettingsManager
24
- } from "./chunk-CDZERT7Z.js";
25
- import "./chunk-M7JJCX53.js";
26
- import "./chunk-3RUPPQRG.js";
27
- import "./chunk-CE26YH2U.js";
25
+ } from "./chunk-VWNS6DH5.js";
26
+ import "./chunk-OEGECBFS.js";
27
+ import "./chunk-KO2FOMHL.js";
28
+ import "./chunk-SJ2GZ6RF.js";
28
29
  import {
30
+ generateDeterministicSessionId,
29
31
  launchClaude
30
- } from "./chunk-OYF4VIFI.js";
31
- import "./chunk-RW54ZMBM.js";
32
- import "./chunk-UJL4HI2R.js";
32
+ } from "./chunk-RUC7OULH.js";
33
+ import "./chunk-VAYGNQTE.js";
34
+ import "./chunk-Z5NXYJIG.js";
35
+ import "./chunk-6UIGZD2N.js";
36
+ import {
37
+ PromptTemplateManager
38
+ } from "./chunk-74VMN2KC.js";
33
39
  import {
34
40
  logger
35
- } from "./chunk-GEHQXLEI.js";
41
+ } from "./chunk-UYVWLISQ.js";
36
42
 
37
43
  // src/commands/ignite.ts
38
- import path from "path";
44
+ import path2 from "path";
39
45
  import { readFile } from "fs/promises";
46
+
47
+ // src/lib/ClaudeHookManager.ts
48
+ import os from "os";
49
+ import path from "path";
50
+ import fs from "fs-extra";
51
+ import { parse, modify, applyEdits } from "jsonc-parser";
52
+ import { fileURLToPath } from "url";
53
+ import { accessSync } from "fs";
54
+ var ClaudeHookManager = class {
55
+ constructor() {
56
+ this.claudeDir = path.join(os.homedir(), ".claude");
57
+ this.hooksDir = path.join(this.claudeDir, "hooks");
58
+ this.settingsPath = path.join(this.claudeDir, "settings.json");
59
+ const currentFileUrl = import.meta.url;
60
+ const currentFilePath = fileURLToPath(currentFileUrl);
61
+ const distDir = path.dirname(currentFilePath);
62
+ let templateDir = path.join(distDir, "hooks");
63
+ let currentDir = distDir;
64
+ while (currentDir !== path.dirname(currentDir)) {
65
+ const candidatePath = path.join(currentDir, "hooks");
66
+ try {
67
+ accessSync(candidatePath);
68
+ templateDir = candidatePath;
69
+ break;
70
+ } catch {
71
+ currentDir = path.dirname(currentDir);
72
+ }
73
+ }
74
+ this.templateDir = templateDir;
75
+ logger.debug("ClaudeHookManager initialized", {
76
+ claudeDir: this.claudeDir,
77
+ hooksDir: this.hooksDir,
78
+ settingsPath: this.settingsPath,
79
+ templateDir: this.templateDir
80
+ });
81
+ }
82
+ /**
83
+ * Install Claude hooks for VSCode integration
84
+ *
85
+ * This is idempotent - safe to call on every spin.
86
+ * Installs hook script to ~/.claude/hooks/ and merges
87
+ * hook configuration into ~/.claude/settings.json
88
+ */
89
+ async installHooks() {
90
+ try {
91
+ await fs.ensureDir(this.hooksDir);
92
+ await this.installHookScript();
93
+ await this.mergeHookConfig();
94
+ logger.debug("Claude hooks installed successfully");
95
+ } catch (error) {
96
+ logger.warn(
97
+ `Failed to install Claude hooks: ${error instanceof Error ? error.message : "Unknown error"}`
98
+ );
99
+ }
100
+ }
101
+ /**
102
+ * Check if hooks are already installed
103
+ */
104
+ async isHooksInstalled() {
105
+ try {
106
+ const hookScriptPath = path.join(this.hooksDir, "iloom-hook.js");
107
+ if (!await fs.pathExists(hookScriptPath)) {
108
+ return false;
109
+ }
110
+ if (!await fs.pathExists(this.settingsPath)) {
111
+ return false;
112
+ }
113
+ const content = await fs.readFile(this.settingsPath, "utf8");
114
+ const errors = [];
115
+ const settings = parse(content, errors, { allowTrailingComma: true });
116
+ if (errors.length > 0 || !(settings == null ? void 0 : settings.hooks)) {
117
+ return false;
118
+ }
119
+ return Array.isArray(settings.hooks.SessionStart);
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+ /**
125
+ * Install the hook script from bundled templates
126
+ * Skips write if destination already has identical content
127
+ */
128
+ async installHookScript() {
129
+ const sourcePath = path.join(this.templateDir, "iloom-hook.js");
130
+ const destPath = path.join(this.hooksDir, "iloom-hook.js");
131
+ if (!await fs.pathExists(sourcePath)) {
132
+ throw new Error(`Hook template not found at ${sourcePath}`);
133
+ }
134
+ if (await fs.pathExists(destPath)) {
135
+ const [sourceContent, destContent] = await Promise.all([
136
+ fs.readFile(sourcePath, "utf8"),
137
+ fs.readFile(destPath, "utf8")
138
+ ]);
139
+ if (sourceContent === destContent) {
140
+ logger.debug("Hook script already up to date, skipping");
141
+ return;
142
+ }
143
+ }
144
+ await fs.copyFile(sourcePath, destPath);
145
+ logger.debug("Hook script installed", { sourcePath, destPath });
146
+ }
147
+ /**
148
+ * Merge hook configuration into settings.json
149
+ * Preserves existing user hooks and comments
150
+ */
151
+ async mergeHookConfig() {
152
+ var _a, _b, _c;
153
+ await fs.ensureDir(this.claudeDir);
154
+ let existingContent = "{}";
155
+ let existingSettings = {};
156
+ if (await fs.pathExists(this.settingsPath)) {
157
+ existingContent = await fs.readFile(this.settingsPath, "utf8");
158
+ const errors = [];
159
+ existingSettings = parse(existingContent, errors, { allowTrailingComma: true });
160
+ if (errors.length > 0) {
161
+ logger.warn("Existing settings.json has parse errors, will attempt to merge anyway");
162
+ }
163
+ }
164
+ const ourHooks = this.getHookConfig();
165
+ const mergedHooks = { ...existingSettings.hooks ?? {} };
166
+ let hooksAdded = false;
167
+ for (const [eventName, eventConfigs] of Object.entries(ourHooks)) {
168
+ const existing = mergedHooks[eventName] ?? [];
169
+ const ourCommand = (_c = (_b = (_a = eventConfigs[0]) == null ? void 0 : _a.hooks) == null ? void 0 : _b[0]) == null ? void 0 : _c.command;
170
+ const alreadyRegistered = existing.some(
171
+ (config) => {
172
+ var _a2;
173
+ return (_a2 = config.hooks) == null ? void 0 : _a2.some((h) => h.command === ourCommand);
174
+ }
175
+ );
176
+ if (!alreadyRegistered) {
177
+ mergedHooks[eventName] = [...existing, ...eventConfigs];
178
+ hooksAdded = true;
179
+ }
180
+ }
181
+ if (!hooksAdded) {
182
+ logger.debug("All hooks already registered, skipping settings.json update");
183
+ return;
184
+ }
185
+ let content;
186
+ if (existingContent.includes("//") || existingContent.includes("/*")) {
187
+ let modifiedContent = existingContent;
188
+ const edits = modify(modifiedContent, ["hooks"], mergedHooks, {});
189
+ content = applyEdits(modifiedContent, edits);
190
+ } else {
191
+ const updatedSettings = {
192
+ ...existingSettings,
193
+ hooks: mergedHooks
194
+ };
195
+ content = JSON.stringify(updatedSettings, null, 2) + "\n";
196
+ }
197
+ const tempPath = `${this.settingsPath}.tmp`;
198
+ await fs.writeFile(tempPath, content, "utf8");
199
+ await fs.rename(tempPath, this.settingsPath);
200
+ logger.debug("Hook configuration merged into settings.json");
201
+ }
202
+ /**
203
+ * Get the hook configuration to register
204
+ *
205
+ * Each event maps to a hook that runs iloom-hook.js
206
+ */
207
+ getHookConfig() {
208
+ const hookCommand = `node ${path.join(this.hooksDir, "iloom-hook.js")}`;
209
+ return {
210
+ Notification: [
211
+ { hooks: [{ type: "command", command: hookCommand }] }
212
+ ],
213
+ Stop: [
214
+ { hooks: [{ type: "command", command: hookCommand }] }
215
+ ],
216
+ SubagentStop: [
217
+ { hooks: [{ type: "command", command: hookCommand }] }
218
+ ],
219
+ PermissionRequest: [
220
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand, timeout: 86400 }] }
221
+ ],
222
+ PreToolUse: [
223
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand }] }
224
+ ],
225
+ PostToolUse: [
226
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand }] }
227
+ ],
228
+ SessionStart: [
229
+ { hooks: [{ type: "command", command: hookCommand }] }
230
+ ],
231
+ SessionEnd: [
232
+ { hooks: [{ type: "command", command: hookCommand }] }
233
+ ]
234
+ };
235
+ }
236
+ };
237
+
238
+ // src/commands/ignite.ts
40
239
  var IgniteCommand = class {
41
- constructor(templateManager, gitWorktreeManager, agentManager, settingsManager, firstRunManager) {
240
+ constructor(templateManager, gitWorktreeManager, agentManager, settingsManager, firstRunManager, hookManager) {
42
241
  this.templateManager = templateManager ?? new PromptTemplateManager();
43
242
  this.gitWorktreeManager = gitWorktreeManager ?? new GitWorktreeManager();
44
243
  this.agentManager = agentManager ?? new AgentManager();
45
244
  this.settingsManager = settingsManager ?? new SettingsManager();
46
245
  this.firstRunManager = firstRunManager ?? new FirstRunManager("spin");
246
+ this.hookManager = hookManager ?? new ClaudeHookManager();
47
247
  }
48
248
  /**
49
249
  * Main entry point for spin command
50
250
  */
51
251
  async execute(oneShot = "default") {
52
252
  var _a;
253
+ process.env.ILOOM = "1";
53
254
  try {
54
255
  logger.info("\u{1F680} Your loom is spinning up, please wait...");
55
256
  const isFirstRun = await this.firstRunManager.isFirstRun();
56
257
  if (isFirstRun) {
57
258
  logger.success("Welcome to iloom! Preparing first-time experience...");
58
259
  }
260
+ await this.hookManager.installHooks();
59
261
  const context = await this.detectWorkspaceContext();
60
262
  logger.debug("Auto-detected workspace context", { context });
61
263
  this.logDetectedContext(context);
@@ -72,7 +274,7 @@ var IgniteCommand = class {
72
274
  const cliOverrides = extractSettingsOverrides();
73
275
  this.settings = await this.settingsManager.loadSettings(void 0, cliOverrides);
74
276
  }
75
- const model = this.getModelForWorkflow(context.type);
277
+ const model = this.settingsManager.getSpinModel(this.settings);
76
278
  let permissionMode = this.getPermissionModeForWorkflow(context.type);
77
279
  if (oneShot === "bypassPermissions") {
78
280
  permissionMode = "bypassPermissions";
@@ -82,10 +284,22 @@ var IgniteCommand = class {
82
284
  "\u26A0\uFE0F WARNING: Using bypassPermissions mode - Claude will execute all tool calls without confirmation. This can be dangerous. Use with caution."
83
285
  );
84
286
  }
287
+ let sessionId;
288
+ const metadataManager = new MetadataManager();
289
+ const metadata = await metadataManager.readMetadata(context.workspacePath);
290
+ if (metadata == null ? void 0 : metadata.sessionId) {
291
+ sessionId = metadata.sessionId;
292
+ logger.debug("Using session ID from metadata", { sessionId });
293
+ } else {
294
+ sessionId = generateDeterministicSessionId(context.workspacePath);
295
+ logger.debug("Generated session ID (no metadata found)", { sessionId, workspacePath: context.workspacePath });
296
+ }
85
297
  const claudeOptions = {
86
298
  headless: false,
87
299
  // Enable stdio: 'inherit' for current terminal
88
- addDir: context.workspacePath
300
+ addDir: context.workspacePath,
301
+ sessionId
302
+ // Enable Claude Code session resume
89
303
  };
90
304
  if (model !== void 0) {
91
305
  claudeOptions.model = model;
@@ -200,19 +414,11 @@ var IgniteCommand = class {
200
414
  }
201
415
  if (oneShot === "noReview" || oneShot === "bypassPermissions") {
202
416
  variables.ONE_SHOT_MODE = true;
417
+ } else {
418
+ variables.INTERACTIVE_MODE = true;
203
419
  }
204
420
  return variables;
205
421
  }
206
- /**
207
- * Get the appropriate model for a workflow type
208
- * Same logic as ClaudeService.getModelForWorkflow()
209
- */
210
- getModelForWorkflow(type) {
211
- if (type === "issue") {
212
- return "claude-sonnet-4-20250514";
213
- }
214
- return void 0;
215
- }
216
422
  /**
217
423
  * Get the appropriate permission mode for a workflow type
218
424
  * Same logic as ClaudeService.getPermissionModeForWorkflow()
@@ -242,7 +448,7 @@ var IgniteCommand = class {
242
448
  */
243
449
  async detectWorkspaceContext() {
244
450
  const workspacePath = process.cwd();
245
- const currentDir = path.basename(workspacePath);
451
+ const currentDir = path2.basename(workspacePath);
246
452
  const prPattern = /_pr_(\d+)$/;
247
453
  const prMatch = currentDir.match(prPattern);
248
454
  if (prMatch == null ? void 0 : prMatch[1]) {
@@ -380,15 +586,15 @@ var IgniteCommand = class {
380
586
  */
381
587
  async loadReadmeContent() {
382
588
  try {
383
- let currentDir = path.dirname(new URL(import.meta.url).pathname);
384
- while (currentDir !== path.dirname(currentDir)) {
385
- const readmePath = path.join(currentDir, "README.md");
589
+ let currentDir = path2.dirname(new URL(import.meta.url).pathname);
590
+ while (currentDir !== path2.dirname(currentDir)) {
591
+ const readmePath = path2.join(currentDir, "README.md");
386
592
  try {
387
593
  const content = await readFile(readmePath, "utf-8");
388
594
  logger.debug("Loaded README.md for first-time user", { readmePath });
389
595
  return content;
390
596
  } catch {
391
- currentDir = path.dirname(currentDir);
597
+ currentDir = path2.dirname(currentDir);
392
598
  }
393
599
  }
394
600
  logger.debug("README.md not found, returning empty string");
@@ -404,15 +610,15 @@ var IgniteCommand = class {
404
610
  */
405
611
  async loadSettingsSchemaContent() {
406
612
  try {
407
- let currentDir = path.dirname(new URL(import.meta.url).pathname);
408
- while (currentDir !== path.dirname(currentDir)) {
409
- const schemaPath = path.join(currentDir, ".iloom", "README.md");
613
+ let currentDir = path2.dirname(new URL(import.meta.url).pathname);
614
+ while (currentDir !== path2.dirname(currentDir)) {
615
+ const schemaPath = path2.join(currentDir, ".iloom", "README.md");
410
616
  try {
411
617
  const content = await readFile(schemaPath, "utf-8");
412
618
  logger.debug("Loaded .iloom/README.md for first-time user", { schemaPath });
413
619
  return content;
414
620
  } catch {
415
- currentDir = path.dirname(currentDir);
621
+ currentDir = path2.dirname(currentDir);
416
622
  }
417
623
  }
418
624
  logger.debug(".iloom/README.md not found, returning empty string");
@@ -426,4 +632,4 @@ var IgniteCommand = class {
426
632
  export {
427
633
  IgniteCommand
428
634
  };
429
- //# sourceMappingURL=ignite-T74RYXCA.js.map
635
+ //# sourceMappingURL=ignite-3B264M7K.js.map