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/README.md
CHANGED
|
@@ -34,6 +34,7 @@ npx context-lens ...
|
|
|
34
34
|
context-lens claude
|
|
35
35
|
context-lens codex
|
|
36
36
|
context-lens gemini
|
|
37
|
+
context-lens gm # alias for gemini
|
|
37
38
|
context-lens aider --model claude-sonnet-4
|
|
38
39
|
context-lens pi
|
|
39
40
|
context-lens -- python my_agent.py
|
|
@@ -43,6 +44,42 @@ Or without installing: replace `context-lens` with `npx context-lens`.
|
|
|
43
44
|
|
|
44
45
|
This starts the proxy (port 4040), opens the web UI (http://localhost:4041), sets the right env vars, and runs your command. Multiple tools can share one proxy; just open more terminals.
|
|
45
46
|
|
|
47
|
+
## CLI options
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
context-lens --help
|
|
51
|
+
context-lens --version
|
|
52
|
+
context-lens --privacy=minimal claude
|
|
53
|
+
context-lens --no-open codex
|
|
54
|
+
context-lens --no-ui -- claude
|
|
55
|
+
context-lens doctor
|
|
56
|
+
context-lens background start --no-ui
|
|
57
|
+
context-lens background status
|
|
58
|
+
context-lens background stop
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `--help`, `--version`: show usage/version and exit
|
|
62
|
+
- `--privacy <minimal|standard|full>`: controls privacy mode passed to the analysis server
|
|
63
|
+
- `--no-open`: do not auto-open `http://localhost:4041` when launching a command
|
|
64
|
+
- `--no-ui`: run proxy only (no analysis/web UI server) for capture-only data gathering
|
|
65
|
+
- `--no-update-check`: skip npm update check for this run
|
|
66
|
+
|
|
67
|
+
`--no-ui` is not compatible with `codex` subscription mode (`mitmproxy` ingestion depends on `http://localhost:4041/api/ingest`).
|
|
68
|
+
|
|
69
|
+
Built-in commands:
|
|
70
|
+
- `doctor`: run local diagnostics (ports, mitmproxy availability, cert path, writable dirs, background state)
|
|
71
|
+
- `background start [--no-ui]`: start detached proxy (and analysis/web UI unless `--no-ui`)
|
|
72
|
+
- `background status`: show detached process state
|
|
73
|
+
- `background stop`: stop detached process state
|
|
74
|
+
|
|
75
|
+
Aliases:
|
|
76
|
+
- `cc` -> `claude`
|
|
77
|
+
- `cpi` -> `pi`
|
|
78
|
+
- `cx` -> `codex`
|
|
79
|
+
- `gm` -> `gemini`
|
|
80
|
+
|
|
81
|
+
By default, the CLI does a cached (once per day) non-blocking check for new npm versions and prints an upgrade hint when a newer release is available. Disable globally with `CONTEXT_LENS_NO_UPDATE_CHECK=1`.
|
|
82
|
+
|
|
46
83
|
## Supported Providers
|
|
47
84
|
|
|
48
85
|
| Provider | Method | Status | Environment Variable |
|
|
@@ -153,22 +190,25 @@ If Codex fails with certificate trust errors, install/trust the mitmproxy CA cer
|
|
|
153
190
|
|
|
154
191
|
## How It Works
|
|
155
192
|
|
|
156
|
-
Context Lens sits between your coding tool and the LLM API, capturing requests in transit.
|
|
157
|
-
|
|
158
|
-
**Reverse proxy (Claude Code, aider, OpenAI API tools)**
|
|
193
|
+
Context Lens sits between your coding tool and the LLM API, capturing requests in transit. It has two parts: a **proxy** and an **analysis server**.
|
|
159
194
|
|
|
160
195
|
```
|
|
161
|
-
Tool ─HTTP─▶
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
Tool ─HTTP─▶ Proxy (:4040) ─HTTPS─▶ api.anthropic.com / api.openai.com
|
|
197
|
+
│
|
|
198
|
+
capture files
|
|
199
|
+
│
|
|
200
|
+
Analysis Server (:4041) → Web UI
|
|
165
201
|
```
|
|
166
202
|
|
|
167
|
-
The
|
|
203
|
+
The **proxy** (`src/proxy/`) forwards requests to the LLM API and writes each request/response pair to disk. It has **zero external dependencies** (only Node.js built-ins), so you can read the entire proxy source and verify it does nothing unexpected with your API keys. This is an intentional architectural constraint: your API keys pass through the proxy, so it must stay small, auditable, and free of transitive supply-chain risk.
|
|
204
|
+
|
|
205
|
+
The **analysis server** picks up those captures, parses request bodies, estimates tokens, groups requests into conversations, computes composition breakdowns, calculates costs, scores context health, and scans for prompt injection patterns. It serves the web UI and API. The two sides communicate only through capture files on disk, so the analysis server, CLI, and web UI are free to use whatever dependencies they need without affecting the proxy's trust boundary.
|
|
206
|
+
|
|
207
|
+
The CLI sets env vars like `ANTHROPIC_BASE_URL=http://localhost:4040` so the tool sends requests to the proxy instead of the real API. The tool never knows it's being proxied.
|
|
168
208
|
|
|
169
209
|
**Forward HTTPS proxy (Codex subscription mode)**
|
|
170
210
|
|
|
171
|
-
|
|
211
|
+
Codex with a ChatGPT subscription authenticates against `chatgpt.com`, which is behind Cloudflare. A reverse proxy changes the TLS fingerprint, causing Cloudflare to reject the request. For this case, Context Lens uses mitmproxy as a forward HTTPS proxy:
|
|
172
212
|
|
|
173
213
|
```
|
|
174
214
|
Tool ─HTTPS via proxy─▶ mitmproxy (:8080) ─HTTPS─▶ chatgpt.com
|
|
@@ -176,14 +216,10 @@ Tool ─HTTPS via proxy─▶ mitmproxy (:8080) ─HTTPS─▶ chatgpt.com
|
|
|
176
216
|
mitm_addon.py
|
|
177
217
|
│
|
|
178
218
|
▼
|
|
179
|
-
|
|
219
|
+
Analysis Server /api/ingest
|
|
180
220
|
```
|
|
181
221
|
|
|
182
|
-
The tool makes its own TLS connection through the proxy, preserving its native
|
|
183
|
-
|
|
184
|
-
**What the proxy captures**
|
|
185
|
-
|
|
186
|
-
Each request is parsed to extract: model name, system prompts, tool definitions, message history (with per-message token estimates), and content block types (text, tool calls, tool results, images, thinking). The response is captured to extract usage stats and cost. Requests are grouped into conversations using session IDs (Anthropic `metadata.user_id`), response chaining (OpenAI `previous_response_id`), or a fingerprint of the system prompt + first user message.
|
|
222
|
+
The tool makes its own TLS connection through the proxy, preserving its native fingerprint. The mitmproxy addon intercepts completed request/response pairs and posts them to the analysis server's ingest API. The tool needs `https_proxy` and `SSL_CERT_FILE` env vars set to route through mitmproxy and trust its CA certificate.
|
|
187
223
|
|
|
188
224
|
## Why Context Lens?
|
|
189
225
|
|
|
@@ -208,7 +244,7 @@ Context Lens is for developers who want to understand and optimize their coding
|
|
|
208
244
|
|
|
209
245
|
## Data
|
|
210
246
|
|
|
211
|
-
Captured requests are kept in memory (last 100) and persisted to
|
|
247
|
+
Captured requests are kept in memory (last 100 sessions) and persisted to `~/.context-lens/data/state.jsonl` across restarts. Each session is also logged as a separate `.lhar` file in `~/.context-lens/data/`. Use the Reset button in the UI to clear everything.
|
|
212
248
|
|
|
213
249
|
## License
|
|
214
250
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture ingestion: bridges raw proxy captures into the Store.
|
|
3
|
+
*
|
|
4
|
+
* Takes a CaptureData object (raw request/response from disk) and
|
|
5
|
+
* runs the full analysis pipeline, then stores the result.
|
|
6
|
+
*/
|
|
7
|
+
import type { CaptureData } from "../proxy/capture.js";
|
|
8
|
+
import type { Store } from "../server/store.js";
|
|
9
|
+
/**
|
|
10
|
+
* Process a single capture and feed it into the Store.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ingestCapture(store: Store, capture: CaptureData): void;
|
|
13
|
+
//# sourceMappingURL=ingest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/analysis/ingest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAGhD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAsEtE"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture ingestion: bridges raw proxy captures into the Store.
|
|
3
|
+
*
|
|
4
|
+
* Takes a CaptureData object (raw request/response from disk) and
|
|
5
|
+
* runs the full analysis pipeline, then stores the result.
|
|
6
|
+
*/
|
|
7
|
+
import { estimateTokens, parseContextInfo } from "../core.js";
|
|
8
|
+
/**
|
|
9
|
+
* Process a single capture and feed it into the Store.
|
|
10
|
+
*/
|
|
11
|
+
export function ingestCapture(store, capture) {
|
|
12
|
+
const { provider, apiFormat, requestBody, responseBody } = capture;
|
|
13
|
+
// Build contextInfo from the request body
|
|
14
|
+
let contextInfo;
|
|
15
|
+
if (requestBody) {
|
|
16
|
+
const body = { ...requestBody };
|
|
17
|
+
// Gemini: model is in the URL path, not in the body
|
|
18
|
+
if (apiFormat === "gemini" && !body.model) {
|
|
19
|
+
const modelMatch = capture.path.match(/\/models\/([^/:]+)/);
|
|
20
|
+
if (modelMatch)
|
|
21
|
+
body.model = modelMatch[1];
|
|
22
|
+
}
|
|
23
|
+
contextInfo = parseContextInfo(provider, body, apiFormat);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Non-JSON request: create a raw contextInfo
|
|
27
|
+
contextInfo = {
|
|
28
|
+
provider,
|
|
29
|
+
apiFormat: "raw",
|
|
30
|
+
model: "unknown",
|
|
31
|
+
systemTokens: 0,
|
|
32
|
+
toolsTokens: 0,
|
|
33
|
+
messagesTokens: estimateTokens(responseBody),
|
|
34
|
+
totalTokens: estimateTokens(responseBody),
|
|
35
|
+
systemPrompts: [],
|
|
36
|
+
tools: [],
|
|
37
|
+
messages: [
|
|
38
|
+
{
|
|
39
|
+
role: "raw",
|
|
40
|
+
content: responseBody.substring(0, 2000),
|
|
41
|
+
tokens: estimateTokens(responseBody),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Parse the response
|
|
47
|
+
let responseData;
|
|
48
|
+
if (capture.responseIsStreaming) {
|
|
49
|
+
responseData = { streaming: true, chunks: responseBody };
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
try {
|
|
53
|
+
responseData = JSON.parse(responseBody);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
responseData = { raw: responseBody };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Build request metadata
|
|
60
|
+
const meta = {
|
|
61
|
+
httpStatus: capture.responseStatus,
|
|
62
|
+
timings: {
|
|
63
|
+
...capture.timings,
|
|
64
|
+
tokens_per_second: null,
|
|
65
|
+
},
|
|
66
|
+
requestBytes: capture.requestBytes,
|
|
67
|
+
responseBytes: capture.responseBytes,
|
|
68
|
+
targetUrl: capture.targetUrl,
|
|
69
|
+
requestHeaders: capture.requestHeaders,
|
|
70
|
+
responseHeaders: capture.responseHeaders,
|
|
71
|
+
};
|
|
72
|
+
// Feed into the store (which handles fingerprinting, scoring, etc.)
|
|
73
|
+
store.storeRequest(contextInfo, responseData, capture.source, requestBody ?? undefined, meta, capture.requestHeaders);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=ingest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../src/analysis/ingest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAK9D;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,KAAY,EAAE,OAAoB;IAC9D,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IAEnE,0CAA0C;IAC1C,IAAI,WAAwB,CAAC;IAC7B,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,IAAI,GAAG,EAAE,GAAG,WAAW,EAAE,CAAC;QAChC,oDAAoD;QACpD,IAAI,SAAS,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;YAC5D,IAAI,UAAU;gBAAE,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;QACD,WAAW,GAAG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IAC5D,CAAC;SAAM,CAAC;QACN,6CAA6C;QAC7C,WAAW,GAAG;YACZ,QAAQ;YACR,SAAS,EAAE,KAAK;YAChB,KAAK,EAAE,SAAS;YAChB,YAAY,EAAE,CAAC;YACf,WAAW,EAAE,CAAC;YACd,cAAc,EAAE,cAAc,CAAC,YAAY,CAAC;YAC5C,WAAW,EAAE,cAAc,CAAC,YAAY,CAAC;YACzC,aAAa,EAAE,EAAE;YACjB,KAAK,EAAE,EAAE;YACT,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,KAAK;oBACX,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC;oBACxC,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC;iBACrC;aACF;SACF,CAAC;IACJ,CAAC;IAED,qBAAqB;IACrB,IAAI,YAA0B,CAAC;IAC/B,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;QAChC,YAAY,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAC3D,CAAC;SAAM,CAAC;QACN,IAAI,CAAC;YACH,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,YAAY,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC;QACvC,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,MAAM,IAAI,GAAgB;QACxB,UAAU,EAAE,OAAO,CAAC,cAAc;QAClC,OAAO,EAAE;YACP,GAAG,OAAO,CAAC,OAAO;YAClB,iBAAiB,EAAE,IAAI;SACxB;QACD,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,eAAe,EAAE,OAAO,CAAC,eAAe;KACzC,CAAC;IAEF,oEAAoE;IACpE,KAAK,CAAC,YAAY,CAChB,WAAW,EACX,YAAY,EACZ,OAAO,CAAC,MAAM,EACd,WAAW,IAAI,SAAS,EACxB,IAAI,EACJ,OAAO,CAAC,cAAc,CACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Context Lens Analysis Server.
|
|
4
|
+
*
|
|
5
|
+
* Watches the capture directory for new files from the proxy,
|
|
6
|
+
* processes them through the analysis pipeline, and serves
|
|
7
|
+
* the Web UI and API.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/analysis/server.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Context Lens Analysis Server.
|
|
4
|
+
*
|
|
5
|
+
* Watches the capture directory for new files from the proxy,
|
|
6
|
+
* processes them through the analysis pipeline, and serves
|
|
7
|
+
* the Web UI and API.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { serve } from "@hono/node-server";
|
|
14
|
+
import { Store } from "../server/store.js";
|
|
15
|
+
import { createApp, loadHtmlUI } from "../server/webui.js";
|
|
16
|
+
import { ingestCapture } from "./ingest.js";
|
|
17
|
+
import { CaptureWatcher } from "./watcher.js";
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
// --- Config ---
|
|
21
|
+
const bindHost = process.env.CONTEXT_LENS_BIND_HOST || "127.0.0.1";
|
|
22
|
+
const port = parseInt(process.env.CONTEXT_LENS_ANALYSIS_PORT || "4041", 10);
|
|
23
|
+
const captureDir = process.env.CONTEXT_LENS_CAPTURE_DIR ||
|
|
24
|
+
path.join(homedir(), ".context-lens", "captures");
|
|
25
|
+
const privacyEnv = (process.env.CONTEXT_LENS_PRIVACY || "standard").toLowerCase();
|
|
26
|
+
const privacy = privacyEnv === "minimal" || privacyEnv === "full" ? privacyEnv : "standard";
|
|
27
|
+
// Data directory: check for explicit env, then legacy location, then new default.
|
|
28
|
+
// Pre-split installs stored data in <project>/data/ next to dist/.
|
|
29
|
+
function resolveDataDir() {
|
|
30
|
+
if (process.env.CONTEXT_LENS_DATA_DIR)
|
|
31
|
+
return process.env.CONTEXT_LENS_DATA_DIR;
|
|
32
|
+
// Legacy location: <project>/data/ (sibling of dist/)
|
|
33
|
+
const legacyDir = path.resolve(__dirname, "..", "..", "data");
|
|
34
|
+
const legacyState = path.join(legacyDir, "state.jsonl");
|
|
35
|
+
if (fs.existsSync(legacyState)) {
|
|
36
|
+
console.log(`📦 Found existing data at legacy location: ${legacyDir}`);
|
|
37
|
+
return legacyDir;
|
|
38
|
+
}
|
|
39
|
+
return path.join(homedir(), ".context-lens", "data");
|
|
40
|
+
}
|
|
41
|
+
const dataDir = resolveDataDir();
|
|
42
|
+
const maxSessions = 200;
|
|
43
|
+
const maxCompactMessages = 60;
|
|
44
|
+
// --- Setup ---
|
|
45
|
+
const store = new Store({
|
|
46
|
+
dataDir,
|
|
47
|
+
stateFile: path.join(dataDir, "state.jsonl"),
|
|
48
|
+
maxSessions,
|
|
49
|
+
maxCompactMessages,
|
|
50
|
+
privacy,
|
|
51
|
+
});
|
|
52
|
+
store.loadState();
|
|
53
|
+
// --- Capture watcher ---
|
|
54
|
+
const isUtilityEndpoint = (capturePath) => /\/count_tokens\b|:countTokens\b|:loadCodeAssist\b|:retrieveUserQuota\b|:listExperiments\b|:onboardUser\b|:fetchAdminControls\b|:recordCodeAssistMetrics\b/.test(capturePath);
|
|
55
|
+
const watcher = new CaptureWatcher({
|
|
56
|
+
captureDir,
|
|
57
|
+
onCapture: (capture, filename) => {
|
|
58
|
+
// Skip utility endpoints
|
|
59
|
+
if (isUtilityEndpoint(capture.path))
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
ingestCapture(store, capture);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error(`Ingest error (${filename}):`, err instanceof Error ? err.message : String(err));
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
deleteAfterProcessing: true,
|
|
69
|
+
});
|
|
70
|
+
watcher.start();
|
|
71
|
+
// --- Web UI server ---
|
|
72
|
+
const projectDistDir = path.resolve(__dirname, "..");
|
|
73
|
+
const htmlUI = loadHtmlUI();
|
|
74
|
+
const app = createApp(store, htmlUI, projectDistDir);
|
|
75
|
+
const server = serve({ fetch: app.fetch, hostname: bindHost, port }, (info) => {
|
|
76
|
+
console.log(`🌐 Context Lens Analysis running on http://${info.address}:${info.port}`);
|
|
77
|
+
console.log(`📁 Watching captures → ${captureDir}`);
|
|
78
|
+
console.log(`💾 Data → ${dataDir}`);
|
|
79
|
+
});
|
|
80
|
+
server.on("error", (err) => {
|
|
81
|
+
if (err.code === "EADDRINUSE") {
|
|
82
|
+
console.log(`🌐 Context Lens Analysis already running on port ${port}`);
|
|
83
|
+
watcher.stop();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
});
|
|
88
|
+
// --- Graceful shutdown ---
|
|
89
|
+
function shutdown() {
|
|
90
|
+
watcher.stop();
|
|
91
|
+
server.close();
|
|
92
|
+
}
|
|
93
|
+
process.on("SIGINT", shutdown);
|
|
94
|
+
process.on("SIGTERM", shutdown);
|
|
95
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/analysis/server.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,iBAAiB;AAEjB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,WAAW,CAAC;AACnE,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;AAE5E,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,CAAC,wBAAwB;IACpC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;AAEpD,MAAM,UAAU,GAAG,CACjB,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,UAAU,CAC/C,CAAC,WAAW,EAAE,CAAC;AAChB,MAAM,OAAO,GACX,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;AAE9E,kFAAkF;AAClF,mEAAmE;AACnE,SAAS,cAAc;IACrB,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB;QACnC,OAAO,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAE3C,sDAAsD;IACtD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IACxD,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,8CAA8C,SAAS,EAAE,CAAC,CAAC;QACvE,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;AAEjC,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAE9B,gBAAgB;AAEhB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;IACtB,OAAO;IACP,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC;IAC5C,WAAW;IACX,kBAAkB;IAClB,OAAO;CACR,CAAC,CAAC;AAEH,KAAK,CAAC,SAAS,EAAE,CAAC;AAElB,0BAA0B;AAE1B,MAAM,iBAAiB,GAAG,CAAC,WAAmB,EAAW,EAAE,CACzD,2JAA2J,CAAC,IAAI,CAC9J,WAAW,CACZ,CAAC;AAEJ,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC;IACjC,UAAU;IACV,SAAS,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC/B,yBAAyB;QACzB,IAAI,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO;QAE5C,IAAI,CAAC;YACH,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CACX,iBAAiB,QAAQ,IAAI,EAC7B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CACjD,CAAC;QACJ,CAAC;IACH,CAAC;IACD,qBAAqB,EAAE,IAAI;CAC5B,CAAC,CAAC;AAEH,OAAO,CAAC,KAAK,EAAE,CAAC;AAEhB,wBAAwB;AAExB,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AACrD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;AAC5B,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC;AAErD,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;IAC5E,OAAO,CAAC,GAAG,CACT,8CAA8C,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,CAC1E,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,0BAA0B,UAAU,EAAE,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,aAAa,OAAO,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;IAChD,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,oDAAoD,IAAI,EAAE,CAAC,CAAC;QACxE,OAAO,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,GAAG,CAAC;AACZ,CAAC,CAAC,CAAC;AAEH,4BAA4B;AAE5B,SAAS,QAAQ;IACf,OAAO,CAAC,IAAI,EAAE,CAAC;IACf,MAAM,CAAC,KAAK,EAAE,CAAC;AACjB,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture directory watcher.
|
|
3
|
+
*
|
|
4
|
+
* Watches for new capture files written by the proxy, reads them,
|
|
5
|
+
* and feeds them into the analysis pipeline (Store).
|
|
6
|
+
*
|
|
7
|
+
* On startup, replays all existing capture files in sorted order.
|
|
8
|
+
* Then watches for new files via fs.watch. Ignores .tmp files
|
|
9
|
+
* (the proxy writes atomically: .tmp then rename).
|
|
10
|
+
*/
|
|
11
|
+
import type { CaptureData } from "../proxy/capture.js";
|
|
12
|
+
export type CaptureHandler = (capture: CaptureData, filename: string) => void;
|
|
13
|
+
export interface CaptureWatcherOptions {
|
|
14
|
+
captureDir: string;
|
|
15
|
+
onCapture: CaptureHandler;
|
|
16
|
+
/** Delete capture files after successful processing (default: true) */
|
|
17
|
+
deleteAfterProcessing?: boolean;
|
|
18
|
+
/** Poll interval in ms when fs.watch is unreliable (default: 0 = disabled) */
|
|
19
|
+
pollInterval?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class CaptureWatcher {
|
|
22
|
+
private readonly captureDir;
|
|
23
|
+
private readonly onCapture;
|
|
24
|
+
private readonly deleteAfterProcessing;
|
|
25
|
+
private readonly pollInterval;
|
|
26
|
+
private readonly processed;
|
|
27
|
+
private readonly processing;
|
|
28
|
+
private watcher;
|
|
29
|
+
private pollTimer;
|
|
30
|
+
private running;
|
|
31
|
+
constructor(opts: CaptureWatcherOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Start watching. Replays existing captures first, then watches for new ones.
|
|
34
|
+
*/
|
|
35
|
+
start(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Stop watching.
|
|
38
|
+
*/
|
|
39
|
+
stop(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Process all existing capture files in sorted order.
|
|
42
|
+
*/
|
|
43
|
+
private replayExisting;
|
|
44
|
+
/**
|
|
45
|
+
* Start fs.watch on the capture directory, with optional polling fallback.
|
|
46
|
+
*/
|
|
47
|
+
private startWatch;
|
|
48
|
+
private startPolling;
|
|
49
|
+
private scanForNew;
|
|
50
|
+
/**
|
|
51
|
+
* Read and process a single capture file.
|
|
52
|
+
*/
|
|
53
|
+
private processFile;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/analysis/watcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAE9E,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,uEAAuE;IACvE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAC3C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAU;IAChD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,qBAAqB;IAOvC;;OAEG;IACH,KAAK,IAAI,IAAI;IAWb;;OAEG;IACH,IAAI,IAAI,IAAI;IAYZ;;OAEG;IACH,OAAO,CAAC,cAAc;IAiBtB;;OAEG;IACH,OAAO,CAAC,UAAU;IAkClB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,UAAU;IAsBlB;;OAEG;IACH,OAAO,CAAC,WAAW;CA4BpB"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture directory watcher.
|
|
3
|
+
*
|
|
4
|
+
* Watches for new capture files written by the proxy, reads them,
|
|
5
|
+
* and feeds them into the analysis pipeline (Store).
|
|
6
|
+
*
|
|
7
|
+
* On startup, replays all existing capture files in sorted order.
|
|
8
|
+
* Then watches for new files via fs.watch. Ignores .tmp files
|
|
9
|
+
* (the proxy writes atomically: .tmp then rename).
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
export class CaptureWatcher {
|
|
14
|
+
captureDir;
|
|
15
|
+
onCapture;
|
|
16
|
+
deleteAfterProcessing;
|
|
17
|
+
pollInterval;
|
|
18
|
+
processed = new Set();
|
|
19
|
+
processing = new Set();
|
|
20
|
+
watcher = null;
|
|
21
|
+
pollTimer = null;
|
|
22
|
+
running = false;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.captureDir = opts.captureDir;
|
|
25
|
+
this.onCapture = opts.onCapture;
|
|
26
|
+
this.deleteAfterProcessing = opts.deleteAfterProcessing ?? true;
|
|
27
|
+
this.pollInterval = opts.pollInterval ?? 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Start watching. Replays existing captures first, then watches for new ones.
|
|
31
|
+
*/
|
|
32
|
+
start() {
|
|
33
|
+
if (this.running)
|
|
34
|
+
return;
|
|
35
|
+
this.running = true;
|
|
36
|
+
// Replay existing captures
|
|
37
|
+
this.replayExisting();
|
|
38
|
+
// Start watching for new files
|
|
39
|
+
this.startWatch();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Stop watching.
|
|
43
|
+
*/
|
|
44
|
+
stop() {
|
|
45
|
+
this.running = false;
|
|
46
|
+
if (this.watcher) {
|
|
47
|
+
this.watcher.close();
|
|
48
|
+
this.watcher = null;
|
|
49
|
+
}
|
|
50
|
+
if (this.pollTimer) {
|
|
51
|
+
clearInterval(this.pollTimer);
|
|
52
|
+
this.pollTimer = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Process all existing capture files in sorted order.
|
|
57
|
+
*/
|
|
58
|
+
replayExisting() {
|
|
59
|
+
if (!fs.existsSync(this.captureDir))
|
|
60
|
+
return;
|
|
61
|
+
const files = fs
|
|
62
|
+
.readdirSync(this.captureDir)
|
|
63
|
+
.filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"))
|
|
64
|
+
.sort();
|
|
65
|
+
for (const filename of files) {
|
|
66
|
+
this.processFile(filename);
|
|
67
|
+
}
|
|
68
|
+
if (files.length > 0) {
|
|
69
|
+
console.log(`📂 Replayed ${this.processed.size} existing captures`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Start fs.watch on the capture directory, with optional polling fallback.
|
|
74
|
+
*/
|
|
75
|
+
startWatch() {
|
|
76
|
+
// Ensure the directory exists before watching
|
|
77
|
+
if (!fs.existsSync(this.captureDir)) {
|
|
78
|
+
fs.mkdirSync(this.captureDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
this.watcher = fs.watch(this.captureDir, (eventType, filename) => {
|
|
82
|
+
if (!filename)
|
|
83
|
+
return;
|
|
84
|
+
if (!filename.endsWith(".json") || filename.endsWith(".tmp"))
|
|
85
|
+
return;
|
|
86
|
+
// Small delay to ensure rename is complete
|
|
87
|
+
setTimeout(() => this.processFile(filename), 10);
|
|
88
|
+
});
|
|
89
|
+
this.watcher.on("error", (err) => {
|
|
90
|
+
console.error("Watcher error:", err.message);
|
|
91
|
+
// Fall back to polling
|
|
92
|
+
this.watcher = null;
|
|
93
|
+
this.startPolling();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error("fs.watch failed, falling back to polling:", err instanceof Error ? err.message : String(err));
|
|
98
|
+
this.startPolling();
|
|
99
|
+
}
|
|
100
|
+
// Optional: also poll for reliability (fs.watch can miss events on some platforms)
|
|
101
|
+
if (this.pollInterval > 0) {
|
|
102
|
+
this.startPolling();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
startPolling() {
|
|
106
|
+
if (this.pollTimer)
|
|
107
|
+
return;
|
|
108
|
+
const interval = this.pollInterval > 0 ? this.pollInterval : 1000;
|
|
109
|
+
this.pollTimer = setInterval(() => {
|
|
110
|
+
if (!this.running)
|
|
111
|
+
return;
|
|
112
|
+
this.scanForNew();
|
|
113
|
+
}, interval);
|
|
114
|
+
}
|
|
115
|
+
scanForNew() {
|
|
116
|
+
if (!fs.existsSync(this.captureDir))
|
|
117
|
+
return;
|
|
118
|
+
const files = fs
|
|
119
|
+
.readdirSync(this.captureDir)
|
|
120
|
+
.filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"))
|
|
121
|
+
.sort();
|
|
122
|
+
const present = new Set(files);
|
|
123
|
+
// Keep dedupe state bounded to files that still exist.
|
|
124
|
+
// This prevents unbounded growth in long-running processes.
|
|
125
|
+
for (const filename of this.processed) {
|
|
126
|
+
if (!present.has(filename))
|
|
127
|
+
this.processed.delete(filename);
|
|
128
|
+
}
|
|
129
|
+
for (const filename of files) {
|
|
130
|
+
if (!this.processed.has(filename)) {
|
|
131
|
+
this.processFile(filename);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Read and process a single capture file.
|
|
137
|
+
*/
|
|
138
|
+
processFile(filename) {
|
|
139
|
+
if (this.processed.has(filename) || this.processing.has(filename))
|
|
140
|
+
return;
|
|
141
|
+
const filePath = join(this.captureDir, filename);
|
|
142
|
+
this.processing.add(filename);
|
|
143
|
+
try {
|
|
144
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
145
|
+
const capture = JSON.parse(content);
|
|
146
|
+
this.processed.add(filename);
|
|
147
|
+
this.onCapture(capture, filename);
|
|
148
|
+
if (this.deleteAfterProcessing) {
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(filePath);
|
|
151
|
+
this.processed.delete(filename);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* file may already be gone */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// File might still be being written, or it's corrupt
|
|
160
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
161
|
+
if (msg.includes("ENOENT"))
|
|
162
|
+
return; // File disappeared, ignore
|
|
163
|
+
console.error(`Capture read error (${filename}):`, msg);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
this.processing.delete(filename);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/analysis/watcher.ts"],"names":[],"mappings":"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"}
|
package/dist/cli-utils.d.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { ToolConfig } from "./types.js";
|
|
2
|
+
export interface ParsedCliArgs {
|
|
3
|
+
showHelp: boolean;
|
|
4
|
+
showVersion: boolean;
|
|
5
|
+
noOpen: boolean;
|
|
6
|
+
noUi: boolean;
|
|
7
|
+
noUpdateCheck: boolean;
|
|
8
|
+
privacyLevel?: string;
|
|
9
|
+
commandName?: string;
|
|
10
|
+
commandArguments: string[];
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
2
13
|
export declare function getToolConfig(toolName: string): ToolConfig;
|
|
14
|
+
export declare function resolveCommandAlias(commandName: string): string;
|
|
15
|
+
export declare function parseCliArgs(args: string[]): ParsedCliArgs;
|
|
16
|
+
export declare function formatHelpText(): string;
|
|
3
17
|
export declare const CLI_CONSTANTS: {
|
|
4
18
|
readonly PROXY_URL: "http://localhost:4040";
|
|
5
19
|
readonly MITM_PORT: 8080;
|
|
6
20
|
readonly MITM_PROXY_URL: "http://localhost:8080";
|
|
7
21
|
readonly PI_AGENT_DIR_PREFIX: "/tmp/context-lens-pi-agent-";
|
|
22
|
+
readonly COMMAND_ALIASES: Record<string, string>;
|
|
23
|
+
readonly KNOWN_PRIVACY_LEVELS: readonly ["minimal", "standard", "full"];
|
|
8
24
|
readonly MITM_ADDON_PATH: string;
|
|
9
25
|
};
|
|
10
26
|
//# sourceMappingURL=cli-utils.d.ts.map
|
package/dist/cli-utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-utils.d.ts","sourceRoot":"","sources":["../src/cli-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cli-utils.d.ts","sourceRoot":"","sources":["../src/cli-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAoB7C,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2DD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAY1D;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAiH1D;AAED,wBAAgB,cAAc,IAAI,MAAM,CAqDvC;AAGD,eAAO,MAAM,aAAa;;;;;;;;CAShB,CAAC"}
|