context-lens 0.1.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/.idea/context-lens.iml +12 -0
- package/.idea/inspectionProfiles/Project_Default.xml +17 -0
- package/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- package/.idea/material_theme_project_new.xml +10 -0
- package/.idea/misc.xml +11 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/CHANGELOG.md +104 -0
- package/README.md +60 -0
- package/cli.js +323 -0
- package/package.json +19 -0
- package/server.js +1225 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="PYTHON_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$" />
|
|
5
|
+
<orderEntry type="jdk" jdkName="Python 3.9" jdkType="Python SDK" />
|
|
6
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
7
|
+
</component>
|
|
8
|
+
<component name="PyDocumentationSettings">
|
|
9
|
+
<option name="format" value="PLAIN" />
|
|
10
|
+
<option name="myDocStringFormat" value="Plain" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<component name="InspectionProjectProfileManager">
|
|
2
|
+
<profile version="1.0">
|
|
3
|
+
<option name="myName" value="Project Default" />
|
|
4
|
+
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
5
|
+
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
|
6
|
+
<option name="myValues">
|
|
7
|
+
<value>
|
|
8
|
+
<list size="1">
|
|
9
|
+
<item index="0" class="java.lang.String" itemvalue="className" />
|
|
10
|
+
</list>
|
|
11
|
+
</value>
|
|
12
|
+
</option>
|
|
13
|
+
<option name="myCustomValuesEnabled" value="true" />
|
|
14
|
+
</inspection_tool>
|
|
15
|
+
<inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" />
|
|
16
|
+
</profile>
|
|
17
|
+
</component>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="MaterialThemeProjectNewConfig">
|
|
4
|
+
<option name="metadata">
|
|
5
|
+
<MTProjectMetadataState>
|
|
6
|
+
<option name="userId" value="-6abbd721:19c326b38fc:-66b2" />
|
|
7
|
+
</MTProjectMetadataState>
|
|
8
|
+
</option>
|
|
9
|
+
</component>
|
|
10
|
+
</project>
|
package/.idea/misc.xml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="Black">
|
|
4
|
+
<option name="sdkName" value="Python 3.9" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="KubernetesApiPersistence">{}</component>
|
|
7
|
+
<component name="KubernetesApiProvider"><![CDATA[{
|
|
8
|
+
"isMigrated": true
|
|
9
|
+
}]]></component>
|
|
10
|
+
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
|
|
11
|
+
</project>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/context-lens.iml" filepath="$PROJECT_DIR$/.idea/context-lens.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/vcs.xml
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0] - 2026-02-07
|
|
4
|
+
|
|
5
|
+
### Added - CLI Wrapper with Multi-Client Support
|
|
6
|
+
|
|
7
|
+
#### Major Features
|
|
8
|
+
- **CLI Wrapper** (`cli.js`) - Launch any tool through Context Lens with automatic setup
|
|
9
|
+
- Usage: `npx context-lens <tool> [args...]`
|
|
10
|
+
- Automatically starts proxy and web UI
|
|
11
|
+
- Sets environment variables for the tool
|
|
12
|
+
- Opens browser at http://localhost:4041
|
|
13
|
+
- Full TTY/interactivity support
|
|
14
|
+
|
|
15
|
+
- **Multi-Client Support** - Run multiple tools simultaneously sharing one proxy
|
|
16
|
+
- First client starts the proxy
|
|
17
|
+
- Subsequent clients detect and attach to existing proxy
|
|
18
|
+
- Reference counting via `/tmp/context-lens.lock`
|
|
19
|
+
- Only last client to exit shuts down the proxy
|
|
20
|
+
- Each client shows attachment status and active client count
|
|
21
|
+
|
|
22
|
+
- **Source Tagging** - Identify which tool made each request
|
|
23
|
+
- Tool name encoded in base URL path: `http://localhost:4040/<tool-name>`
|
|
24
|
+
- Proxy extracts source and stores with each request
|
|
25
|
+
- Web UI displays source badge for each request
|
|
26
|
+
- Example: `[anthropic] [claude] claude-sonnet-4`
|
|
27
|
+
|
|
28
|
+
#### Technical Changes
|
|
29
|
+
- `cli.js` (new):
|
|
30
|
+
- Port detection via TCP connect
|
|
31
|
+
- Reference counting with atomic file operations
|
|
32
|
+
- Smart shutdown logic
|
|
33
|
+
- Signal handling (SIGINT/SIGTERM)
|
|
34
|
+
- Tool-specific environment variable mapping
|
|
35
|
+
|
|
36
|
+
- `server.js`:
|
|
37
|
+
- Added `extractSource()` function for URL path parsing
|
|
38
|
+
- Updated `storeRequest()` to accept source parameter
|
|
39
|
+
- Modified `handleProxy()` to extract and pass source
|
|
40
|
+
- Enhanced web UI to display source badges
|
|
41
|
+
- Added CSS styling for source tags
|
|
42
|
+
|
|
43
|
+
- `package.json`:
|
|
44
|
+
- Added `bin` field for `npx` support
|
|
45
|
+
|
|
46
|
+
#### Documentation
|
|
47
|
+
- **README.md** - Updated with CLI-first usage examples
|
|
48
|
+
- **CLI-IMPLEMENTATION.md** - Technical implementation details
|
|
49
|
+
- **MULTI-CLIENT.md** - Comprehensive multi-client guide
|
|
50
|
+
- **CHANGELOG.md** - This file
|
|
51
|
+
|
|
52
|
+
#### Testing
|
|
53
|
+
- ā
Single client mode
|
|
54
|
+
- ā
Multi-client attach/detach
|
|
55
|
+
- ā
Reference counting accuracy
|
|
56
|
+
- ā
Source tag extraction and display
|
|
57
|
+
- ā
Graceful shutdown
|
|
58
|
+
- ā
Lockfile cleanup
|
|
59
|
+
- ā
Port conflict handling
|
|
60
|
+
|
|
61
|
+
#### Known Limitations
|
|
62
|
+
- Lockfile at `/tmp/` may not work on all systems (e.g., Windows without WSL)
|
|
63
|
+
- No IPC - relies on filesystem for coordination
|
|
64
|
+
- Manual cleanup needed if clients crash without cleanup
|
|
65
|
+
- Browser auto-open uses platform detection (may fail on exotic systems)
|
|
66
|
+
|
|
67
|
+
#### Example Usage
|
|
68
|
+
|
|
69
|
+
**Single Tool:**
|
|
70
|
+
```bash
|
|
71
|
+
npx context-lens claude "Explain quantum computing"
|
|
72
|
+
# Starts proxy, opens UI, runs claude
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Multiple Tools (different terminals):**
|
|
76
|
+
```bash
|
|
77
|
+
# Terminal 1
|
|
78
|
+
npx context-lens claude "task 1"
|
|
79
|
+
# ā Starts proxy
|
|
80
|
+
|
|
81
|
+
# Terminal 2 (while T1 running)
|
|
82
|
+
npx context-lens codex "task 2"
|
|
83
|
+
# ā Attaches to proxy
|
|
84
|
+
|
|
85
|
+
# Terminal 3 (while T1+T2 running)
|
|
86
|
+
npx context-lens -- python agent.py
|
|
87
|
+
# ā Also attaches
|
|
88
|
+
|
|
89
|
+
# All requests visible in shared UI with source tags
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## [0.1.0] - 2026-02-07
|
|
95
|
+
|
|
96
|
+
### Initial Release
|
|
97
|
+
- HTTP proxy server (port 4040)
|
|
98
|
+
- Web UI dashboard (port 4041)
|
|
99
|
+
- Request capture and analysis
|
|
100
|
+
- Token estimation (char/4 heuristic)
|
|
101
|
+
- Provider detection (Anthropic/OpenAI)
|
|
102
|
+
- Context usage visualization
|
|
103
|
+
- Streaming response support
|
|
104
|
+
- Zero external dependencies
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Context Lens
|
|
2
|
+
|
|
3
|
+
See what's actually in your LLM's context window. A zero-dependency HTTP proxy that intercepts API calls and visualizes token usage in real-time.
|
|
4
|
+
|
|
5
|
+
> **Early Development** ā Expect rough edges. Contributions welcome.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx context-lens claude "your prompt"
|
|
11
|
+
npx context-lens codex "your prompt"
|
|
12
|
+
npx context-lens aider --model claude-sonnet-4
|
|
13
|
+
npx context-lens -- python my_agent.py
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
## What You Get
|
|
19
|
+
|
|
20
|
+
- **Token breakdown** ā system prompts, tools, messages with visual context bar
|
|
21
|
+
- **Conversation threading** ā groups API calls by session/conversation, shows agents and turns
|
|
22
|
+
- **Content formatting** ā collapsible system prompts, typed message blocks (text/tool_use/tool_result), per-tool schemas
|
|
23
|
+
- **Auto-detection** ā recognizes Claude Code, Codex, aider, and others by source tag or system prompt
|
|
24
|
+
- **Streaming support** ā passes through SSE chunks in real-time
|
|
25
|
+
|
|
26
|
+
## Manual Mode
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node server.js
|
|
30
|
+
# Port 4040 = proxy, port 4041 = web UI
|
|
31
|
+
|
|
32
|
+
ANTHROPIC_BASE_URL=http://localhost:4040 claude "your prompt"
|
|
33
|
+
OPENAI_BASE_URL=http://localhost:4040 codex "your prompt"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Source Tagging
|
|
37
|
+
|
|
38
|
+
Add a path prefix to tag requests by tool:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ANTHROPIC_BASE_URL=http://localhost:4040/claude claude "prompt"
|
|
42
|
+
OPENAI_BASE_URL=http://localhost:4040/aider aider "prompt"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Codex Subscription Mode
|
|
46
|
+
|
|
47
|
+
Codex with a ChatGPT subscription needs mitmproxy for HTTPS interception (Cloudflare blocks reverse proxies). The CLI handles this automatically ā just make sure `mitmdump` is installed:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pipx install mitmproxy
|
|
51
|
+
npx context-lens codex "your prompt"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Data
|
|
55
|
+
|
|
56
|
+
All captured requests are logged to `data/requests.jsonl` and kept in memory (last 100). Restart clears the in-memory state; the JSONL log persists.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const { join } = require('path');
|
|
5
|
+
const { platform } = require('os');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const net = require('net');
|
|
8
|
+
|
|
9
|
+
// Known tool config: env vars for the child process, extra CLI args, server env vars, and whether mitmproxy is needed
|
|
10
|
+
const PROXY_URL = 'http://localhost:4040';
|
|
11
|
+
const MITM_PORT = 8080;
|
|
12
|
+
const MITM_PROXY_URL = `http://localhost:${MITM_PORT}`;
|
|
13
|
+
|
|
14
|
+
const TOOL_CONFIG = {
|
|
15
|
+
'claude': {
|
|
16
|
+
childEnv: { ANTHROPIC_BASE_URL: PROXY_URL },
|
|
17
|
+
extraArgs: [],
|
|
18
|
+
serverEnv: {},
|
|
19
|
+
needsMitm: false,
|
|
20
|
+
},
|
|
21
|
+
'codex': {
|
|
22
|
+
// Codex subscription uses chatgpt.com with Cloudflare ā needs forward proxy (mitmproxy)
|
|
23
|
+
// to intercept HTTPS traffic without breaking TLS fingerprinting.
|
|
24
|
+
childEnv: {
|
|
25
|
+
https_proxy: MITM_PROXY_URL,
|
|
26
|
+
SSL_CERT_FILE: join(process.env.HOME || '', '.mitmproxy', 'mitmproxy-ca-cert.pem'),
|
|
27
|
+
},
|
|
28
|
+
extraArgs: [],
|
|
29
|
+
serverEnv: {},
|
|
30
|
+
needsMitm: true,
|
|
31
|
+
},
|
|
32
|
+
'aider': {
|
|
33
|
+
childEnv: { ANTHROPIC_BASE_URL: PROXY_URL, OPENAI_BASE_URL: PROXY_URL },
|
|
34
|
+
extraArgs: [],
|
|
35
|
+
serverEnv: {},
|
|
36
|
+
needsMitm: false,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function getToolConfig(toolName) {
|
|
41
|
+
return TOOL_CONFIG[toolName] || {
|
|
42
|
+
childEnv: { ANTHROPIC_BASE_URL: PROXY_URL, OPENAI_BASE_URL: PROXY_URL },
|
|
43
|
+
extraArgs: [],
|
|
44
|
+
serverEnv: {},
|
|
45
|
+
needsMitm: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const LOCKFILE = '/tmp/context-lens.lock';
|
|
50
|
+
|
|
51
|
+
// Parse command line arguments
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
|
|
54
|
+
if (args.length === 0) {
|
|
55
|
+
// Standalone mode: just start the proxy server
|
|
56
|
+
const serverPath = join(__dirname, 'server.js');
|
|
57
|
+
const server = spawn('node', [serverPath], { stdio: 'inherit' });
|
|
58
|
+
server.on('exit', (code) => process.exit(code || 0));
|
|
59
|
+
process.on('SIGINT', () => server.kill('SIGINT'));
|
|
60
|
+
process.on('SIGTERM', () => server.kill('SIGTERM'));
|
|
61
|
+
// Prevent early exit
|
|
62
|
+
process.stdin.resume();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Skip '--' separator if present
|
|
67
|
+
let commandArgs = args;
|
|
68
|
+
if (args[0] === '--') {
|
|
69
|
+
commandArgs = args.slice(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (commandArgs.length === 0) {
|
|
73
|
+
console.error('Error: No command specified after --');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const commandName = commandArgs[0];
|
|
78
|
+
const commandArguments = commandArgs.slice(1);
|
|
79
|
+
|
|
80
|
+
// Get tool-specific config
|
|
81
|
+
const toolConfig = getToolConfig(commandName);
|
|
82
|
+
|
|
83
|
+
// Check if proxy is already running
|
|
84
|
+
function isProxyRunning() {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const socket = net.connect({ port: 4040, host: 'localhost' }, () => {
|
|
87
|
+
socket.end();
|
|
88
|
+
resolve(true);
|
|
89
|
+
});
|
|
90
|
+
socket.on('error', () => resolve(false));
|
|
91
|
+
socket.setTimeout(1000, () => {
|
|
92
|
+
socket.destroy();
|
|
93
|
+
resolve(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Increment reference count in lockfile
|
|
99
|
+
function incrementRefCount() {
|
|
100
|
+
try {
|
|
101
|
+
let count = 0;
|
|
102
|
+
if (fs.existsSync(LOCKFILE)) {
|
|
103
|
+
const data = fs.readFileSync(LOCKFILE, 'utf8');
|
|
104
|
+
count = parseInt(data) || 0;
|
|
105
|
+
}
|
|
106
|
+
fs.writeFileSync(LOCKFILE, String(count + 1));
|
|
107
|
+
return count + 1;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('Warning: failed to update lockfile:', err.message);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If the proxy isn't actually running but a lockfile exists, it's stale (e.g. prior crash).
|
|
115
|
+
function clearStaleLockfile() {
|
|
116
|
+
try {
|
|
117
|
+
if (fs.existsSync(LOCKFILE)) fs.unlinkSync(LOCKFILE);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('Warning: failed to clear stale lockfile:', err.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Decrement reference count in lockfile
|
|
124
|
+
function decrementRefCount() {
|
|
125
|
+
try {
|
|
126
|
+
if (!fs.existsSync(LOCKFILE)) return 0;
|
|
127
|
+
const data = fs.readFileSync(LOCKFILE, 'utf8');
|
|
128
|
+
const count = Math.max(0, (parseInt(data) || 1) - 1);
|
|
129
|
+
if (count === 0) {
|
|
130
|
+
fs.unlinkSync(LOCKFILE);
|
|
131
|
+
} else {
|
|
132
|
+
fs.writeFileSync(LOCKFILE, String(count));
|
|
133
|
+
}
|
|
134
|
+
return count;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('Warning: failed to update lockfile:', err.message);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let serverProcess = null;
|
|
142
|
+
let mitmProcess = null;
|
|
143
|
+
let serverReady = false;
|
|
144
|
+
let mitmReady = false;
|
|
145
|
+
let childProcess = null;
|
|
146
|
+
let shouldShutdownProxy = false;
|
|
147
|
+
|
|
148
|
+
// Start proxy or attach to existing one
|
|
149
|
+
async function initializeProxy() {
|
|
150
|
+
const alreadyRunning = await isProxyRunning();
|
|
151
|
+
|
|
152
|
+
if (alreadyRunning) {
|
|
153
|
+
console.log('š Context Lens proxy already running, attaching to it...');
|
|
154
|
+
incrementRefCount();
|
|
155
|
+
serverReady = true;
|
|
156
|
+
shouldShutdownProxy = false;
|
|
157
|
+
maybeStartMitmThenChild();
|
|
158
|
+
} else {
|
|
159
|
+
console.log('š Starting Context Lens proxy and web UI...');
|
|
160
|
+
// No proxy is listening on :4040. Any existing lockfile is stale and would prevent shutdown later.
|
|
161
|
+
clearStaleLockfile();
|
|
162
|
+
incrementRefCount();
|
|
163
|
+
shouldShutdownProxy = true;
|
|
164
|
+
|
|
165
|
+
const serverPath = join(__dirname, 'server.js');
|
|
166
|
+
serverProcess = spawn('node', [serverPath], {
|
|
167
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
|
+
detached: false,
|
|
169
|
+
env: { ...toolConfig.serverEnv, ...process.env, CONTEXT_LENS_CLI: '1' },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Wait for server to be ready, then suppress output (visible in web UI at :4041)
|
|
173
|
+
serverProcess.stdout.on('data', (data) => {
|
|
174
|
+
const output = data.toString();
|
|
175
|
+
if (!serverReady) {
|
|
176
|
+
process.stderr.write(output);
|
|
177
|
+
}
|
|
178
|
+
if (output.includes('Context Lens Web UI running') && !serverReady) {
|
|
179
|
+
serverReady = true;
|
|
180
|
+
maybeStartMitmThenChild();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
serverProcess.stderr.on('data', (data) => {
|
|
185
|
+
if (!serverReady) {
|
|
186
|
+
process.stderr.write(data);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
serverProcess.on('error', (err) => {
|
|
191
|
+
console.error('Failed to start server:', err);
|
|
192
|
+
decrementRefCount();
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
serverProcess.on('exit', (code) => {
|
|
197
|
+
if (!serverReady) {
|
|
198
|
+
console.error('Server exited unexpectedly');
|
|
199
|
+
decrementRefCount();
|
|
200
|
+
process.exit(code || 1);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Open browser after a short delay (only when starting new server)
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
openBrowser('http://localhost:4041');
|
|
207
|
+
}, 1000);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
initializeProxy();
|
|
212
|
+
|
|
213
|
+
// Start mitmproxy if needed, then start the child
|
|
214
|
+
function maybeStartMitmThenChild() {
|
|
215
|
+
if (!toolConfig.needsMitm) {
|
|
216
|
+
return startChild();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const addonPath = join(__dirname, 'mitm_addon.py');
|
|
220
|
+
console.log('š Starting mitmproxy (forward proxy for HTTPS interception)...');
|
|
221
|
+
|
|
222
|
+
mitmProcess = spawn('mitmdump', ['-s', addonPath, '--quiet', '--listen-port', String(MITM_PORT)], {
|
|
223
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
mitmProcess.on('error', (err) => {
|
|
227
|
+
console.error('Failed to start mitmproxy:', err.message);
|
|
228
|
+
console.error('Install it: pipx install mitmproxy');
|
|
229
|
+
cleanup(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
mitmProcess.on('exit', (code) => {
|
|
233
|
+
if (!mitmReady) {
|
|
234
|
+
console.error('mitmproxy exited unexpectedly');
|
|
235
|
+
cleanup(code || 1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Poll until mitmproxy is accepting connections
|
|
240
|
+
const pollMitm = setInterval(() => {
|
|
241
|
+
const socket = net.connect({ port: MITM_PORT, host: 'localhost' }, () => {
|
|
242
|
+
socket.end();
|
|
243
|
+
if (!mitmReady) {
|
|
244
|
+
mitmReady = true;
|
|
245
|
+
clearInterval(pollMitm);
|
|
246
|
+
console.log(`š mitmproxy listening on port ${MITM_PORT}`);
|
|
247
|
+
startChild();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
socket.on('error', () => {}); // not ready yet
|
|
251
|
+
socket.setTimeout(500, () => socket.destroy());
|
|
252
|
+
}, 200);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Start the child command
|
|
256
|
+
function startChild() {
|
|
257
|
+
// Inject extra args (e.g. codex -c chatgpt_base_url=...) before user args
|
|
258
|
+
const allArgs = [...toolConfig.extraArgs, ...commandArguments];
|
|
259
|
+
console.log(`\nš Launching: ${commandName} ${allArgs.join(' ')}\n`);
|
|
260
|
+
|
|
261
|
+
const childEnv = {
|
|
262
|
+
...process.env,
|
|
263
|
+
...toolConfig.childEnv,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Spawn the child process with inherited stdio (interactive)
|
|
267
|
+
// No shell: true ā avoids intermediate process that breaks signal delivery
|
|
268
|
+
childProcess = spawn(commandName, allArgs, {
|
|
269
|
+
stdio: 'inherit',
|
|
270
|
+
env: childEnv,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
childProcess.on('error', (err) => {
|
|
274
|
+
console.error(`\nFailed to start ${commandName}:`, err.message);
|
|
275
|
+
cleanup(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// When the child exits (however it happens), clean up and mirror its exit code
|
|
279
|
+
childProcess.on('exit', (code, signal) => {
|
|
280
|
+
cleanup(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code || 0));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Open browser (cross-platform)
|
|
285
|
+
function openBrowser(url) {
|
|
286
|
+
const cmd = platform() === 'darwin' ? 'open'
|
|
287
|
+
: platform() === 'win32' ? 'start'
|
|
288
|
+
: 'xdg-open';
|
|
289
|
+
|
|
290
|
+
const browserProcess = spawn(cmd, [url], {
|
|
291
|
+
stdio: 'ignore',
|
|
292
|
+
detached: true,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
browserProcess.unref(); // Don't wait for browser to close
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Cleanup on exit
|
|
299
|
+
function cleanup(exitCode) {
|
|
300
|
+
if (cleanup._didRun) return;
|
|
301
|
+
cleanup._didRun = true;
|
|
302
|
+
|
|
303
|
+
const remainingRefs = decrementRefCount();
|
|
304
|
+
|
|
305
|
+
if (mitmProcess && !mitmProcess.killed) {
|
|
306
|
+
mitmProcess.kill();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (remainingRefs === 0 && shouldShutdownProxy && serverProcess && !serverProcess.killed) {
|
|
310
|
+
serverProcess.kill();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
process.exit(exitCode);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Ignore SIGINT in the parent ā let it flow to the child (claude/codex) naturally.
|
|
317
|
+
// The child handles Ctrl+C itself; when it eventually exits, cleanup runs via the 'exit' handler.
|
|
318
|
+
process.on('SIGINT', () => {});
|
|
319
|
+
|
|
320
|
+
// SIGTERM: external shutdown request ā forward to child
|
|
321
|
+
process.on('SIGTERM', () => {
|
|
322
|
+
if (childProcess && !childProcess.killed) childProcess.kill('SIGTERM');
|
|
323
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "context-lens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local HTTP proxy that intercepts LLM API calls and visualizes context windows",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"context-lens": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node server.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["llm", "proxy", "context", "anthropic", "openai", "visualization"],
|
|
13
|
+
"author": "Lars de Ridder",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/larsderidder/context-lens"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|