zenflo 0.11.5 → 0.11.7

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.
@@ -1,9 +1,21 @@
1
+ #!/usr/bin/env node
2
+
1
3
  const crypto = require('crypto');
2
4
  const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync, spawn } = require('child_process');
3
7
 
4
8
  // Disable autoupdater (never works really)
5
9
  process.env.DISABLE_AUTOUPDATER = '1';
6
10
 
11
+ // Debug helper - only output when DEBUG=1
12
+ const DEBUG = process.env.DEBUG === '1' || process.env.DEBUG === 'true';
13
+ function debug(...args) {
14
+ if (DEBUG) {
15
+ console.error(...args);
16
+ }
17
+ }
18
+
7
19
  // Helper to write JSON messages to fd 3
8
20
  function writeMessage(message) {
9
21
  try {
@@ -13,6 +25,145 @@ function writeMessage(message) {
13
25
  }
14
26
  }
15
27
 
28
+ // Check if we're being called from zenflo (has fd 3 open) or directly from Claude Code extension
29
+ let isCalledFromZenflo = false;
30
+ try {
31
+ // Try to write to fd 3 - if it exists, we're being called from zenflo
32
+ fs.writeSync(3, '');
33
+ isCalledFromZenflo = true;
34
+ } catch (err) {
35
+ // fd 3 doesn't exist, we're being called directly from Claude Code extension
36
+ isCalledFromZenflo = false;
37
+ }
38
+
39
+ // Track session IDs to prevent duplicate notifications
40
+ const capturedSessionIds = new Set();
41
+ let daemonNotified = false;
42
+ let sessionWatcher = null;
43
+
44
+ // Helper to get project path (same logic as zenflo)
45
+ function getProjectPath(workingDirectory) {
46
+ const { join, resolve } = require('path');
47
+ const { homedir } = require('os');
48
+
49
+ // Resolve and convert to a filesystem-safe path (replace /, \, ., : with -)
50
+ const projectId = resolve(workingDirectory).replace(/[\\\/\.:]/g, '-');
51
+
52
+ // Get Claude config directory
53
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
54
+
55
+ // Return project directory path
56
+ return join(claudeConfigDir, 'projects', projectId);
57
+ }
58
+
59
+ // Helper to notify daemon about session (only once per session)
60
+ async function notifyDaemon(sessionId) {
61
+ if (daemonNotified || capturedSessionIds.has(sessionId)) {
62
+ return;
63
+ }
64
+ capturedSessionIds.add(sessionId);
65
+
66
+ try {
67
+ const { readFileSync } = require('fs');
68
+ const { homedir } = require('os');
69
+
70
+ // Read daemon state to get HTTP port
71
+ const daemonStatePath = path.join(
72
+ process.env.ZENFLO_HOME_DIR || path.join(homedir(), '.happy'),
73
+ 'daemon.state.json'
74
+ );
75
+
76
+ let daemonState;
77
+ try {
78
+ daemonState = JSON.parse(readFileSync(daemonStatePath, 'utf8'));
79
+ } catch (err) {
80
+ // Daemon not running or state file doesn't exist
81
+ return;
82
+ }
83
+
84
+ if (!daemonState.httpPort) {
85
+ return;
86
+ }
87
+
88
+ // Notify daemon about the session
89
+ const metadata = {
90
+ path: process.cwd(),
91
+ host: require('os').hostname(),
92
+ hostPid: process.pid,
93
+ startedBy: 'claude-code-extension',
94
+ flavor: 'claude'
95
+ };
96
+
97
+ const response = await fetch(`http://127.0.0.1:${daemonState.httpPort}/session-started`, {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify({ sessionId, metadata })
101
+ });
102
+
103
+ if (response.ok) {
104
+ daemonNotified = true;
105
+ }
106
+ } catch (err) {
107
+ // Ignore errors - daemon might not be ready or not running
108
+ }
109
+ }
110
+
111
+ // Helper to handle detected session
112
+ function handleSessionFile(sessionId) {
113
+ if (capturedSessionIds.has(sessionId)) {
114
+ return; // Already processed
115
+ }
116
+ capturedSessionIds.add(sessionId);
117
+
118
+ // Emit UUID message on fd 3 for session detection (when called from zenflo)
119
+ if (isCalledFromZenflo) {
120
+ writeMessage({ type: 'uuid', value: sessionId });
121
+ }
122
+
123
+ // Notify daemon (when called directly)
124
+ if (!isCalledFromZenflo) {
125
+ notifyDaemon(sessionId).catch(() => {});
126
+ }
127
+ }
128
+
129
+ // Helper to start session file watcher
130
+ function startSessionWatcher() {
131
+ const { watch, mkdirSync } = require('fs');
132
+
133
+ try {
134
+ const cwd = process.cwd();
135
+ const projectDir = getProjectPath(cwd);
136
+ mkdirSync(projectDir, { recursive: true});
137
+
138
+ debug('[launcher] Working directory:', cwd);
139
+ debug('[launcher] Watching for sessions in:', projectDir);
140
+ debug('[launcher] Called from zenflo:', isCalledFromZenflo);
141
+
142
+ // Watch for MODIFIED session files only (not existing ones)
143
+ // This ensures we only track sessions that are actively being used
144
+ sessionWatcher = watch(projectDir, (eventType, filename) => {
145
+ if (typeof filename === 'string' && filename.toLowerCase().endsWith('.jsonl')) {
146
+ const sessionId = filename.replace('.jsonl', '');
147
+
148
+ // Only handle 'change' events (file modifications), not 'rename' (file creation)
149
+ // This prevents tracking empty session files that are created but never used
150
+ if (eventType === 'change') {
151
+ debug('[launcher] Active session detected:', sessionId);
152
+ handleSessionFile(sessionId);
153
+ }
154
+ }
155
+ });
156
+ } catch (err) {
157
+ console.error('[launcher] Error starting session watcher:', err.message);
158
+ }
159
+ }
160
+
161
+ // Start watching for session files
162
+ // - When called directly (extension): notify daemon
163
+ // - When called from zenflo with native binary: emit UUID messages for session detection
164
+ // - When called from zenflo with JS module: UUID messages come from crypto interception
165
+ startSessionWatcher();
166
+
16
167
  // Intercept crypto.randomUUID
17
168
  const originalRandomUUID = crypto.randomUUID;
18
169
  Object.defineProperty(global, 'crypto', {
@@ -23,6 +174,7 @@ Object.defineProperty(global, 'crypto', {
23
174
  randomUUID: () => {
24
175
  const uuid = originalRandomUUID();
25
176
  writeMessage({ type: 'uuid', value: uuid });
177
+ // Don't notify daemon from UUID interceptor - let file watcher handle it
26
178
  return uuid;
27
179
  }
28
180
  };
@@ -35,6 +187,7 @@ Object.defineProperty(crypto, 'randomUUID', {
35
187
  return () => {
36
188
  const uuid = originalRandomUUID();
37
189
  writeMessage({ type: 'uuid', value: uuid });
190
+ // Don't notify daemon from UUID interceptor - let file watcher handle it
38
191
  return uuid;
39
192
  }
40
193
  }
@@ -51,15 +204,15 @@ global.fetch = function(...args) {
51
204
 
52
205
  // Parse URL for privacy
53
206
  let hostname = '';
54
- let path = '';
207
+ let pathname = '';
55
208
  try {
56
209
  const urlObj = new URL(url, 'http://localhost');
57
210
  hostname = urlObj.hostname;
58
- path = urlObj.pathname;
211
+ pathname = urlObj.pathname;
59
212
  } catch (e) {
60
213
  // If URL parsing fails, use defaults
61
214
  hostname = 'unknown';
62
- path = url;
215
+ pathname = url;
63
216
  }
64
217
 
65
218
  // Send fetch start event
@@ -67,7 +220,7 @@ global.fetch = function(...args) {
67
220
  type: 'fetch-start',
68
221
  id,
69
222
  hostname,
70
- path,
223
+ path: pathname,
71
224
  method,
72
225
  timestamp: Date.now()
73
226
  });
@@ -95,4 +248,108 @@ global.fetch = function(...args) {
95
248
  Object.defineProperty(global.fetch, 'name', { value: 'fetch' });
96
249
  Object.defineProperty(global.fetch, 'length', { value: originalFetch.length });
97
250
 
98
- import('@anthropic-ai/claude-code/cli.js')
251
+ // Get paths
252
+ const scriptDir = __dirname;
253
+ const cliDir = path.resolve(scriptDir, '..');
254
+ const zenfloBin = path.resolve(cliDir, 'bin', 'zenflo.mjs');
255
+ const claudeCodePath = path.resolve(cliDir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
256
+ const claudeCodePathMonorepo = path.resolve(cliDir, '..', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
257
+
258
+ if (!isCalledFromZenflo) {
259
+ // Called directly from Claude Code extension
260
+ // Just ensure daemon is running, then load Claude Code directly
261
+ // The file watcher will detect the session and notify daemon
262
+ try {
263
+ // Try to start zenflo daemon in background if not already running (non-blocking)
264
+ const daemonProcess = spawn(process.execPath, [zenfloBin, 'daemon', 'start'], {
265
+ detached: true,
266
+ stdio: 'ignore'
267
+ });
268
+ daemonProcess.unref();
269
+ // Give daemon a moment to start before we continue
270
+ setTimeout(() => {}, 500);
271
+ } catch (err) {
272
+ // Ignore errors - daemon might already be running
273
+ }
274
+ // Continue to load Claude Code directly - this creates ONE session
275
+ // The file watcher will detect it and notify daemon
276
+ }
277
+
278
+ // We're being called from zenflo (or fallback) - just load Claude Code with intercepts
279
+ // Use require for CommonJS compatibility since this is a .cjs file
280
+ try {
281
+ // Check if native binary path was provided by extension wrapper
282
+ const nativeBinary = process.env.CLAUDE_CODE_NATIVE_BINARY;
283
+
284
+ // Debug logging to stderr (won't interfere with stdio)
285
+ if (nativeBinary) {
286
+ debug(`[launcher] Native binary path provided: ${nativeBinary}`);
287
+ debug(`[launcher] File exists: ${fs.existsSync(nativeBinary)}`);
288
+ } else {
289
+ debug('[launcher] No CLAUDE_CODE_NATIVE_BINARY env var');
290
+ }
291
+
292
+ if (nativeBinary && fs.existsSync(nativeBinary)) {
293
+ debug('[launcher] Spawning native binary...');
294
+ debug('[launcher] Working directory:', process.cwd());
295
+ // Native binary is a compiled executable, need to spawn it instead of require
296
+ // Pass through all arguments, stdio, and working directory
297
+ const child = spawn(nativeBinary, process.argv.slice(2), {
298
+ stdio: 'inherit',
299
+ cwd: process.cwd(), // Ensure native binary uses correct working directory
300
+ env: process.env
301
+ });
302
+
303
+ // Forward all termination signals to child
304
+ const killChild = () => {
305
+ if (!child.killed) {
306
+ debug('[launcher] Killing child process...');
307
+ child.kill('SIGTERM');
308
+ // Force kill after 1 second if not dead
309
+ setTimeout(() => {
310
+ if (!child.killed) {
311
+ child.kill('SIGKILL');
312
+ }
313
+ }, 1000);
314
+ }
315
+ };
316
+
317
+ process.on('SIGTERM', killChild);
318
+ process.on('SIGINT', killChild);
319
+ process.on('exit', killChild); // Also kill on normal exit (e.g., abort signal)
320
+
321
+ // Exit with child's exit code
322
+ child.on('exit', (code, signal) => {
323
+ if (signal) {
324
+ process.kill(process.pid, signal);
325
+ } else {
326
+ process.exit(code || 0);
327
+ }
328
+ });
329
+ } else if (fs.existsSync(claudeCodePath)) {
330
+ debug('[launcher] Loading Claude Code from cli/node_modules');
331
+ require(claudeCodePath);
332
+ } else if (fs.existsSync(claudeCodePathMonorepo)) {
333
+ // Try monorepo root node_modules (Yarn workspace hoisting)
334
+ debug('[launcher] Loading Claude Code from monorepo node_modules');
335
+ require(claudeCodePathMonorepo);
336
+ } else {
337
+ // Fallback: try to use the system claude command
338
+ try {
339
+ const claudePath = execSync('which claude', { encoding: 'utf8' }).trim();
340
+ const resolvedPath = fs.realpathSync(claudePath);
341
+ require(resolvedPath);
342
+ } catch (whichErr) {
343
+ console.error('Failed to find Claude Code CLI');
344
+ console.error('Tried local path:', claudeCodePath);
345
+ console.error('Tried monorepo path:', claudeCodePathMonorepo);
346
+ console.error('Tried system claude command (not found)');
347
+ console.error('Make sure @anthropic-ai/claude-code is installed in node_modules or claude is in PATH');
348
+ process.exit(1);
349
+ }
350
+ }
351
+ } catch (err) {
352
+ console.error('Failed to load Claude Code CLI:', err.message);
353
+ console.error('Tried paths:', claudeCodePath, claudeCodePathMonorepo, process.env.CLAUDE_CODE_NATIVE_BINARY);
354
+ process.exit(1);
355
+ }
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+ # Wrapper for Claude Code extension to launch ZenFlo instead of raw Claude
3
+
4
+ # The extension passes the native binary path as first argument
5
+ NATIVE_BINARY="$1"
6
+ shift
7
+
8
+ # Export the native binary path for the launcher to use
9
+ export CLAUDE_CODE_NATIVE_BINARY="$NATIVE_BINARY"
10
+
11
+ # Launch zenflo which creates full backend sessions
12
+ # ZenFlo will spawn Claude Code with proper stdio handling
13
+ exec zenflo "$@"