zenflo 0.11.6 → 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.
@@ -21,7 +21,7 @@ import { platform } from 'os';
21
21
  import { Expo } from 'expo-server-sdk';
22
22
 
23
23
  var name = "zenflo";
24
- var version = "0.11.6";
24
+ var version = "0.11.7";
25
25
  var description = "Mobile and Web client for Claude Code and Codex - ZenFlo edition";
26
26
  var author = "Combined Memory";
27
27
  var license = "MIT";
@@ -172,7 +172,7 @@ class Configuration {
172
172
  webappUrl;
173
173
  isDaemonProcess;
174
174
  // Directories and paths (from persistence)
175
- happyHomeDir;
175
+ zenfloHomeDir;
176
176
  logsDir;
177
177
  settingsFile;
178
178
  privateKeyFile;
@@ -188,20 +188,20 @@ class Configuration {
188
188
  this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
189
189
  if (process.env.ZENFLO_HOME_DIR) {
190
190
  const expandedPath = process.env.ZENFLO_HOME_DIR.replace(/^~/, homedir());
191
- this.happyHomeDir = expandedPath;
191
+ this.zenfloHomeDir = expandedPath;
192
192
  } else {
193
- this.happyHomeDir = join(homedir(), ".happy");
193
+ this.zenfloHomeDir = join(homedir(), ".zenflo");
194
194
  }
195
- this.logsDir = join(this.happyHomeDir, "logs");
196
- this.settingsFile = join(this.happyHomeDir, "settings.json");
197
- this.privateKeyFile = join(this.happyHomeDir, "access.key");
198
- this.daemonStateFile = join(this.happyHomeDir, "daemon.state.json");
199
- this.daemonLockFile = join(this.happyHomeDir, "daemon.state.json.lock");
200
- this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
201
- this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.HAPPY_DISABLE_CAFFEINATE?.toLowerCase() || "");
195
+ this.logsDir = join(this.zenfloHomeDir, "logs");
196
+ this.settingsFile = join(this.zenfloHomeDir, "settings.json");
197
+ this.privateKeyFile = join(this.zenfloHomeDir, "access.key");
198
+ this.daemonStateFile = join(this.zenfloHomeDir, "daemon.state.json");
199
+ this.daemonLockFile = join(this.zenfloHomeDir, "daemon.state.json.lock");
200
+ this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.ZENFLO_EXPERIMENTAL?.toLowerCase() || "");
201
+ this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.ZENFLO_DISABLE_CAFFEINATE?.toLowerCase() || "");
202
202
  this.currentCliVersion = packageJson.version;
