@arvoretech/runtime-lens-mcp 1.0.0 → 1.2.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/.vscodeignore DELETED
@@ -1,21 +0,0 @@
1
- src/**
2
- extension/**/*.ts
3
- agent/**/*.ts
4
- node_modules/**
5
- coverage/**
6
- examples/**
7
- .gitignore
8
- *.tsbuildinfo
9
- tsconfig.json
10
- **/tsconfig.json
11
- vitest.config.ts
12
- eslint.config.js
13
- pnpm-lock.yaml
14
- **/*.map
15
- dist/index.d.ts
16
- dist/log-collector.d.ts
17
- dist/process-inspector.d.ts
18
- dist/runtime-interceptor.d.ts
19
- dist/server.d.ts
20
- dist/types.d.ts
21
- dist/agent/index.debug.js
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "CommonJS",
5
- "moduleResolution": "Node",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "declaration": false,
11
- "sourceMap": true,
12
- "outDir": "../dist/agent",
13
- "rootDir": "."
14
- },
15
- "include": ["./**/*.ts"],
16
- "exclude": ["node_modules"]
17
- }
package/eslint.config.js DELETED
@@ -1,41 +0,0 @@
1
- import eslint from '@eslint/js';
2
- import tseslint from '@typescript-eslint/eslint-plugin';
3
- import tsparser from '@typescript-eslint/parser';
4
-
5
- export default [
6
- eslint.configs.recommended,
7
- {
8
- files: ['src/**/*.ts'],
9
- languageOptions: {
10
- parser: tsparser,
11
- parserOptions: {
12
- ecmaVersion: 2022,
13
- sourceType: 'module',
14
- },
15
- globals: {
16
- console: 'readonly',
17
- process: 'readonly',
18
- Buffer: 'readonly',
19
- BufferEncoding: 'readonly',
20
- __dirname: 'readonly',
21
- __filename: 'readonly',
22
- global: 'readonly',
23
- setTimeout: 'readonly',
24
- clearTimeout: 'readonly',
25
- setInterval: 'readonly',
26
- clearInterval: 'readonly',
27
- require: 'readonly',
28
- },
29
- },
30
- plugins: {
31
- '@typescript-eslint': tseslint,
32
- },
33
- rules: {
34
- ...tseslint.configs.recommended.rules,
35
- 'no-redeclare': 'off',
36
- '@typescript-eslint/no-unused-vars': 'error',
37
- '@typescript-eslint/explicit-function-return-type': 'warn',
38
- '@typescript-eslint/no-explicit-any': 'warn',
39
- },
40
- },
41
- ];
@@ -1,144 +0,0 @@
1
- import * as vscode from "vscode";
2
-
3
- interface InlineValue {
4
- file: string;
5
- line: number;
6
- column: number;
7
- text: string;
8
- type: "log" | "error" | "warn" | "info" | "debug" | "result";
9
- timestamp: number;
10
- }
11
-
12
- const TYPE_COLORS: Record<string, string> = {
13
- log: "#6A9955",
14
- info: "#569CD6",
15
- warn: "#CE9178",
16
- error: "#F44747",
17
- debug: "#9CDCFE",
18
- result: "#DCDCAA",
19
- };
20
-
21
- export class InlineDecorator {
22
- private decorationTypes: Map<string, vscode.TextEditorDecorationType> = new Map();
23
- private values: Map<string, InlineValue[]> = new Map();
24
- private disposables: vscode.Disposable[] = [];
25
-
26
- activate(context: vscode.ExtensionContext): void {
27
- this.disposables.push(
28
- vscode.window.onDidChangeActiveTextEditor(() => this.refresh()),
29
- vscode.workspace.onDidChangeTextDocument((e) => {
30
- if (e.document === vscode.window.activeTextEditor?.document) {
31
- this.refresh();
32
- }
33
- })
34
- );
35
- context.subscriptions.push(...this.disposables);
36
- }
37
-
38
- addValue(value: InlineValue): void {
39
- const key = value.file;
40
- if (!this.values.has(key)) {
41
- this.values.set(key, []);
42
- }
43
-
44
- const existing = this.values.get(key)!;
45
- const idx = existing.findIndex((v) => v.line === value.line);
46
- if (idx >= 0) {
47
- existing[idx] = value;
48
- } else {
49
- existing.push(value);
50
- }
51
-
52
- if (existing.length > 1000) {
53
- existing.splice(0, existing.length - 1000);
54
- }
55
-
56
- this.refresh();
57
- }
58
-
59
- clearFile(file: string): void {
60
- this.values.delete(file);
61
- this.refresh();
62
- }
63
-
64
- clearAll(): void {
65
- this.values.clear();
66
- this.clearDecorations();
67
- }
68
-
69
- refresh(): void {
70
- const editor = vscode.window.activeTextEditor;
71
- if (!editor) return;
72
-
73
- this.clearDecorations();
74
-
75
- const filePath = editor.document.uri.fsPath;
76
- const fileValues = this.values.get(filePath);
77
- if (!fileValues || fileValues.length === 0) return;
78
-
79
- const decorationsByType = new Map<string, vscode.DecorationOptions[]>();
80
-
81
- for (const value of fileValues) {
82
- const lineIndex = value.line - 1;
83
- if (lineIndex < 0 || lineIndex >= editor.document.lineCount) continue;
84
-
85
- const lineText = editor.document.lineAt(lineIndex).text;
86
- const range = new vscode.Range(
87
- new vscode.Position(lineIndex, lineText.length),
88
- new vscode.Position(lineIndex, lineText.length)
89
- );
90
-
91
- const truncated = value.text.length > 80
92
- ? value.text.slice(0, 80) + "…"
93
- : value.text;
94
-
95
- const decoration: vscode.DecorationOptions = {
96
- range,
97
- hoverMessage: new vscode.MarkdownString(
98
- `**Runtime Lens** (${value.type})\n\n\`\`\`\n${value.text}\n\`\`\`\n\n*${new Date(value.timestamp).toLocaleTimeString()}*`
99
- ),
100
- renderOptions: {
101
- after: {
102
- contentText: ` // → ${truncated}`,
103
- color: TYPE_COLORS[value.type] || "#6A9955",
104
- fontStyle: "italic",
105
- margin: "0 0 0 1em",
106
- },
107
- },
108
- };
109
-
110
- const typeKey = value.type;
111
- if (!decorationsByType.has(typeKey)) {
112
- decorationsByType.set(typeKey, []);
113
- }
114
- decorationsByType.get(typeKey)!.push(decoration);
115
- }
116
-
117
- for (const [typeKey, decorations] of decorationsByType) {
118
- const decorationType = vscode.window.createTextEditorDecorationType({
119
- after: {
120
- color: TYPE_COLORS[typeKey] || "#6A9955",
121
- fontStyle: "italic",
122
- },
123
- isWholeLine: false,
124
- });
125
-
126
- this.decorationTypes.set(typeKey, decorationType);
127
- editor.setDecorations(decorationType, decorations);
128
- }
129
- }
130
-
131
- private clearDecorations(): void {
132
- for (const decorationType of this.decorationTypes.values()) {
133
- decorationType.dispose();
134
- }
135
- this.decorationTypes.clear();
136
- }
137
-
138
- dispose(): void {
139
- this.clearDecorations();
140
- for (const d of this.disposables) {
141
- d.dispose();
142
- }
143
- }
144
- }
@@ -1,98 +0,0 @@
1
- import * as vscode from "vscode";
2
- import { join } from "node:path";
3
- import { InlineDecorator } from "./decorator.js";
4
- import { RuntimeBridge } from "./runtime-bridge.js";
5
-
6
- let decorator: InlineDecorator;
7
- let bridge: RuntimeBridge;
8
- let statusBarItem: vscode.StatusBarItem;
9
-
10
- export function activate(context: vscode.ExtensionContext): void {
11
- const outputChannel = vscode.window.createOutputChannel("Runtime Lens");
12
- const agentPath = join(context.extensionPath, "dist", "agent", "index.js");
13
-
14
- decorator = new InlineDecorator();
15
- decorator.activate(context);
16
-
17
- bridge = new RuntimeBridge(decorator, outputChannel);
18
-
19
- statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
20
- statusBarItem.text = "$(eye) Lens";
21
- statusBarItem.tooltip = "Runtime Lens - Click to toggle";
22
- statusBarItem.command = "runtimeLens.toggle";
23
- statusBarItem.show();
24
- context.subscriptions.push(statusBarItem);
25
-
26
- const port = vscode.workspace.getConfiguration("runtimeLens").get("port", 9500);
27
-
28
- context.subscriptions.push(
29
- vscode.commands.registerCommand("runtimeLens.start", () => {
30
- bridge.connect();
31
- updateStatusBar(true);
32
- vscode.window.showInformationMessage("Runtime Lens: Listening for logs...");
33
- }),
34
-
35
- vscode.commands.registerCommand("runtimeLens.stop", () => {
36
- bridge.disconnect();
37
- decorator.clearAll();
38
- updateStatusBar(false);
39
- vscode.window.showInformationMessage("Runtime Lens: Stopped");
40
- }),
41
-
42
- vscode.commands.registerCommand("runtimeLens.toggle", () => {
43
- if (bridge.isConnected()) {
44
- vscode.commands.executeCommand("runtimeLens.stop");
45
- } else {
46
- vscode.commands.executeCommand("runtimeLens.start");
47
- }
48
- }),
49
-
50
- vscode.commands.registerCommand("runtimeLens.clear", () => {
51
- decorator.clearAll();
52
- vscode.window.showInformationMessage("Runtime Lens: Cleared");
53
- }),
54
-
55
- vscode.commands.registerCommand("runtimeLens.connect", () => {
56
- bridge.connect();
57
- updateStatusBar(true);
58
- }),
59
-
60
- vscode.commands.registerCommand("runtimeLens.showOutput", () => {
61
- outputChannel.show();
62
- }),
63
-
64
- vscode.commands.registerCommand("runtimeLens.injectEnv", () => {
65
- const terminal = vscode.window.activeTerminal;
66
- if (!terminal) {
67
- vscode.window.showWarningMessage("Runtime Lens: No active terminal");
68
- return;
69
- }
70
- terminal.sendText(`export NODE_OPTIONS="--require ${agentPath}"`, true);
71
- terminal.sendText(`export RUNTIME_LENS_PORT="${port}"`, true);
72
- vscode.window.showInformationMessage("Runtime Lens: Environment injected. Run your app now.");
73
- })
74
- );
75
-
76
- const autoStart = vscode.workspace.getConfiguration("runtimeLens").get("autoStart", false);
77
- if (autoStart) {
78
- vscode.commands.executeCommand("runtimeLens.start");
79
- }
80
-
81
- outputChannel.appendLine(`[runtime-lens] Extension activated`);
82
- outputChannel.appendLine(`[runtime-lens] Agent path: ${agentPath}`);
83
- }
84
-
85
- function updateStatusBar(active: boolean): void {
86
- if (active) {
87
- statusBarItem.text = "$(eye) Lens ●";
88
- statusBarItem.backgroundColor = new vscode.ThemeColor("statusBarItem.warningBackground");
89
- } else {
90
- statusBarItem.text = "$(eye) Lens";
91
- statusBarItem.backgroundColor = undefined;
92
- }
93
- }
94
-
95
- export function deactivate(): void {
96
- bridge?.disconnect();
97
- decorator?.dispose();
98
- }
@@ -1,206 +0,0 @@
1
- import * as vscode from "vscode";
2
- import { request } from "node:http";
3
- import { randomBytes } from "node:crypto";
4
- import type { Socket } from "node:net";
5
- import { InlineDecorator } from "./decorator.js";
6
-
7
- interface AgentMessage {
8
- type: "log" | "error" | "warn" | "info" | "debug" | "result";
9
- file: string;
10
- line: number;
11
- column: number;
12
- values: string[];
13
- timestamp: number;
14
- expression?: string;
15
- }
16
-
17
- export class RuntimeBridge {
18
- private socket: Socket | null = null;
19
- private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
20
- private readonly decorator: InlineDecorator;
21
- private readonly port: number;
22
- private connected = false;
23
- private readonly outputChannel: vscode.OutputChannel;
24
- private dataBuffer: Buffer = Buffer.alloc(0);
25
-
26
- constructor(decorator: InlineDecorator, outputChannel: vscode.OutputChannel) {
27
- this.decorator = decorator;
28
- this.port = vscode.workspace.getConfiguration("runtimeLens").get("port", 9500);
29
- this.outputChannel = outputChannel;
30
- }
31
-
32
- connect(): void {
33
- if (this.socket) return;
34
-
35
- const key = randomBytes(16).toString("base64");
36
-
37
- const req = request({
38
- hostname: "localhost",
39
- port: this.port,
40
- path: "/",
41
- method: "GET",
42
- headers: {
43
- Upgrade: "websocket",
44
- Connection: "Upgrade",
45
- "Sec-WebSocket-Key": key,
46
- "Sec-WebSocket-Version": "13",
47
- },
48
- });
49
-
50
- req.on("upgrade", (_res, socket) => {
51
- this.socket = socket;
52
- this.connected = true;
53
- this.dataBuffer = Buffer.alloc(0);
54
- this.outputChannel.appendLine(`[runtime-lens] Connected to agent on port ${this.port}`);
55
- vscode.window.setStatusBarMessage("$(eye) Runtime Lens: Connected", 3000);
56
-
57
- socket.on("data", (data: Buffer) => {
58
- this.dataBuffer = Buffer.concat([this.dataBuffer, data]);
59
- this.processFrames();
60
- });
61
-
62
- socket.on("close", () => {
63
- this.connected = false;
64
- this.socket = null;
65
- this.scheduleReconnect();
66
- });
67
-
68
- socket.on("error", () => {
69
- this.connected = false;
70
- this.socket = null;
71
- });
72
- });
73
-
74
- req.on("error", () => {
75
- this.scheduleReconnect();
76
- });
77
-
78
- req.end();
79
- }
80
-
81
- disconnect(): void {
82
- if (this.reconnectTimer) {
83
- clearTimeout(this.reconnectTimer);
84
- this.reconnectTimer = null;
85
- }
86
- if (this.socket) {
87
- this.socket.end();
88
- this.socket = null;
89
- }
90
- this.connected = false;
91
- }
92
-
93
- isConnected(): boolean {
94
- return this.connected;
95
- }
96
-
97
- evaluate(expression: string, file: string, line: number): void {
98
- if (!this.socket || !this.connected) return;
99
- const payload = JSON.stringify({ type: "eval", expression, file, line });
100
- this.sendFrame(payload);
101
- }
102
-
103
- private processFrames(): void {
104
- while (this.dataBuffer.length >= 2) {
105
- const firstByte = this.dataBuffer[0];
106
- const secondByte = this.dataBuffer[1];
107
- const isFin = (firstByte & 0x80) !== 0;
108
- const opcode = firstByte & 0x0f;
109
-
110
- if (opcode === 0x08) {
111
- this.socket?.end();
112
- return;
113
- }
114
-
115
- let payloadLen = secondByte & 0x7f;
116
- let headerLen = 2;
117
-
118
- if (payloadLen === 126) {
119
- if (this.dataBuffer.length < 4) return;
120
- payloadLen = this.dataBuffer.readUInt16BE(2);
121
- headerLen = 4;
122
- } else if (payloadLen === 127) {
123
- if (this.dataBuffer.length < 10) return;
124
- payloadLen = Number(this.dataBuffer.readBigUInt64BE(2));
125
- headerLen = 10;
126
- }
127
-
128
- const totalLen = headerLen + payloadLen;
129
- if (this.dataBuffer.length < totalLen) return;
130
-
131
- const payload = this.dataBuffer.subarray(headerLen, totalLen);
132
- this.dataBuffer = this.dataBuffer.subarray(totalLen);
133
-
134
- if (isFin && (opcode === 0x01 || opcode === 0x02)) {
135
- const text = payload.toString("utf-8");
136
- try {
137
- const msg: AgentMessage = JSON.parse(text);
138
- this.handleMessage(msg);
139
- } catch {
140
- // invalid JSON
141
- }
142
- }
143
- }
144
- }
145
-
146
- private sendFrame(text: string): void {
147
- if (!this.socket) return;
148
- const data = Buffer.from(text, "utf-8");
149
- const len = data.length;
150
- let header: Buffer;
151
-
152
- if (len < 126) {
153
- header = Buffer.alloc(2);
154
- header[0] = 0x81;
155
- header[1] = len;
156
- } else if (len < 65536) {
157
- header = Buffer.alloc(4);
158
- header[0] = 0x81;
159
- header[1] = 126;
160
- header.writeUInt16BE(len, 2);
161
- } else {
162
- header = Buffer.alloc(10);
163
- header[0] = 0x81;
164
- header[1] = 127;
165
- header.writeBigUInt64BE(BigInt(len), 2);
166
- }
167
-
168
- this.socket.write(Buffer.concat([header, data]));
169
- }
170
-
171
- private handleMessage(msg: AgentMessage): void {
172
- const workspaceFolders = vscode.workspace.workspaceFolders;
173
- if (!workspaceFolders) return;
174
-
175
- let resolvedFile = msg.file;
176
- for (const folder of workspaceFolders) {
177
- if (msg.file.startsWith(folder.uri.fsPath)) {
178
- resolvedFile = msg.file;
179
- break;
180
- }
181
- }
182
-
183
- const text = msg.values.join(", ");
184
-
185
- this.decorator.addValue({
186
- file: resolvedFile,
187
- line: msg.line,
188
- column: msg.column,
189
- text,
190
- type: msg.type,
191
- timestamp: msg.timestamp,
192
- });
193
-
194
- this.outputChannel.appendLine(
195
- `[${msg.type}] ${resolvedFile}:${msg.line} → ${text}`
196
- );
197
- }
198
-
199
- private scheduleReconnect(): void {
200
- if (this.reconnectTimer) return;
201
- this.reconnectTimer = setTimeout(() => {
202
- this.reconnectTimer = null;
203
- this.connect();
204
- }, 3000);
205
- }
206
- }
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "CommonJS",
5
- "moduleResolution": "Node",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "declaration": false,
11
- "sourceMap": true,
12
- "outDir": "../dist/extension",
13
- "rootDir": "."
14
- },
15
- "include": ["./**/*.ts"],
16
- "exclude": ["node_modules"]
17
- }
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Node",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "declaration": true,
11
- "declarationMap": true,
12
- "sourceMap": true,
13
- "outDir": "./dist",
14
- "rootDir": "./src",
15
- "resolveJsonModule": true,
16
- "allowSyntheticDefaultImports": true
17
- },
18
- "include": ["src/**/*"],
19
- "exclude": ["node_modules", "dist"]
20
- }
package/vitest.config.ts DELETED
@@ -1,13 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- environment: "node",
7
- passWithNoTests: true,
8
- coverage: {
9
- provider: "v8",
10
- reporter: ["text", "json", "html"],
11
- },
12
- },
13
- });