context-lens 0.3.2 → 0.4.0
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 +52 -16
- package/dist/analysis/ingest.d.ts +13 -0
- package/dist/analysis/ingest.d.ts.map +1 -0
- package/dist/analysis/ingest.js +75 -0
- package/dist/analysis/ingest.js.map +1 -0
- package/dist/analysis/server.d.ts +10 -0
- package/dist/analysis/server.d.ts.map +1 -0
- package/dist/analysis/server.js +95 -0
- package/dist/analysis/server.js.map +1 -0
- package/dist/analysis/watcher.d.ts +55 -0
- package/dist/analysis/watcher.d.ts.map +1 -0
- package/dist/analysis/watcher.js +170 -0
- package/dist/analysis/watcher.js.map +1 -0
- package/dist/cli-utils.d.ts +16 -0
- package/dist/cli-utils.d.ts.map +1 -1
- package/dist/cli-utils.js +185 -3
- package/dist/cli-utils.js.map +1 -1
- package/dist/cli.js +629 -88
- package/dist/cli.js.map +1 -1
- package/dist/core/conversation.d.ts +8 -1
- package/dist/core/conversation.d.ts.map +1 -1
- package/dist/core/conversation.js +38 -12
- package/dist/core/conversation.js.map +1 -1
- package/dist/core/parse.d.ts.map +1 -1
- package/dist/core/parse.js +17 -1
- package/dist/core/parse.js.map +1 -1
- package/dist/core/session-analysis.d.ts +100 -0
- package/dist/core/session-analysis.d.ts.map +1 -0
- package/dist/core/session-analysis.js +435 -0
- package/dist/core/session-analysis.js.map +1 -0
- package/dist/core/session-format.d.ts +20 -0
- package/dist/core/session-format.d.ts.map +1 -0
- package/dist/core/session-format.js +298 -0
- package/dist/core/session-format.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +2 -0
- package/dist/core.js.map +1 -1
- package/dist/lhar/reader.d.ts +22 -0
- package/dist/lhar/reader.d.ts.map +1 -0
- package/dist/lhar/reader.js +43 -0
- package/dist/lhar/reader.js.map +1 -0
- package/dist/lhar/record.d.ts.map +1 -1
- package/dist/lhar/record.js +3 -0
- package/dist/lhar/record.js.map +1 -1
- package/dist/lhar/response.d.ts +8 -0
- package/dist/lhar/response.d.ts.map +1 -1
- package/dist/lhar/response.js +44 -0
- package/dist/lhar/response.js.map +1 -1
- package/dist/lhar/tools.d.ts +17 -0
- package/dist/lhar/tools.d.ts.map +1 -0
- package/dist/lhar/tools.js +48 -0
- package/dist/lhar/tools.js.map +1 -0
- package/dist/lhar-types.generated.d.ts +34 -0
- package/dist/lhar-types.generated.d.ts.map +1 -1
- package/dist/lhar.d.ts +4 -1
- package/dist/lhar.d.ts.map +1 -1
- package/dist/lhar.js +5 -1
- package/dist/lhar.js.map +1 -1
- package/dist/proxy/capture.d.ts +40 -0
- package/dist/proxy/capture.d.ts.map +1 -0
- package/dist/proxy/capture.js +56 -0
- package/dist/proxy/capture.js.map +1 -0
- package/dist/proxy/config.d.ts +16 -0
- package/dist/proxy/config.d.ts.map +1 -0
- package/dist/proxy/config.js +34 -0
- package/dist/proxy/config.js.map +1 -0
- package/dist/proxy/forward.d.ts +20 -0
- package/dist/proxy/forward.d.ts.map +1 -0
- package/dist/proxy/forward.js +210 -0
- package/dist/proxy/forward.js.map +1 -0
- package/dist/proxy/headers.d.ts +16 -0
- package/dist/proxy/headers.d.ts.map +1 -0
- package/dist/proxy/headers.js +37 -0
- package/dist/proxy/headers.js.map +1 -0
- package/dist/proxy/routing.d.ts +44 -0
- package/dist/proxy/routing.d.ts.map +1 -0
- package/dist/proxy/routing.js +114 -0
- package/dist/proxy/routing.js.map +1 -0
- package/dist/proxy/server.d.ts +27 -0
- package/dist/proxy/server.d.ts.map +1 -0
- package/dist/proxy/server.js +55 -0
- package/dist/proxy/server.js.map +1 -0
- package/dist/schemas.d.ts +328 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +249 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server/api.d.ts +3 -4
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +195 -220
- package/dist/server/api.js.map +1 -1
- package/dist/server/store.d.ts +23 -7
- package/dist/server/store.d.ts.map +1 -1
- package/dist/server/store.js +141 -50
- package/dist/server/store.js.map +1 -1
- package/dist/server/webui.d.ts +5 -2
- package/dist/server/webui.d.ts.map +1 -1
- package/dist/server/webui.js +32 -13
- package/dist/server/webui.js.map +1 -1
- package/dist/server-utils.d.ts +1 -2
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +0 -2
- package/dist/server-utils.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/mitm_addon.py +99 -28
- package/package.json +12 -5
- package/schema/lhar.schema.json +50 -0
- package/dist/server/config.d.ts +0 -13
- package/dist/server/config.d.ts.map +0 -1
- package/dist/server/config.js +0 -36
- package/dist/server/config.js.map +0 -1
- package/dist/server/proxy.d.ts +0 -13
- package/dist/server/proxy.d.ts.map +0 -1
- package/dist/server/proxy.js +0 -218
- package/dist/server/proxy.js.map +0 -1
- package/dist/server/static.d.ts +0 -9
- package/dist/server/static.d.ts.map +0 -1
- package/dist/server/static.js +0 -78
- package/dist/server/static.js.map +0 -1
- package/dist/server.d.ts +0 -3
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -43
- package/dist/server.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,69 +1,103 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import https from "node:https";
|
|
4
5
|
import net from "node:net";
|
|
5
|
-
import { platform, tmpdir } from "node:os";
|
|
6
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
6
7
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { CLI_CONSTANTS, getToolConfig } from "./cli-utils.js";
|
|
9
|
+
import { CLI_CONSTANTS, formatHelpText, getToolConfig, parseCliArgs, } from "./cli-utils.js";
|
|
10
|
+
import { VERSION } from "./version.generated.js";
|
|
9
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
12
|
const __dirname = dirname(__filename);
|
|
11
13
|
// Known tool config: env vars for the child process, extra CLI args, server env vars, and whether mitmproxy is needed
|
|
12
14
|
// Note: actual tool config lives in cli-utils.ts so it can be unit-tested without importing this entrypoint.
|
|
13
15
|
const LOCKFILE = "/tmp/context-lens.lock";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const filteredArgs = [];
|
|
19
|
-
for (let i = 0; i < args.length; i++) {
|
|
20
|
-
if (args[i] === "--privacy" && i + 1 < args.length) {
|
|
21
|
-
privacyLevel = args[i + 1];
|
|
22
|
-
i++; // skip the value
|
|
23
|
-
}
|
|
24
|
-
else if (args[i].startsWith("--privacy=")) {
|
|
25
|
-
privacyLevel = args[i].split("=", 2)[1];
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
filteredArgs.push(args[i]);
|
|
29
|
-
}
|
|
16
|
+
const parsedArgs = parseCliArgs(process.argv.slice(2));
|
|
17
|
+
if (parsedArgs.error) {
|
|
18
|
+
console.error(parsedArgs.error);
|
|
19
|
+
process.exit(1);
|
|
30
20
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
console.error(`Error: Invalid privacy level '${privacyLevel}'. Must be one of: minimal, standard, full`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
// Pass to server via env var
|
|
38
|
-
process.env.CONTEXT_LENS_PRIVACY = privacyLevel;
|
|
21
|
+
if (parsedArgs.showHelp) {
|
|
22
|
+
console.log(formatHelpText());
|
|
23
|
+
process.exit(0);
|
|
39
24
|
}
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const server = spawn("node", [serverPath], {
|
|
44
|
-
stdio: "inherit",
|
|
45
|
-
env: { ...process.env },
|
|
46
|
-
});
|
|
47
|
-
server.on("exit", (code) => process.exit(code || 0));
|
|
48
|
-
process.on("SIGINT", () => server.kill("SIGINT"));
|
|
49
|
-
process.on("SIGTERM", () => server.kill("SIGTERM"));
|
|
50
|
-
// Prevent early exit
|
|
51
|
-
process.stdin.resume();
|
|
25
|
+
if (parsedArgs.showVersion) {
|
|
26
|
+
console.log(VERSION);
|
|
27
|
+
process.exit(0);
|
|
52
28
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
29
|
+
if (parsedArgs.privacyLevel !== undefined) {
|
|
30
|
+
process.env.CONTEXT_LENS_PRIVACY = parsedArgs.privacyLevel;
|
|
31
|
+
}
|
|
32
|
+
if (!parsedArgs.noUpdateCheck &&
|
|
33
|
+
process.env.CONTEXT_LENS_NO_UPDATE_CHECK !== "1") {
|
|
34
|
+
void checkForUpdate(VERSION);
|
|
35
|
+
}
|
|
36
|
+
if (parsedArgs.commandName === "analyze") {
|
|
37
|
+
void runAnalyze(parsedArgs.commandArguments).then((exitCode) => process.exit(exitCode));
|
|
38
|
+
}
|
|
39
|
+
else if (parsedArgs.commandName === "doctor") {
|
|
40
|
+
void runDoctor().then((exitCode) => process.exit(exitCode));
|
|
41
|
+
}
|
|
42
|
+
else if (parsedArgs.commandName === "background") {
|
|
43
|
+
void runBackgroundCommand(parsedArgs.commandArguments, parsedArgs.noUi).then((exitCode) => process.exit(exitCode));
|
|
44
|
+
}
|
|
45
|
+
else if (!parsedArgs.commandName) {
|
|
46
|
+
if (parsedArgs.noUi) {
|
|
47
|
+
// Standalone mode (no UI): start proxy only
|
|
48
|
+
const proxyPath = join(__dirname, "proxy", "server.js");
|
|
49
|
+
const proxy = spawn("node", [proxyPath], {
|
|
50
|
+
stdio: "inherit",
|
|
51
|
+
env: { ...process.env },
|
|
52
|
+
});
|
|
53
|
+
function shutdownStandaloneProxyOnly(code) {
|
|
54
|
+
if (!proxy.killed)
|
|
55
|
+
proxy.kill();
|
|
56
|
+
process.exit(code);
|
|
57
|
+
}
|
|
58
|
+
proxy.on("exit", (code) => shutdownStandaloneProxyOnly(code || 0));
|
|
59
|
+
process.on("SIGINT", () => shutdownStandaloneProxyOnly(0));
|
|
60
|
+
process.on("SIGTERM", () => shutdownStandaloneProxyOnly(0));
|
|
61
|
+
process.stdin.resume();
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
else {
|
|
64
|
+
// Standalone mode: start both proxy and analysis server
|
|
65
|
+
const proxyPath = join(__dirname, "proxy", "server.js");
|
|
66
|
+
const analysisPath = join(__dirname, "analysis", "server.js");
|
|
67
|
+
const proxy = spawn("node", [proxyPath], {
|
|
68
|
+
stdio: "inherit",
|
|
69
|
+
env: { ...process.env },
|
|
70
|
+
});
|
|
71
|
+
const analysis = spawn("node", [analysisPath], {
|
|
72
|
+
stdio: "inherit",
|
|
73
|
+
env: { ...process.env },
|
|
74
|
+
});
|
|
75
|
+
function shutdownStandalone(code) {
|
|
76
|
+
if (!proxy.killed)
|
|
77
|
+
proxy.kill();
|
|
78
|
+
if (!analysis.killed)
|
|
79
|
+
analysis.kill();
|
|
80
|
+
process.exit(code);
|
|
81
|
+
}
|
|
82
|
+
proxy.on("exit", (code) => shutdownStandalone(code || 0));
|
|
83
|
+
analysis.on("exit", (code) => shutdownStandalone(code || 0));
|
|
84
|
+
process.on("SIGINT", () => shutdownStandalone(0));
|
|
85
|
+
process.on("SIGTERM", () => shutdownStandalone(0));
|
|
86
|
+
// Prevent early exit
|
|
87
|
+
process.stdin.resume();
|
|
62
88
|
}
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const commandName = parsedArgs.commandName;
|
|
92
|
+
const commandArguments = parsedArgs.commandArguments;
|
|
93
|
+
const noOpen = parsedArgs.noOpen;
|
|
94
|
+
const noUi = parsedArgs.noUi;
|
|
65
95
|
// Get tool-specific config
|
|
66
96
|
const toolConfig = getToolConfig(commandName);
|
|
97
|
+
if (noUi && toolConfig.needsMitm) {
|
|
98
|
+
console.error("Error: --no-ui is not supported for this command because mitm capture requires the analysis ingest API on :4041.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
67
101
|
// Check if proxy is already running
|
|
68
102
|
function isProxyRunning() {
|
|
69
103
|
return new Promise((resolve) => {
|
|
@@ -124,71 +158,151 @@ else {
|
|
|
124
158
|
return 0;
|
|
125
159
|
}
|
|
126
160
|
}
|
|
127
|
-
let
|
|
161
|
+
let proxyProcess = null;
|
|
162
|
+
let analysisProcess = null;
|
|
128
163
|
let mitmProcess = null;
|
|
129
|
-
let
|
|
164
|
+
let proxyReady = false;
|
|
165
|
+
let analysisReady = false;
|
|
130
166
|
let mitmReady = false;
|
|
131
167
|
let childProcess = null;
|
|
132
168
|
let piAgentDirToCleanup = null;
|
|
133
|
-
let
|
|
169
|
+
let shouldShutdownServers = false;
|
|
134
170
|
let cleanupDidRun = false;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (alreadyRunning) {
|
|
139
|
-
console.log("🔍 Context Lens proxy already running, attaching to it...");
|
|
140
|
-
incrementRefCount();
|
|
141
|
-
serverReady = true;
|
|
142
|
-
shouldShutdownProxy = false;
|
|
171
|
+
const requiresAnalysis = !noUi;
|
|
172
|
+
function checkBothReady() {
|
|
173
|
+
if (proxyReady && analysisReady) {
|
|
143
174
|
maybeStartMitmThenChild();
|
|
144
175
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
}
|
|
177
|
+
// Check if analysis server is already running
|
|
178
|
+
function isAnalysisRunning() {
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
const socket = net.connect({ port: 4041, host: "localhost" }, () => {
|
|
181
|
+
socket.end();
|
|
182
|
+
resolve(true);
|
|
183
|
+
});
|
|
184
|
+
socket.on("error", () => resolve(false));
|
|
185
|
+
socket.setTimeout(1000, () => {
|
|
186
|
+
socket.destroy();
|
|
187
|
+
resolve(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Start proxy and analysis server (or attach to existing ones)
|
|
192
|
+
async function initializeServers() {
|
|
193
|
+
const proxyAlreadyRunning = await isProxyRunning();
|
|
194
|
+
const analysisAlreadyRunning = requiresAnalysis
|
|
195
|
+
? await isAnalysisRunning()
|
|
196
|
+
: false;
|
|
197
|
+
const allRequiredRunning = proxyAlreadyRunning && (!requiresAnalysis || analysisAlreadyRunning);
|
|
198
|
+
if (allRequiredRunning) {
|
|
199
|
+
console.log("🔍 Context Lens already running, attaching...");
|
|
149
200
|
incrementRefCount();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
201
|
+
proxyReady = true;
|
|
202
|
+
analysisReady = !requiresAnalysis || analysisAlreadyRunning;
|
|
203
|
+
shouldShutdownServers = false;
|
|
204
|
+
checkBothReady();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.log("🔍 Starting Context Lens proxy and analysis server...");
|
|
208
|
+
// Clear stale lockfile if servers aren't actually running
|
|
209
|
+
if (!proxyAlreadyRunning)
|
|
210
|
+
clearStaleLockfile();
|
|
211
|
+
incrementRefCount();
|
|
212
|
+
shouldShutdownServers = true;
|
|
213
|
+
const serverEnv = {
|
|
214
|
+
...toolConfig.serverEnv,
|
|
215
|
+
...process.env,
|
|
216
|
+
CONTEXT_LENS_CLI: "1",
|
|
217
|
+
};
|
|
218
|
+
// Start proxy
|
|
219
|
+
if (proxyAlreadyRunning) {
|
|
220
|
+
proxyReady = true;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const proxyPath = join(__dirname, "proxy", "server.js");
|
|
224
|
+
proxyProcess = spawn("node", [proxyPath], {
|
|
153
225
|
stdio: ["ignore", "pipe", "pipe"],
|
|
154
226
|
detached: false,
|
|
155
|
-
env:
|
|
227
|
+
env: serverEnv,
|
|
156
228
|
});
|
|
157
|
-
|
|
158
|
-
serverProcess.stdout?.on("data", (data) => {
|
|
229
|
+
proxyProcess.stdout?.on("data", (data) => {
|
|
159
230
|
const output = data.toString();
|
|
160
|
-
if (!
|
|
231
|
+
if (!proxyReady)
|
|
161
232
|
process.stderr.write(output);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
maybeStartMitmThenChild();
|
|
233
|
+
if (output.includes("Context Lens Proxy running") && !proxyReady) {
|
|
234
|
+
proxyReady = true;
|
|
235
|
+
checkBothReady();
|
|
166
236
|
}
|
|
167
237
|
});
|
|
168
|
-
|
|
169
|
-
if (!
|
|
238
|
+
proxyProcess.stderr?.on("data", (data) => {
|
|
239
|
+
if (!proxyReady)
|
|
170
240
|
process.stderr.write(data);
|
|
241
|
+
});
|
|
242
|
+
proxyProcess.on("error", (err) => {
|
|
243
|
+
console.error("Failed to start proxy:", err);
|
|
244
|
+
decrementRefCount();
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
|
247
|
+
proxyProcess.on("exit", (code) => {
|
|
248
|
+
if (!proxyReady) {
|
|
249
|
+
console.error("Proxy exited unexpectedly");
|
|
250
|
+
decrementRefCount();
|
|
251
|
+
process.exit(code || 1);
|
|
171
252
|
}
|
|
172
253
|
});
|
|
173
|
-
|
|
174
|
-
|
|
254
|
+
}
|
|
255
|
+
// Start analysis server
|
|
256
|
+
if (!requiresAnalysis) {
|
|
257
|
+
analysisReady = true;
|
|
258
|
+
}
|
|
259
|
+
else if (analysisAlreadyRunning) {
|
|
260
|
+
analysisReady = true;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const analysisPath = join(__dirname, "analysis", "server.js");
|
|
264
|
+
analysisProcess = spawn("node", [analysisPath], {
|
|
265
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
266
|
+
detached: false,
|
|
267
|
+
env: serverEnv,
|
|
268
|
+
});
|
|
269
|
+
analysisProcess.stdout?.on("data", (data) => {
|
|
270
|
+
const output = data.toString();
|
|
271
|
+
if (!analysisReady)
|
|
272
|
+
process.stderr.write(output);
|
|
273
|
+
if (output.includes("Context Lens Analysis running") &&
|
|
274
|
+
!analysisReady) {
|
|
275
|
+
analysisReady = true;
|
|
276
|
+
checkBothReady();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
analysisProcess.stderr?.on("data", (data) => {
|
|
280
|
+
if (!analysisReady)
|
|
281
|
+
process.stderr.write(data);
|
|
282
|
+
});
|
|
283
|
+
analysisProcess.on("error", (err) => {
|
|
284
|
+
console.error("Failed to start analysis server:", err);
|
|
175
285
|
decrementRefCount();
|
|
176
286
|
process.exit(1);
|
|
177
287
|
});
|
|
178
|
-
|
|
179
|
-
if (!
|
|
180
|
-
console.error("
|
|
288
|
+
analysisProcess.on("exit", (code) => {
|
|
289
|
+
if (!analysisReady) {
|
|
290
|
+
console.error("Analysis server exited unexpectedly");
|
|
181
291
|
decrementRefCount();
|
|
182
292
|
process.exit(code || 1);
|
|
183
293
|
}
|
|
184
294
|
});
|
|
185
|
-
|
|
295
|
+
}
|
|
296
|
+
// Open browser after a short delay (only when starting new servers)
|
|
297
|
+
if (!noOpen && requiresAnalysis) {
|
|
186
298
|
setTimeout(() => {
|
|
187
299
|
openBrowser("http://localhost:4041");
|
|
188
300
|
}, 1000);
|
|
189
301
|
}
|
|
302
|
+
// If both were already ready (mixed scenario), check now
|
|
303
|
+
checkBothReady();
|
|
190
304
|
}
|
|
191
|
-
|
|
305
|
+
initializeServers();
|
|
192
306
|
// Start mitmproxy if needed, then start the child
|
|
193
307
|
function maybeStartMitmThenChild() {
|
|
194
308
|
if (!toolConfig.needsMitm) {
|
|
@@ -241,6 +355,16 @@ else {
|
|
|
241
355
|
...process.env,
|
|
242
356
|
...toolConfig.childEnv,
|
|
243
357
|
};
|
|
358
|
+
// Fill in mitmproxy CA cert path for tools that need HTTPS interception
|
|
359
|
+
if (toolConfig.needsMitm && childEnv.SSL_CERT_FILE === "") {
|
|
360
|
+
const certPath = join(homedir(), ".mitmproxy", "mitmproxy-ca-cert.pem");
|
|
361
|
+
if (fs.existsSync(certPath)) {
|
|
362
|
+
childEnv.SSL_CERT_FILE = certPath;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.error(`Warning: mitmproxy CA cert not found at ${certPath}. Run 'mitmdump' once to generate it.`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
244
368
|
if (commandName === "pi") {
|
|
245
369
|
childEnv.PI_CODING_AGENT_DIR = preparePiAgentDir(childEnv.PI_CODING_AGENT_DIR);
|
|
246
370
|
}
|
|
@@ -251,6 +375,13 @@ else {
|
|
|
251
375
|
env: childEnv,
|
|
252
376
|
});
|
|
253
377
|
childProcess.on("error", (err) => {
|
|
378
|
+
if (err.code === "ENOENT") {
|
|
379
|
+
console.error(`\nFailed to start '${commandName}': command not found.`);
|
|
380
|
+
console.error("Try a known tool (claude, codex, gemini, aider, pi) or use:");
|
|
381
|
+
console.error(" context-lens -- <your-command> [args...]");
|
|
382
|
+
cleanup(127);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
254
385
|
console.error(`\nFailed to start ${commandName}:`, err.message);
|
|
255
386
|
cleanup(1);
|
|
256
387
|
});
|
|
@@ -417,11 +548,11 @@ else {
|
|
|
417
548
|
console.error("Warning: failed to clean up temporary Pi config dir:", err instanceof Error ? err.message : String(err));
|
|
418
549
|
}
|
|
419
550
|
}
|
|
420
|
-
if (remainingRefs === 0 &&
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
!
|
|
424
|
-
|
|
551
|
+
if (remainingRefs === 0 && shouldShutdownServers) {
|
|
552
|
+
if (proxyProcess && !proxyProcess.killed)
|
|
553
|
+
proxyProcess.kill();
|
|
554
|
+
if (analysisProcess && !analysisProcess.killed)
|
|
555
|
+
analysisProcess.kill();
|
|
425
556
|
}
|
|
426
557
|
process.exit(exitCode);
|
|
427
558
|
}
|
|
@@ -434,4 +565,414 @@ else {
|
|
|
434
565
|
childProcess.kill("SIGTERM");
|
|
435
566
|
});
|
|
436
567
|
}
|
|
568
|
+
function getBackgroundStatePath() {
|
|
569
|
+
return join(homedir(), ".context-lens", "background.json");
|
|
570
|
+
}
|
|
571
|
+
function ensureContextLensDir() {
|
|
572
|
+
fs.mkdirSync(join(homedir(), ".context-lens"), { recursive: true });
|
|
573
|
+
}
|
|
574
|
+
function readBackgroundState() {
|
|
575
|
+
try {
|
|
576
|
+
const raw = fs.readFileSync(getBackgroundStatePath(), "utf8");
|
|
577
|
+
const parsed = JSON.parse(raw);
|
|
578
|
+
if (parsed &&
|
|
579
|
+
typeof parsed === "object" &&
|
|
580
|
+
typeof parsed.proxyPid === "number" &&
|
|
581
|
+
(typeof parsed.analysisPid === "number" || parsed.analysisPid === null) &&
|
|
582
|
+
typeof parsed.noUi === "boolean" &&
|
|
583
|
+
typeof parsed.startedAt === "string") {
|
|
584
|
+
return parsed;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch { }
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
function writeBackgroundState(state) {
|
|
591
|
+
ensureContextLensDir();
|
|
592
|
+
fs.writeFileSync(getBackgroundStatePath(), `${JSON.stringify(state, null, 2)}\n`);
|
|
593
|
+
}
|
|
594
|
+
function clearBackgroundState() {
|
|
595
|
+
try {
|
|
596
|
+
fs.unlinkSync(getBackgroundStatePath());
|
|
597
|
+
}
|
|
598
|
+
catch { }
|
|
599
|
+
}
|
|
600
|
+
function isPidAlive(pid) {
|
|
601
|
+
try {
|
|
602
|
+
process.kill(pid, 0);
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function isBackgroundRunning(state) {
|
|
610
|
+
const proxyAlive = isPidAlive(state.proxyPid);
|
|
611
|
+
const analysisAlive = state.analysisPid == null || isPidAlive(state.analysisPid);
|
|
612
|
+
return proxyAlive && analysisAlive;
|
|
613
|
+
}
|
|
614
|
+
function parseBackgroundArgs(args, globalNoUi) {
|
|
615
|
+
const actionArg = args[0] || "status";
|
|
616
|
+
if (!["start", "stop", "status"].includes(actionArg)) {
|
|
617
|
+
return {
|
|
618
|
+
error: "Error: background command requires one of: start, stop, status",
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const localNoUi = args.includes("--no-ui");
|
|
622
|
+
return {
|
|
623
|
+
action: actionArg,
|
|
624
|
+
noUi: globalNoUi || localNoUi,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
async function runBackgroundCommand(args, globalNoUi) {
|
|
628
|
+
const parsed = parseBackgroundArgs(args, globalNoUi);
|
|
629
|
+
if ("error" in parsed) {
|
|
630
|
+
console.error(parsed.error);
|
|
631
|
+
return 1;
|
|
632
|
+
}
|
|
633
|
+
if (parsed.action === "status") {
|
|
634
|
+
return backgroundStatus();
|
|
635
|
+
}
|
|
636
|
+
if (parsed.action === "stop") {
|
|
637
|
+
return backgroundStop();
|
|
638
|
+
}
|
|
639
|
+
return backgroundStart(parsed.noUi);
|
|
640
|
+
}
|
|
641
|
+
function backgroundStatus() {
|
|
642
|
+
const state = readBackgroundState();
|
|
643
|
+
if (!state) {
|
|
644
|
+
console.log("Background status: not running");
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
const running = isBackgroundRunning(state);
|
|
648
|
+
if (!running) {
|
|
649
|
+
console.log("Background status: stale state found (not running)");
|
|
650
|
+
clearBackgroundState();
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
console.log("Background status: running");
|
|
654
|
+
console.log(` proxy pid: ${state.proxyPid}`);
|
|
655
|
+
if (state.analysisPid != null) {
|
|
656
|
+
console.log(` analysis pid: ${state.analysisPid}`);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
console.log(" analysis: disabled (--no-ui)");
|
|
660
|
+
}
|
|
661
|
+
console.log(` started: ${state.startedAt}`);
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
function backgroundStop() {
|
|
665
|
+
const state = readBackgroundState();
|
|
666
|
+
if (!state) {
|
|
667
|
+
console.log("Background status: not running");
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
const pids = [state.proxyPid, state.analysisPid].filter((pid) => pid != null);
|
|
671
|
+
for (const pid of pids) {
|
|
672
|
+
try {
|
|
673
|
+
process.kill(pid, "SIGTERM");
|
|
674
|
+
}
|
|
675
|
+
catch { }
|
|
676
|
+
}
|
|
677
|
+
clearBackgroundState();
|
|
678
|
+
console.log("Background services stopped.");
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
async function backgroundStart(noUi) {
|
|
682
|
+
const existing = readBackgroundState();
|
|
683
|
+
if (existing && isBackgroundRunning(existing)) {
|
|
684
|
+
console.log("Background status: already running");
|
|
685
|
+
console.log(` proxy pid: ${existing.proxyPid}`);
|
|
686
|
+
return 0;
|
|
687
|
+
}
|
|
688
|
+
if (existing)
|
|
689
|
+
clearBackgroundState();
|
|
690
|
+
const proxyPath = join(__dirname, "proxy", "server.js");
|
|
691
|
+
const proxy = spawn("node", [proxyPath], {
|
|
692
|
+
stdio: "ignore",
|
|
693
|
+
detached: true,
|
|
694
|
+
env: { ...process.env, CONTEXT_LENS_CLI: "1" },
|
|
695
|
+
});
|
|
696
|
+
proxy.unref();
|
|
697
|
+
let analysis = null;
|
|
698
|
+
if (!noUi) {
|
|
699
|
+
const analysisPath = join(__dirname, "analysis", "server.js");
|
|
700
|
+
analysis = spawn("node", [analysisPath], {
|
|
701
|
+
stdio: "ignore",
|
|
702
|
+
detached: true,
|
|
703
|
+
env: { ...process.env, CONTEXT_LENS_CLI: "1" },
|
|
704
|
+
});
|
|
705
|
+
analysis.unref();
|
|
706
|
+
}
|
|
707
|
+
// Give children a brief chance to fail immediately (e.g. command not found).
|
|
708
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
709
|
+
const proxyPid = proxy.pid ?? 0;
|
|
710
|
+
const analysisPid = analysis?.pid ?? null;
|
|
711
|
+
if (!proxyPid || !isPidAlive(proxyPid)) {
|
|
712
|
+
console.error("Failed to start proxy in background.");
|
|
713
|
+
return 1;
|
|
714
|
+
}
|
|
715
|
+
if (analysisPid != null && !isPidAlive(analysisPid)) {
|
|
716
|
+
console.error("Failed to start analysis server in background.");
|
|
717
|
+
try {
|
|
718
|
+
process.kill(proxyPid, "SIGTERM");
|
|
719
|
+
}
|
|
720
|
+
catch { }
|
|
721
|
+
return 1;
|
|
722
|
+
}
|
|
723
|
+
writeBackgroundState({
|
|
724
|
+
proxyPid,
|
|
725
|
+
analysisPid,
|
|
726
|
+
noUi,
|
|
727
|
+
startedAt: new Date().toISOString(),
|
|
728
|
+
});
|
|
729
|
+
console.log("Background services started.");
|
|
730
|
+
console.log(` proxy: http://localhost:4040 (pid ${proxyPid})`);
|
|
731
|
+
if (analysisPid != null) {
|
|
732
|
+
console.log(` analysis/web UI: http://localhost:4041 (pid ${analysisPid})`);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
console.log(" analysis/web UI: disabled (--no-ui)");
|
|
736
|
+
}
|
|
737
|
+
return 0;
|
|
738
|
+
}
|
|
739
|
+
async function isPortListening(port) {
|
|
740
|
+
return await new Promise((resolve) => {
|
|
741
|
+
const socket = net.connect({ port, host: "localhost" }, () => {
|
|
742
|
+
socket.end();
|
|
743
|
+
resolve(true);
|
|
744
|
+
});
|
|
745
|
+
socket.on("error", () => resolve(false));
|
|
746
|
+
socket.setTimeout(700, () => {
|
|
747
|
+
socket.destroy();
|
|
748
|
+
resolve(false);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function findBinaryOnPath(binary) {
|
|
753
|
+
const pathValue = process.env.PATH || "";
|
|
754
|
+
const dirs = pathValue.split(":").filter(Boolean);
|
|
755
|
+
for (const dir of dirs) {
|
|
756
|
+
const full = join(dir, binary);
|
|
757
|
+
if (fs.existsSync(full))
|
|
758
|
+
return full;
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
function checkWritableDir(targetDir) {
|
|
763
|
+
try {
|
|
764
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
765
|
+
fs.accessSync(targetDir, fs.constants.W_OK);
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function runAnalyze(args) {
|
|
773
|
+
// Lazy import to avoid loading analysis code unless needed
|
|
774
|
+
const { readLharFile } = await import("./lhar.js");
|
|
775
|
+
const { analyzeSession, formatSessionAnalysis } = await import("./core.js");
|
|
776
|
+
// Parse analyze-specific arguments
|
|
777
|
+
let filepath;
|
|
778
|
+
let outputJson = false;
|
|
779
|
+
let mainOnly = false;
|
|
780
|
+
let showPath = true;
|
|
781
|
+
let compositionArg;
|
|
782
|
+
for (const arg of args) {
|
|
783
|
+
if (arg === "--json") {
|
|
784
|
+
outputJson = true;
|
|
785
|
+
}
|
|
786
|
+
else if (arg === "--main-only") {
|
|
787
|
+
mainOnly = true;
|
|
788
|
+
}
|
|
789
|
+
else if (arg === "--no-path") {
|
|
790
|
+
showPath = false;
|
|
791
|
+
}
|
|
792
|
+
else if (arg.startsWith("--composition=")) {
|
|
793
|
+
compositionArg = arg.split("=", 1 + 1)[1];
|
|
794
|
+
}
|
|
795
|
+
else if (arg === "--help" || arg === "-h") {
|
|
796
|
+
console.log([
|
|
797
|
+
"Usage: context-lens analyze <session.lhar> [options]",
|
|
798
|
+
"",
|
|
799
|
+
"Analyze an LHAR session file and print detailed statistics.",
|
|
800
|
+
"",
|
|
801
|
+
"Options:",
|
|
802
|
+
" --json Output as JSON",
|
|
803
|
+
" --no-path Omit agent path trace",
|
|
804
|
+
" --main-only Only analyze main agent entries",
|
|
805
|
+
" --composition=last Composition of last entry (default)",
|
|
806
|
+
" --composition=pre-compaction Composition before each compaction",
|
|
807
|
+
" --composition=N Composition at end of user turn N",
|
|
808
|
+
].join("\n"));
|
|
809
|
+
return 0;
|
|
810
|
+
}
|
|
811
|
+
else if (!arg.startsWith("-")) {
|
|
812
|
+
filepath = arg;
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
console.error(`Unknown option: ${arg}`);
|
|
816
|
+
return 1;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (!filepath) {
|
|
820
|
+
console.error("Error: no session file specified. Usage: context-lens analyze <session.lhar>");
|
|
821
|
+
return 1;
|
|
822
|
+
}
|
|
823
|
+
// Resolve filepath: try as-is, then in ~/.context-lens/data/, then in ./data/
|
|
824
|
+
let resolvedPath = filepath;
|
|
825
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
826
|
+
const homeData = join(homedir(), ".context-lens", "data", filepath);
|
|
827
|
+
const localData = join("data", filepath);
|
|
828
|
+
if (fs.existsSync(homeData)) {
|
|
829
|
+
resolvedPath = homeData;
|
|
830
|
+
}
|
|
831
|
+
else if (fs.existsSync(localData)) {
|
|
832
|
+
resolvedPath = localData;
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
console.error(`Error: file not found: ${filepath}`);
|
|
836
|
+
console.error(` Searched: ${filepath}, ${homeData}, ${localData}`);
|
|
837
|
+
return 1;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
const { session, entries } = readLharFile(resolvedPath);
|
|
842
|
+
const basename = resolvedPath.split("/").pop() || resolvedPath;
|
|
843
|
+
const analysis = analyzeSession(session, entries, basename, { mainOnly });
|
|
844
|
+
if (outputJson) {
|
|
845
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
const output = formatSessionAnalysis(analysis, {
|
|
849
|
+
showPath,
|
|
850
|
+
composition: compositionArg,
|
|
851
|
+
entries,
|
|
852
|
+
});
|
|
853
|
+
console.log(output);
|
|
854
|
+
}
|
|
855
|
+
return 0;
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
console.error(`Error analyzing ${resolvedPath}:`, err instanceof Error ? err.message : String(err));
|
|
859
|
+
return 1;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function runDoctor() {
|
|
863
|
+
let hasFailures = false;
|
|
864
|
+
function report(name, ok, detail) {
|
|
865
|
+
const mark = ok ? "OK" : "FAIL";
|
|
866
|
+
console.log(`[${mark}] ${name}: ${detail}`);
|
|
867
|
+
if (!ok)
|
|
868
|
+
hasFailures = true;
|
|
869
|
+
}
|
|
870
|
+
console.log(`Context Lens doctor v${VERSION}`);
|
|
871
|
+
report("node", true, process.version);
|
|
872
|
+
const proxyListening = await isPortListening(4040);
|
|
873
|
+
report("proxy port :4040", true, proxyListening ? "already running" : "available/not running");
|
|
874
|
+
const analysisListening = await isPortListening(4041);
|
|
875
|
+
report("analysis port :4041", true, analysisListening ? "already running" : "available/not running");
|
|
876
|
+
const mitmdumpPath = findBinaryOnPath("mitmdump");
|
|
877
|
+
report("mitmdump", !!mitmdumpPath, mitmdumpPath ??
|
|
878
|
+
"not found (required for Codex; install: pipx install mitmproxy)");
|
|
879
|
+
const certPath = join(homedir(), ".mitmproxy", "mitmproxy-ca-cert.pem");
|
|
880
|
+
report("mitm CA cert", fs.existsSync(certPath), fs.existsSync(certPath)
|
|
881
|
+
? certPath
|
|
882
|
+
: `${certPath} (not present; run 'mitmdump' once to generate it)`);
|
|
883
|
+
const contextDir = join(homedir(), ".context-lens");
|
|
884
|
+
const dataDir = join(contextDir, "data");
|
|
885
|
+
report("context dir writable", checkWritableDir(contextDir), contextDir);
|
|
886
|
+
report("data dir writable", checkWritableDir(dataDir), dataDir);
|
|
887
|
+
const bg = readBackgroundState();
|
|
888
|
+
if (!bg) {
|
|
889
|
+
report("background state", true, "not running");
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
report("background state", isBackgroundRunning(bg), isBackgroundRunning(bg) ? "running" : "stale state file");
|
|
893
|
+
}
|
|
894
|
+
const lockfileExists = fs.existsSync(LOCKFILE);
|
|
895
|
+
report("lockfile", true, lockfileExists ? `${LOCKFILE} present` : `${LOCKFILE} absent`);
|
|
896
|
+
if (hasFailures) {
|
|
897
|
+
console.log("Doctor result: issues found.");
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
console.log("Doctor result: all checks passed.");
|
|
901
|
+
return 0;
|
|
902
|
+
}
|
|
903
|
+
function checkForUpdate(currentVersion) {
|
|
904
|
+
const cachePath = join(homedir(), ".context-lens", "update-check.json");
|
|
905
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
906
|
+
let cached = null;
|
|
907
|
+
try {
|
|
908
|
+
const raw = fs.readFileSync(cachePath, "utf8");
|
|
909
|
+
const parsed = JSON.parse(raw);
|
|
910
|
+
if (parsed &&
|
|
911
|
+
typeof parsed === "object" &&
|
|
912
|
+
typeof parsed.checkedAt === "number" &&
|
|
913
|
+
typeof parsed.latestVersion === "string") {
|
|
914
|
+
cached = parsed;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
catch { }
|
|
918
|
+
if (cached && Date.now() - cached.checkedAt < dayMs) {
|
|
919
|
+
if (isNewerVersion(cached.latestVersion, currentVersion)) {
|
|
920
|
+
printUpdateNotice(currentVersion, cached.latestVersion);
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const req = https.get("https://registry.npmjs.org/context-lens/latest", { timeout: 1500 }, (res) => {
|
|
925
|
+
if (res.statusCode !== 200)
|
|
926
|
+
return;
|
|
927
|
+
let body = "";
|
|
928
|
+
res.setEncoding("utf8");
|
|
929
|
+
res.on("data", (chunk) => {
|
|
930
|
+
body += chunk;
|
|
931
|
+
});
|
|
932
|
+
res.on("end", () => {
|
|
933
|
+
try {
|
|
934
|
+
const parsed = JSON.parse(body);
|
|
935
|
+
if (!parsed.version)
|
|
936
|
+
return;
|
|
937
|
+
const latestVersion = parsed.version;
|
|
938
|
+
try {
|
|
939
|
+
fs.mkdirSync(join(homedir(), ".context-lens"), {
|
|
940
|
+
recursive: true,
|
|
941
|
+
});
|
|
942
|
+
fs.writeFileSync(cachePath, `${JSON.stringify({ checkedAt: Date.now(), latestVersion }, null, 2)}\n`);
|
|
943
|
+
}
|
|
944
|
+
catch { }
|
|
945
|
+
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
946
|
+
printUpdateNotice(currentVersion, latestVersion);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
catch { }
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
req.on("error", () => { });
|
|
953
|
+
req.on("timeout", () => req.destroy());
|
|
954
|
+
}
|
|
955
|
+
function isNewerVersion(candidate, current) {
|
|
956
|
+
const a = splitSemver(candidate);
|
|
957
|
+
const b = splitSemver(current);
|
|
958
|
+
for (let i = 0; i < 3; i++) {
|
|
959
|
+
if (a[i] > b[i])
|
|
960
|
+
return true;
|
|
961
|
+
if (a[i] < b[i])
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
function splitSemver(version) {
|
|
967
|
+
const [major, minor, patch] = version.split(".", 3).map((part) => {
|
|
968
|
+
const parsed = Number.parseInt(part.replace(/[^0-9].*$/, ""), 10);
|
|
969
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
970
|
+
});
|
|
971
|
+
return [major ?? 0, minor ?? 0, patch ?? 0];
|
|
972
|
+
}
|
|
973
|
+
function printUpdateNotice(currentVersion, latestVersion) {
|
|
974
|
+
console.error(`\nUpdate available: context-lens ${currentVersion} -> ${latestVersion}`);
|
|
975
|
+
console.error("Run: npm install -g context-lens");
|
|
976
|
+
console.error("Skip this check: --no-update-check or CONTEXT_LENS_NO_UPDATE_CHECK=1\n");
|
|
977
|
+
}
|
|
437
978
|
//# sourceMappingURL=cli.js.map
|