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.
Files changed (126) hide show
  1. package/README.md +52 -16
  2. package/dist/analysis/ingest.d.ts +13 -0
  3. package/dist/analysis/ingest.d.ts.map +1 -0
  4. package/dist/analysis/ingest.js +75 -0
  5. package/dist/analysis/ingest.js.map +1 -0
  6. package/dist/analysis/server.d.ts +10 -0
  7. package/dist/analysis/server.d.ts.map +1 -0
  8. package/dist/analysis/server.js +95 -0
  9. package/dist/analysis/server.js.map +1 -0
  10. package/dist/analysis/watcher.d.ts +55 -0
  11. package/dist/analysis/watcher.d.ts.map +1 -0
  12. package/dist/analysis/watcher.js +170 -0
  13. package/dist/analysis/watcher.js.map +1 -0
  14. package/dist/cli-utils.d.ts +16 -0
  15. package/dist/cli-utils.d.ts.map +1 -1
  16. package/dist/cli-utils.js +185 -3
  17. package/dist/cli-utils.js.map +1 -1
  18. package/dist/cli.js +629 -88
  19. package/dist/cli.js.map +1 -1
  20. package/dist/core/conversation.d.ts +8 -1
  21. package/dist/core/conversation.d.ts.map +1 -1
  22. package/dist/core/conversation.js +38 -12
  23. package/dist/core/conversation.js.map +1 -1
  24. package/dist/core/parse.d.ts.map +1 -1
  25. package/dist/core/parse.js +17 -1
  26. package/dist/core/parse.js.map +1 -1
  27. package/dist/core/session-analysis.d.ts +100 -0
  28. package/dist/core/session-analysis.d.ts.map +1 -0
  29. package/dist/core/session-analysis.js +435 -0
  30. package/dist/core/session-analysis.js.map +1 -0
  31. package/dist/core/session-format.d.ts +20 -0
  32. package/dist/core/session-format.d.ts.map +1 -0
  33. package/dist/core/session-format.js +298 -0
  34. package/dist/core/session-format.js.map +1 -0
  35. package/dist/core.d.ts +4 -0
  36. package/dist/core.d.ts.map +1 -1
  37. package/dist/core.js +2 -0
  38. package/dist/core.js.map +1 -1
  39. package/dist/lhar/reader.d.ts +22 -0
  40. package/dist/lhar/reader.d.ts.map +1 -0
  41. package/dist/lhar/reader.js +43 -0
  42. package/dist/lhar/reader.js.map +1 -0
  43. package/dist/lhar/record.d.ts.map +1 -1
  44. package/dist/lhar/record.js +3 -0
  45. package/dist/lhar/record.js.map +1 -1
  46. package/dist/lhar/response.d.ts +8 -0
  47. package/dist/lhar/response.d.ts.map +1 -1
  48. package/dist/lhar/response.js +44 -0
  49. package/dist/lhar/response.js.map +1 -1
  50. package/dist/lhar/tools.d.ts +17 -0
  51. package/dist/lhar/tools.d.ts.map +1 -0
  52. package/dist/lhar/tools.js +48 -0
  53. package/dist/lhar/tools.js.map +1 -0
  54. package/dist/lhar-types.generated.d.ts +34 -0
  55. package/dist/lhar-types.generated.d.ts.map +1 -1
  56. package/dist/lhar.d.ts +4 -1
  57. package/dist/lhar.d.ts.map +1 -1
  58. package/dist/lhar.js +5 -1
  59. package/dist/lhar.js.map +1 -1
  60. package/dist/proxy/capture.d.ts +40 -0
  61. package/dist/proxy/capture.d.ts.map +1 -0
  62. package/dist/proxy/capture.js +56 -0
  63. package/dist/proxy/capture.js.map +1 -0
  64. package/dist/proxy/config.d.ts +16 -0
  65. package/dist/proxy/config.d.ts.map +1 -0
  66. package/dist/proxy/config.js +34 -0
  67. package/dist/proxy/config.js.map +1 -0
  68. package/dist/proxy/forward.d.ts +20 -0
  69. package/dist/proxy/forward.d.ts.map +1 -0
  70. package/dist/proxy/forward.js +210 -0
  71. package/dist/proxy/forward.js.map +1 -0
  72. package/dist/proxy/headers.d.ts +16 -0
  73. package/dist/proxy/headers.d.ts.map +1 -0
  74. package/dist/proxy/headers.js +37 -0
  75. package/dist/proxy/headers.js.map +1 -0
  76. package/dist/proxy/routing.d.ts +44 -0
  77. package/dist/proxy/routing.d.ts.map +1 -0
  78. package/dist/proxy/routing.js +114 -0
  79. package/dist/proxy/routing.js.map +1 -0
  80. package/dist/proxy/server.d.ts +27 -0
  81. package/dist/proxy/server.d.ts.map +1 -0
  82. package/dist/proxy/server.js +55 -0
  83. package/dist/proxy/server.js.map +1 -0
  84. package/dist/schemas.d.ts +328 -0
  85. package/dist/schemas.d.ts.map +1 -0
  86. package/dist/schemas.js +249 -0
  87. package/dist/schemas.js.map +1 -0
  88. package/dist/server/api.d.ts +3 -4
  89. package/dist/server/api.d.ts.map +1 -1
  90. package/dist/server/api.js +195 -220
  91. package/dist/server/api.js.map +1 -1
  92. package/dist/server/store.d.ts +23 -7
  93. package/dist/server/store.d.ts.map +1 -1
  94. package/dist/server/store.js +141 -50
  95. package/dist/server/store.js.map +1 -1
  96. package/dist/server/webui.d.ts +5 -2
  97. package/dist/server/webui.d.ts.map +1 -1
  98. package/dist/server/webui.js +32 -13
  99. package/dist/server/webui.js.map +1 -1
  100. package/dist/server-utils.d.ts +1 -2
  101. package/dist/server-utils.d.ts.map +1 -1
  102. package/dist/server-utils.js +0 -2
  103. package/dist/server-utils.js.map +1 -1
  104. package/dist/types.d.ts +1 -1
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/version.generated.d.ts +1 -1
  107. package/dist/version.generated.js +1 -1
  108. package/mitm_addon.py +99 -28
  109. package/package.json +12 -5
  110. package/schema/lhar.schema.json +50 -0
  111. package/dist/server/config.d.ts +0 -13
  112. package/dist/server/config.d.ts.map +0 -1
  113. package/dist/server/config.js +0 -36
  114. package/dist/server/config.js.map +0 -1
  115. package/dist/server/proxy.d.ts +0 -13
  116. package/dist/server/proxy.d.ts.map +0 -1
  117. package/dist/server/proxy.js +0 -218
  118. package/dist/server/proxy.js.map +0 -1
  119. package/dist/server/static.d.ts +0 -9
  120. package/dist/server/static.d.ts.map +0 -1
  121. package/dist/server/static.js +0 -78
  122. package/dist/server/static.js.map +0 -1
  123. package/dist/server.d.ts +0 -3
  124. package/dist/server.d.ts.map +0 -1
  125. package/dist/server.js +0 -43
  126. 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