203
- if (!existsSync(this.happyHomeDir)) {
204
- mkdirSync(this.happyHomeDir, { recursive: true });
203
+ if (!existsSync(this.zenfloHomeDir)) {
204
+ mkdirSync(this.zenfloHomeDir, { recursive: true });
205
205
  }
206
206
  if (!existsSync(this.logsDir)) {
207
207
  mkdirSync(this.logsDir, { recursive: true });
@@ -361,8 +361,8 @@ async function updateSettings(updater) {
361
361
  try {
362
362
  const current = await readSettings() || { ...defaultSettings };
363
363
  const updated = await updater(current);
364
- if (!existsSync(configuration.happyHomeDir)) {
365
- await mkdir(configuration.happyHomeDir, { recursive: true });
364
+ if (!existsSync(configuration.zenfloHomeDir)) {
365
+ await mkdir(configuration.zenfloHomeDir, { recursive: true });
366
366
  }
367
367
  await writeFile(tmpFile, JSON.stringify(updated, null, 2));
368
368
  await rename(tmpFile, configuration.settingsFile);
@@ -413,8 +413,8 @@ async function readCredentials() {
413
413
  return null;
414
414
  }
415
415
  async function writeCredentialsLegacy(credentials) {
416
- if (!existsSync(configuration.happyHomeDir)) {
417
- await mkdir(configuration.happyHomeDir, { recursive: true });
416
+ if (!existsSync(configuration.zenfloHomeDir)) {
417
+ await mkdir(configuration.zenfloHomeDir, { recursive: true });
418
418
  }
419
419
  await writeFile(configuration.privateKeyFile, JSON.stringify({
420
420
  secret: encodeBase64(credentials.secret),
@@ -422,8 +422,8 @@ async function writeCredentialsLegacy(credentials) {
422
422
  }, null, 2));
423
423
  }
424
424
  async function writeCredentialsDataKey(credentials) {
425
- if (!existsSync(configuration.happyHomeDir)) {
426
- await mkdir(configuration.happyHomeDir, { recursive: true });
425
+ if (!existsSync(configuration.zenfloHomeDir)) {
426
+ await mkdir(configuration.zenfloHomeDir, { recursive: true });
427
427
  }
428
428
  await writeFile(configuration.privateKeyFile, JSON.stringify({
429
429
  encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
@@ -767,10 +767,10 @@ z$1.object({
767
767
  z$1.object({
768
768
  host: z$1.string(),
769
769
  platform: z$1.string(),
770
- happyCliVersion: z$1.string(),
770
+ zenfloCliVersion: z$1.string(),
771
771
  homeDir: z$1.string(),
772
- happyHomeDir: z$1.string(),
773
- happyLibDir: z$1.string()
772
+ zenfloHomeDir: z$1.string(),
773
+ zenfloLibDir: z$1.string()
774
774
  });
775
775
  z$1.object({
776
776
  status: z$1.union([
@@ -1442,9 +1442,11 @@ class ApiSessionClient extends EventEmitter {
1442
1442
  }
1443
1443
  logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
1444
1444
  const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1445
+ const localId = typeof body.uuid === "string" ? body.uuid : null;
1445
1446
  this.socket.emit("message", {
1446
1447
  sid: this.sessionId,
1447
- message: encrypted
1448
+ message: encrypted,
1449
+ localId
1448
1450
  });
1449
1451
  if (body.type === "assistant" && body.message.usage) {
1450
1452
  try {
@@ -2169,6 +2171,14 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
2169
2171
  type: z$1.literal("system"),
2170
2172
  uuid: z$1.string()
2171
2173
  // Used in getMessageKey()
2174
+ }).passthrough(),
2175
+ // Queue operation - internal Claude Code message for managing task queue
2176
+ // These are not sent to backend, just need to be parseable
2177
+ z$1.object({
2178
+ type: z$1.literal("queue-operation"),
2179
+ operation: z$1.enum(["enqueue", "dequeue"]),
2180
+ timestamp: z$1.string(),
2181
+ sessionId: z$1.string()
2172
2182
  }).passthrough()
2173
2183
  ]);
2174
2184
 
@@ -42,7 +42,7 @@ function _interopNamespaceDefault(e) {
42
42
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
43
43
 
44
44
  var name = "zenflo";
45
- var version = "0.11.6";
45
+ var version = "0.11.7";
46
46
  var description = "Mobile and Web client for Claude Code and Codex - ZenFlo edition";
47
47
  var author = "Combined Memory";
48
48
  var license = "MIT";
@@ -193,7 +193,7 @@ class Configuration {
193
193
  webappUrl;
194
194
  isDaemonProcess;
195
195
  // Directories and paths (from persistence)
196
- happyHomeDir;
196
+ zenfloHomeDir;
197
197
  logsDir;
198
198
  settingsFile;
199
199
  privateKeyFile;
@@ -209,20 +209,20 @@ class Configuration {
209
209
  this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
210
210
  if (process.env.ZENFLO_HOME_DIR) {
211
211
  const expandedPath = process.env.ZENFLO_HOME_DIR.replace(/^~/, os.homedir());
212
- this.happyHomeDir = expandedPath;
212
+ this.zenfloHomeDir = expandedPath;
213
213
  } else {
214
- this.happyHomeDir = node_path.join(os.homedir(), ".happy");
214
+ this.zenfloHomeDir = node_path.join(os.homedir(), ".zenflo");
215
215
  }
216
- this.logsDir = node_path.join(this.happyHomeDir, "logs");
217
- this.settingsFile = node_path.join(this.happyHomeDir, "settings.json");
218
- this.privateKeyFile = node_path.join(this.happyHomeDir, "access.key");
219
- this.daemonStateFile = node_path.join(this.happyHomeDir, "daemon.state.json");
220
- this.daemonLockFile = node_path.join(this.happyHomeDir, "daemon.state.json.lock");
221
- this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
222
- this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.HAPPY_DISABLE_CAFFEINATE?.toLowerCase() || "");
216
+ this.logsDir = node_path.join(this.zenfloHomeDir, "logs");
217
+ this.settingsFile = node_path.join(this.zenfloHomeDir, "settings.json");
218
+ this.privateKeyFile = node_path.join(this.zenfloHomeDir, "access.key");
219
+ this.daemonStateFile = node_path.join(this.zenfloHomeDir, "daemon.state.json");
220
+ this.daemonLockFile = node_path.join(this.zenfloHomeDir, "daemon.state.json.lock");
221
+ this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.ZENFLO_EXPERIMENTAL?.toLowerCase() || "");
222
+ this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.ZENFLO_DISABLE_CAFFEINATE?.toLowerCase() || "");
223
223
  this.currentCliVersion = packageJson.version;
224
- if (!fs.existsSync(this.happyHomeDir)) {
225
- fs.mkdirSync(this.happyHomeDir, { recursive: true });
224
+ if (!fs.existsSync(this.zenfloHomeDir)) {
225
+ fs.mkdirSync(this.zenfloHomeDir, { recursive: true });
226
226
  }
227
227
  if (!fs.existsSync(this.logsDir)) {
228
228
  fs.mkdirSync(this.logsDir, { recursive: true });
@@ -382,8 +382,8 @@ async function updateSettings(updater) {
382
382
  try {
383
383
  const current = await readSettings() || { ...defaultSettings };
384
384
  const updated = await updater(current);
385
- if (!fs.existsSync(configuration.happyHomeDir)) {
386
- await promises.mkdir(configuration.happyHomeDir, { recursive: true });
385
+ if (!fs.existsSync(configuration.zenfloHomeDir)) {
386
+ await promises.mkdir(configuration.zenfloHomeDir, { recursive: true });
387
387
  }
388
388
  await promises.writeFile(tmpFile, JSON.stringify(updated, null, 2));
389
389
  await promises.rename(tmpFile, configuration.settingsFile);
@@ -434,8 +434,8 @@ async function readCredentials() {
434
434
  return null;
435
435
  }
436
436
  async function writeCredentialsLegacy(credentials) {
437
- if (!fs.existsSync(configuration.happyHomeDir)) {
438
- await promises.mkdir(configuration.happyHomeDir, { recursive: true });
437
+ if (!fs.existsSync(configuration.zenfloHomeDir)) {
438
+ await promises.mkdir(configuration.zenfloHomeDir, { recursive: true });
439
439
  }
440
440
  await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
441
441
  secret: encodeBase64(credentials.secret),
@@ -443,8 +443,8 @@ async function writeCredentialsLegacy(credentials) {
443
443
  }, null, 2));
444
444
  }
445
445
  async function writeCredentialsDataKey(credentials) {
446
- if (!fs.existsSync(configuration.happyHomeDir)) {
447
- await promises.mkdir(configuration.happyHomeDir, { recursive: true });
446
+ if (!fs.existsSync(configuration.zenfloHomeDir)) {
447
+ await promises.mkdir(configuration.zenfloHomeDir, { recursive: true });
448
448
  }
449
449
  await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
450
450
  encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
@@ -788,10 +788,10 @@ z.z.object({
788
788
  z.z.object({
789
789
  host: z.z.string(),
790
790
  platform: z.z.string(),
791
- happyCliVersion: z.z.string(),
791
+ zenfloCliVersion: z.z.string(),
792
792
  homeDir: z.z.string(),
793
- happyHomeDir: z.z.string(),
794
- happyLibDir: z.z.string()
793
+ zenfloHomeDir: z.z.string(),
794
+ zenfloLibDir: z.z.string()
795
795
  });
796
796
  z.z.object({
797
797
  status: z.z.union([
@@ -1019,7 +1019,7 @@ class RpcHandlerManager {
1019
1019
  }
1020
1020
  }
1021
1021
 
1022
- const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CJaqq466.cjs', document.baseURI).href))));
1022
+ const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-Dvhor4zW.cjs', document.baseURI).href))));
1023
1023
  function projectPath() {
1024
1024
  const path$1 = path.resolve(__dirname$1, "..");
1025
1025
  return path$1;
@@ -1463,9 +1463,11 @@ class ApiSessionClient extends node_events.EventEmitter {
1463
1463
  }
1464
1464
  logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
1465
1465
  const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
1466
+ const localId = typeof body.uuid === "string" ? body.uuid : null;
1466
1467
  this.socket.emit("message", {
1467
1468
  sid: this.sessionId,
1468
- message: encrypted
1469
+ message: encrypted,
1470
+ localId
1469
1471
  });
1470
1472
  if (body.type === "assistant" && body.message.usage) {
1471
1473
  try {
@@ -2190,6 +2192,14 @@ const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
2190
2192
  type: z.z.literal("system"),
2191
2193
  uuid: z.z.string()
2192
2194
  // Used in getMessageKey()
2195
+ }).passthrough(),
2196
+ // Queue operation - internal Claude Code message for managing task queue
2197
+ // These are not sent to backend, just need to be parseable
2198
+ z.z.object({
2199
+ type: z.z.literal("queue-operation"),
2200
+ operation: z.z.enum(["enqueue", "dequeue"]),
2201
+ timestamp: z.z.string(),
2202
+ sessionId: z.z.string()
2193
2203
  }).passthrough()
2194
2204
  ]);
2195
2205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zenflo",
3
- "version": "0.11.6",
3
+ "version": "0.11.7",
4
4
  "description": "Mobile and Web client for Claude Code and Codex - ZenFlo edition",
5
5
  "author": "Combined Memory",
6
6
  "license": "MIT",
@@ -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 "$@"