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.
- package/README.md +419 -37
- package/dist/codex/zenfloMcpStdioBridge.cjs +2 -2
- package/dist/codex/zenfloMcpStdioBridge.mjs +2 -2
- package/dist/{index-BtMdJglm.cjs → index-HBSmEvnF.cjs} +81 -40
- package/dist/{index-QlmPnTH6.mjs → index-yJG0qz0r.mjs} +80 -39
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +28 -13
- package/dist/lib.d.mts +28 -13
- package/dist/lib.mjs +1 -1
- package/dist/{runCodex-DrAbIuHU.mjs → runCodex-Bl0T0A2Z.mjs} +23 -6
- package/dist/{runCodex-Cg3lSta7.cjs → runCodex-cXLrsovg.cjs} +23 -6
- package/dist/{types-DjiA-t1_.mjs → types-DJhJK2jD.mjs} +33 -23
- package/dist/{types-CJaqq466.cjs → types-Dvhor4zW.cjs} +34 -24
- package/package.json +1 -1
- package/scripts/claude_local_launcher.cjs +262 -5
- package/scripts/extension_wrapper.sh +13 -0
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
191
|
+
this.zenfloHomeDir = expandedPath;
|
|
192
192
|
} else {
|
|
193
|
-
this.
|
|
193
|
+
this.zenfloHomeDir = join(homedir(), ".zenflo");
|
|
194
194
|
}
|
|
195
|
-
this.logsDir = join(this.
|
|
196
|
-
this.settingsFile = join(this.
|
|
197
|
-
this.privateKeyFile = join(this.
|
|
198
|
-
this.daemonStateFile = join(this.
|
|
199
|
-
this.daemonLockFile = join(this.
|
|
200
|
-
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.
|
|
201
|
-
this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.
|
|
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.
|
|
204
|
-
mkdirSync(this.
|
|
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.
|
|
365
|
-
await mkdir(configuration.
|
|
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.
|
|
417
|
-
await mkdir(configuration.
|
|
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.
|
|
426
|
-
await mkdir(configuration.
|
|
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
|
-
|
|
770
|
+
zenfloCliVersion: z$1.string(),
|
|
771
771
|
homeDir: z$1.string(),
|
|
772
|
-
|
|
773
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
212
|
+
this.zenfloHomeDir = expandedPath;
|
|
213
213
|
} else {
|
|
214
|
-
this.
|
|
214
|
+
this.zenfloHomeDir = node_path.join(os.homedir(), ".zenflo");
|
|
215
215
|
}
|
|
216
|
-
this.logsDir = node_path.join(this.
|
|
217
|
-
this.settingsFile = node_path.join(this.
|
|
218
|
-
this.privateKeyFile = node_path.join(this.
|
|
219
|
-
this.daemonStateFile = node_path.join(this.
|
|
220
|
-
this.daemonLockFile = node_path.join(this.
|
|
221
|
-
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.
|
|
222
|
-
this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.
|
|
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.
|
|
225
|
-
fs.mkdirSync(this.
|
|
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.
|
|
386
|
-
await promises.mkdir(configuration.
|
|
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.
|
|
438
|
-
await promises.mkdir(configuration.
|
|
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.
|
|
447
|
-
await promises.mkdir(configuration.
|
|
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
|
-
|
|
791
|
+
zenfloCliVersion: z.z.string(),
|
|
792
792
|
homeDir: z.z.string(),
|
|
793
|
-
|
|
794
|
-
|
|
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-
|
|
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,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
|
|
207
|
+
let pathname = '';
|
|
55
208
|
try {
|
|
56
209
|
const urlObj = new URL(url, 'http://localhost');
|
|
57
210
|
hostname = urlObj.hostname;
|
|
58
|
-
|
|
211
|
+
pathname = urlObj.pathname;
|
|
59
212
|
} catch (e) {
|
|
60
213
|
// If URL parsing fails, use defaults
|
|
61
214
|
hostname = 'unknown';
|
|
62
|
-
|
|
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
|
-
|
|
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 "$@"
|