- // Parse command line arguments
15
- const args = process.argv.slice(2);
16
- // Extract --privacy flag from args (before or after command)
17
- let privacyLevel;
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
- // Validate privacy level
32
- if (privacyLevel !== undefined) {
33
- if (!["minimal", "standard", "full"].includes(privacyLevel)) {
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 (filteredArgs.length === 0) {
41
- // Standalone mode: just start the proxy server
42
- const serverPath = join(__dirname, "server.js");
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
- else {
54
- // Skip '--' separator if present
55
- let commandArgs = filteredArgs;
56
- if (filteredArgs[0] === "--") {
57
- commandArgs = filteredArgs.slice(1);
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
- if (commandArgs.length === 0) {
60
- console.error("Error: No command specified after --");
61
- process.exit(1);
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
- const commandName = commandArgs[0];
64
- const commandArguments = commandArgs.slice(1);
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 serverProcess = null;
161
+ let proxyProcess = null;
162
+ let analysisProcess = null;
128
163
  let mitmProcess = null;
129
- let serverReady = false;
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 shouldShutdownProxy = false;
169
+ let shouldShutdownServers = false;
134
170
  let cleanupDidRun = false;
135
- // Start proxy or attach to existing one
136
- async function initializeProxy() {
137
- const alreadyRunning = await isProxyRunning();
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
- else {
146
- console.log("🔍 Starting Context Lens proxy and web UI...");
147
- // No proxy is listening on :4040. Any existing lockfile is stale and would prevent shutdown later.
148
- clearStaleLockfile();
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
- shouldShutdownProxy = true;
151
- const serverPath = join(__dirname, "server.js");
152
- serverProcess = spawn("node", [serverPath], {
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: { ...toolConfig.serverEnv, ...process.env, CONTEXT_LENS_CLI: "1" },
227
+ env: serverEnv,
156
228
  });
157
- // Wait for server to be ready, then suppress output (visible in web UI at :4041)
158
- serverProcess.stdout?.on("data", (data) => {
229
+ proxyProcess.stdout?.on("data", (data) => {
159
230
  const output = data.toString();
160
- if (!serverReady) {
231
+ if (!proxyReady)
161
232
  process.stderr.write(output);
162
- }
163
- if (output.includes("Context Lens Web UI running") && !serverReady) {
164
- serverReady = true;
165
- maybeStartMitmThenChild();
233
+ if (output.includes("Context Lens Proxy running") && !proxyReady) {
234
+ proxyReady = true;
235
+ checkBothReady();
166
236
  }
167
237
  });
168
- serverProcess.stderr?.on("data", (data) => {
169
- if (!serverReady) {
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
- serverProcess.on("error", (err) => {
174
- console.error("Failed to start server:", err);
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
- serverProcess.on("exit", (code) => {
179
- if (!serverReady) {
180
- console.error("Server exited unexpectedly");
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
- // Open browser after a short delay (only when starting new server)
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
- initializeProxy();
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
- shouldShutdownProxy &&
422
- serverProcess &&
423
- !serverProcess.killed) {
424
- serverProcess.kill();
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