vscode-pi-companion 0.0.10

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.
@@ -0,0 +1,62 @@
1
+ name: Publish to VS Code Marketplace & npm
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ publish-vscode:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: write
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ persist-credentials: true
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20'
21
+ cache: 'npm'
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Install vsce
27
+ run: npm install -g @vscode/vsce
28
+
29
+ - name: Publish to VS Code Marketplace
30
+ env:
31
+ VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }}
32
+ run: |
33
+ git config --local user.name "github-actions"
34
+ git config --local user.email "github-actions@github.com"
35
+
36
+ # Package and publish with version bump
37
+ npx vsce package
38
+ vsce publish patch --pat $VSCE_TOKEN --skip-duplicate
39
+
40
+ # Push the tag created by vsce
41
+ git push --follow-tags origin main
42
+
43
+ publish-npm:
44
+ runs-on: ubuntu-latest
45
+ needs: publish-vscode
46
+ if: startsWith(github.ref, 'refs/tags/v')
47
+ permissions:
48
+ id-token: write # Required for OIDC Trusted Publishing
49
+
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+
53
+ - uses: actions/setup-node@v4
54
+ with:
55
+ node-version: '20'
56
+ cache: 'npm'
57
+
58
+ - name: Install dependencies
59
+ run: npm ci
60
+
61
+ - name: Publish to npm
62
+ run: npm publish --access public --skip-duplicate
@@ -0,0 +1,19 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: googleapis/release-please-action@v4
16
+ with:
17
+ release-type: node
18
+ package-name: vscode-pi-companion
19
+ target-branch: main
@@ -0,0 +1,17 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Run Extension",
6
+ "type": "extensionHost",
7
+ "request": "launch",
8
+ "args": [
9
+ "--extensionDevelopmentPath=${workspaceFolder}"
10
+ ],
11
+ "outFiles": [
12
+ "${workspaceFolder}/out/**/*.js"
13
+ ],
14
+ "preLaunchTask": "${defaultBuildTask}"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "type": "npm",
6
+ "script": "compile",
7
+ "problemMatcher": [],
8
+ "label": "npm: compile",
9
+ "detail": "tsc -p ./"
10
+ }
11
+ ]
12
+ }
package/.vscodeignore ADDED
@@ -0,0 +1,12 @@
1
+ src/
2
+ .vscode/
3
+ .github/
4
+ .gitignore
5
+ *.ts
6
+ *.map
7
+ !out/**/*.js
8
+ !out/**/*.d.ts
9
+ node_modules/
10
+ extensions/node_modules/
11
+ *.vsix
12
+ tsconfig.json
package/LICENSE ADDED
@@ -0,0 +1 @@
1
+ {"name":"MIT","url":"https://opensource.org/licenses/MIT"}
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # VS Code PI Companion
2
+
3
+ A VS Code extension that provides real-time IDE context (files, selection) to pi and other agents.
4
+
5
+ ## Features
6
+
7
+ - **Real-time context**: Monitors open files, cursor position, and selection
8
+ - **Instant updates**: Pushes context changes via SSE
9
+ - **Seamless integration**: Selected lines injected into LLM context automatically
10
+ - **Widget display**: Shows current file/selection in pi UI
11
+
12
+ ## Architecture
13
+
14
+ **VS Code Extension** → SSE ← **pi Extension**
15
+
16
+ - **VS Code**: Context tracking, SSE server
17
+ - **pi**: SSE client, Widget, Context injection
18
+
19
+ ## Installation
20
+
21
+ ### 1. Install pi Extension
22
+
23
+ ```bash
24
+ pi install git:github.com/ravshansbox/vscode-pi-companion
25
+ ```
26
+
27
+ Or manually:
28
+ ```bash
29
+ mkdir -p ~/.pi/agent/extensions/vscode-pi-companion
30
+ git clone git@github.com:ravshansbox/vscode-pi-companion.git ~/.pi/agent/extensions/vscode-pi-companion
31
+ cd ~/.pi/agent/extensions/vscode-pi-companion
32
+ npm install
33
+ ```
34
+
35
+ ### 2. Install VS Code Extension
36
+
37
+ The pi extension will auto-install the VS Code extension when you run `pi` in a VS Code terminal.
38
+
39
+ Alternatively, install manually:
40
+ ```bash
41
+ # Download the .vsix from releases
42
+ code --install-extension vscode-pi-companion.vsix
43
+ ```
44
+
45
+ Then reload VS Code window.
46
+
47
+ ## Usage
48
+
49
+ 1. Open a folder in VS Code
50
+ 2. Open VS Code terminal (`Ctrl+\``)
51
+ 3. Run `pi`
52
+ 4. The extension auto-connects and shows current file in widget
53
+ 5. Select code → widget updates instantly
54
+ 6. Selected lines are injected into LLM context automatically
55
+
56
+ ## Widget Display
57
+
58
+ ```
59
+ 📄 filename # no selection
60
+ 📄 filename L9 # single line
61
+ 📄 filename L9:14 # multi-line selection
62
+ ```
63
+
64
+ ## MCP Tools (for LLM)
65
+
66
+ | Tool | Description |
67
+ |------|-------------|
68
+ | `get_ide_context` | Full IDE context |
69
+ | `get_current_file` | Current file with selection |
70
+ | `list_open_files` | List all open files |
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ # Build VS Code extension
76
+ npm install
77
+ npm run compile
78
+
79
+ # Package as .vsix
80
+ npx vsce package
81
+ ```
82
+
83
+ Press F5 to run in Extension Development Host.
@@ -0,0 +1,370 @@
1
+ /**
2
+ * PI Companion Extension
3
+ *
4
+ * Connects to the VS Code extension via SSE for real-time context updates.
5
+ * No polling, instant updates, no tool calls visible.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import * as os from "os";
12
+ import * as http from "http";
13
+ import { execSync } from "child_process";
14
+ import { Type } from "@sinclair/typebox";
15
+
16
+ // Check if running in VS Code integrated terminal
17
+ function isRunningInVSCode(): boolean {
18
+ return process.env.TERM_PROGRAM === 'vscode';
19
+ }
20
+
21
+ // Install VS Code extension from marketplace
22
+ function installFromMarketplace(ctx: ExtensionContext): void {
23
+ try {
24
+ execSync('code --install-extension ravshansbox.vscode-pi-companion --force', { stdio: 'pipe' });
25
+ ctx.ui.notify("PI Companion: VS Code extension installed. Reload window to activate.", "info");
26
+ } catch {
27
+ // Silently ignore
28
+ }
29
+ }
30
+
31
+ // Check if extension is installed
32
+ function isExtensionInstalled(): boolean {
33
+ try {
34
+ const result = execSync('code --list-extensions', { encoding: 'utf8' });
35
+ return result.includes('ravshansbox.vscode-pi-companion');
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ // Install/update VS Code extension if running in VS Code terminal
42
+ async function installVSCodeExtension(ctx: ExtensionContext): Promise<void> {
43
+ if (!isRunningInVSCode()) {
44
+ return;
45
+ }
46
+
47
+ const isInstalled = isExtensionInstalled();
48
+
49
+ if (isInstalled) {
50
+ // Already installed - check for updates
51
+ const latestVersion = await getLatestVersion();
52
+ if (latestVersion) {
53
+ console.log(`[PI Companion] Update available: v${latestVersion}`);
54
+ console.log(`[PI Companion] Run: pi install git:github.com/ravshansbox/vscode-pi-companion`);
55
+ }
56
+ return;
57
+ }
58
+
59
+ // Not installed - install from marketplace
60
+ installFromMarketplace(ctx);
61
+ }
62
+
63
+ // Get latest version from GitHub
64
+ async function getLatestVersion(): Promise<string | null> {
65
+ try {
66
+ const response = await fetch('https://api.github.com/repos/ravshansbox/vscode-pi-companion/releases/latest', {
67
+ headers: { 'Accept': 'application/vnd.github.v3+json' }
68
+ });
69
+
70
+ if (!response.ok) {
71
+ return null;
72
+ }
73
+
74
+ const release = await response.json() as { tag_name?: string };
75
+ return release.tag_name?.replace('v', '') || null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ interface IdeContext {
82
+ workspaceState: {
83
+ openFiles: Array<{
84
+ path: string;
85
+ timestamp: number;
86
+ isActive?: boolean;
87
+ selectedText?: string;
88
+ cursor?: { line: number; character: number };
89
+ selectionStart?: { line: number; character: number };
90
+ selectionEnd?: { line: number; character: number };
91
+ content?: string;
92
+ }>;
93
+ isTrusted: boolean;
94
+ workspacePath: string;
95
+ };
96
+ }
97
+
98
+ let currentContext: IdeContext | undefined;
99
+ let currentCtx: ExtensionContext | undefined;
100
+ let lastWidgetKey = "";
101
+ let widgetDebounceTimer: NodeJS.Timeout | undefined;
102
+ let reconnectTimer: NodeJS.Timeout | undefined;
103
+ let sseRequest: http.ClientRequest | undefined;
104
+
105
+ function getPort(): number | undefined {
106
+ const configDir = path.join(os.tmpdir(), "pi-companion");
107
+
108
+ try {
109
+ const files = fs.readdirSync(configDir);
110
+ const connectionFiles = files
111
+ .filter((f) => f.startsWith("connection-") && f.endsWith(".json"))
112
+ .map((f) => ({
113
+ name: f,
114
+ path: path.join(configDir, f),
115
+ mtime: fs.statSync(path.join(configDir, f)).mtime.getTime(),
116
+ }))
117
+ .sort((a, b) => b.mtime - a.mtime);
118
+
119
+ if (connectionFiles.length > 0) {
120
+ const content = fs.readFileSync(connectionFiles[0].path, "utf-8");
121
+ const info = JSON.parse(content);
122
+ return info.port;
123
+ }
124
+ } catch {
125
+ // No connection files found
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ function updateWidget(): void {
131
+ if (!currentCtx) return;
132
+
133
+ if (!currentContext || !currentContext.workspaceState.openFiles.length) {
134
+ currentCtx.ui.setWidget("pi-companion", ["PI Companion: No files open"]);
135
+ return;
136
+ }
137
+
138
+ const { workspaceState } = currentContext;
139
+ const lines: string[] = [];
140
+
141
+ const activeFile = workspaceState.openFiles.find((f) => f.isActive);
142
+
143
+ if (activeFile) {
144
+ const filename = activeFile.path.split("/").pop() || activeFile.path;
145
+
146
+ // Build location info - ONLY show when text is actually selected
147
+ let location = "";
148
+ if (activeFile.selectionStart && activeFile.selectionEnd) {
149
+ const start = activeFile.selectionStart.line;
150
+ const end = activeFile.selectionEnd.line;
151
+ if (start === end) {
152
+ location = ` L${start}`;
153
+ } else {
154
+ location = ` L${start}:${end}`;
155
+ }
156
+ }
157
+ // Don't show cursor position when nothing is selected
158
+
159
+ lines.push(`📄 ${filename}${location}`);
160
+ }
161
+
162
+ const otherCount = workspaceState.openFiles.length - 1;
163
+ if (otherCount > 0) {
164
+ lines.push(` +${otherCount} more file${otherCount > 1 ? "s" : ""} open`);
165
+ }
166
+
167
+ const widgetKey = lines.join("|");
168
+
169
+ // Debounce widget updates to avoid flickering during rapid selection changes
170
+ if (widgetDebounceTimer) {
171
+ clearTimeout(widgetDebounceTimer);
172
+ }
173
+
174
+ widgetDebounceTimer = setTimeout(() => {
175
+ if (widgetKey !== lastWidgetKey) {
176
+ lastWidgetKey = widgetKey;
177
+ currentCtx!.ui.setWidget("pi-companion", lines);
178
+ }
179
+ widgetDebounceTimer = undefined;
180
+ }, 50); // Wait 50ms after last update
181
+ }
182
+
183
+ function connectSSE(): void {
184
+ const port = getPort();
185
+ if (!port) return;
186
+
187
+ // Close existing connection
188
+ if (sseRequest) {
189
+ sseRequest.destroy();
190
+ sseRequest = undefined;
191
+ }
192
+
193
+ const url = new URL(`http://127.0.0.1:${port}/context/stream`);
194
+
195
+ sseRequest = http.get({
196
+ hostname: url.hostname,
197
+ port: url.port,
198
+ path: url.pathname,
199
+ }, (res) => {
200
+ let buffer = "";
201
+
202
+ res.on("data", (chunk: string) => {
203
+ buffer += chunk;
204
+
205
+ // Process complete SSE messages
206
+ const lines = buffer.split("\n");
207
+ buffer = lines.pop() || "";
208
+
209
+ for (const line of lines) {
210
+ if (line.startsWith("data: ")) {
211
+ const data = line.slice(6);
212
+ if (data && data !== "[object Object]") {
213
+ try {
214
+ const parsed = JSON.parse(data);
215
+ currentContext = parsed;
216
+ updateWidget();
217
+ } catch {
218
+ // Ignore parse errors
219
+ }
220
+ }
221
+ }
222
+ }
223
+ });
224
+
225
+ res.on("end", () => {
226
+ scheduleReconnect();
227
+ });
228
+
229
+ res.on("error", () => {
230
+ scheduleReconnect();
231
+ });
232
+ });
233
+
234
+ sseRequest.on("error", () => {
235
+ scheduleReconnect();
236
+ });
237
+ }
238
+
239
+ function scheduleReconnect(): void {
240
+ if (reconnectTimer || !currentCtx) return;
241
+ reconnectTimer = setTimeout(() => {
242
+ reconnectTimer = undefined;
243
+ connectSSE();
244
+ }, 2000);
245
+ }
246
+
247
+ export default function (pi: ExtensionAPI) {
248
+ // Inject IDE context before each prompt
249
+ pi.on("before_agent_start", async (event, ctx) => {
250
+ if (!currentContext) return;
251
+
252
+ const activeFile = currentContext.workspaceState.openFiles.find((f) => f.isActive);
253
+ if (!activeFile) return;
254
+
255
+ let contextText = "";
256
+
257
+ if (activeFile.selectedText) {
258
+ // Inject selected text
259
+ contextText = `Current selection in ${activeFile.path}:\n\`\`\`\n${activeFile.selectedText}\n\`\`\``;
260
+ } else if (activeFile.content) {
261
+ // Inject full file content for small files, or first 100 lines
262
+ const lines = activeFile.content.split("\n");
263
+ if (lines.length <= 100) {
264
+ contextText = `Current file ${activeFile.path}:\n\`\`\`\n${activeFile.content}\n\`\`\``;
265
+ } else {
266
+ contextText = `Current file ${activeFile.path} (first 100 lines):\n\`\`\`\n${lines.slice(0, 100).join("\n")}\n\`\`\``;
267
+ }
268
+ }
269
+
270
+ if (contextText) {
271
+ return {
272
+ message: {
273
+ customType: "pi-companion",
274
+ content: contextText,
275
+ display: false, // Don't show in chat history
276
+ },
277
+ };
278
+ }
279
+ });
280
+
281
+ pi.on("session_start", async (_event, ctx) => {
282
+ currentCtx = ctx;
283
+
284
+ // Auto-install/update VS Code extension if needed
285
+ installVSCodeExtension(ctx);
286
+
287
+ const port = getPort();
288
+ if (port) {
289
+ ctx.ui.notify("PI Companion: Connected to VS Code", "info");
290
+ connectSSE();
291
+ } else {
292
+ ctx.ui.notify("PI Companion: VS Code extension not detected", "warning");
293
+ }
294
+ });
295
+
296
+ pi.on("session_shutdown", async () => {
297
+ if (reconnectTimer) {
298
+ clearTimeout(reconnectTimer);
299
+ reconnectTimer = undefined;
300
+ }
301
+ if (sseRequest) {
302
+ sseRequest.destroy();
303
+ sseRequest = undefined;
304
+ }
305
+ });
306
+
307
+ // Register tools
308
+ pi.registerTool({
309
+ name: "get_ide_context",
310
+ label: "Get IDE Context",
311
+ description: "Get the current IDE context: open files, cursor position, and selection",
312
+ parameters: Type.Object({}),
313
+ async execute() {
314
+ if (!currentContext) {
315
+ return {
316
+ content: [{ type: "text", text: "No IDE context available. Make sure the PI Companion VS Code extension is installed and running." }],
317
+ details: {},
318
+ };
319
+ }
320
+ return {
321
+ content: [{ type: "text", text: JSON.stringify(currentContext, null, 2) }],
322
+ details: {},
323
+ };
324
+ },
325
+ });
326
+
327
+ pi.registerTool({
328
+ name: "get_current_file",
329
+ label: "Get Current File",
330
+ description: "Get the content of the currently active file in the IDE",
331
+ parameters: Type.Object({}),
332
+ async execute() {
333
+ if (!currentContext) {
334
+ return { content: [{ type: "text", text: "No IDE context available" }], details: {} };
335
+ }
336
+ const activeFile = currentContext.workspaceState.openFiles.find((f) => f.isActive);
337
+ if (!activeFile) {
338
+ return { content: [{ type: "text", text: "No active file" }], details: {} };
339
+ }
340
+ return {
341
+ content: [{ type: "text", text: JSON.stringify({
342
+ path: activeFile.path,
343
+ cursor: activeFile.cursor,
344
+ selection: activeFile.selectedText,
345
+ content: activeFile.content,
346
+ }, null, 2) }],
347
+ details: {},
348
+ };
349
+ },
350
+ });
351
+
352
+ pi.registerTool({
353
+ name: "list_open_files",
354
+ label: "List Open Files",
355
+ description: "List all files currently open in the IDE",
356
+ parameters: Type.Object({}),
357
+ async execute() {
358
+ if (!currentContext) {
359
+ return { content: [{ type: "text", text: "No IDE context available" }], details: {} };
360
+ }
361
+ const files = currentContext.workspaceState.openFiles.map((f) => ({
362
+ path: f.path,
363
+ isActive: f.isActive,
364
+ cursor: f.cursor,
365
+ hasSelection: !!f.selectedText,
366
+ }));
367
+ return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }], details: {} };
368
+ },
369
+ });
370
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "vscode-pi-companion-extension",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "dependencies": {
6
+ "@mariozechner/pi-coding-agent": "*",
7
+ "@sinclair/typebox": "*"
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "vscode-pi-companion",
3
+ "displayName": "PI Companion",
4
+ "description": "Share VS Code context with pi coding agent",
5
+ "repository": "https://github.com/ravshansbox/vscode-pi-companion",
6
+ "version": "0.0.10",
7
+ "publisher": "ravshansbox",
8
+ "keywords": [
9
+ "pi-package"
10
+ ],
11
+ "engines": {
12
+ "vscode": "^1.85.0"
13
+ },
14
+ "categories": [
15
+ "AI",
16
+ "Other"
17
+ ],
18
+ "main": "./out/extension.js",
19
+ "scripts": {
20
+ "compile": "tsc -p ./",
21
+ "watch": "tsc -watch -p ./",
22
+ "package": "vsce package",
23
+ "vscode:prepublish": "npm run compile"
24
+ },
25
+ "devDependencies": {
26
+ "@types/cors": "^2.8.19",
27
+ "@types/express": "^5.0.6",
28
+ "@types/node": "^18.0.0",
29
+ "@types/vscode": "^1.85.0",
30
+ "@types/ws": "^8.18.1",
31
+ "@vscode/vsce": "^3.7.1",
32
+ "esbuild": "^0.27.4",
33
+ "typescript": "^5.0.0"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.0.0",
37
+ "cors": "^2.8.5",
38
+ "express": "^4.18.2",
39
+ "ws": "^8.16.0",
40
+ "zod": "^3.22.4"
41
+ },
42
+ "pi": {
43
+ "extensions": [
44
+ "./extensions"
45
+ ]
46
+ },
47
+ "activationEvents": [
48
+ "onStartupFinished"
49
+ ],
50
+ "contributes": {
51
+ "configuration": {
52
+ "title": "PI Companion",
53
+ "properties": {
54
+ "piCompanion.port": {
55
+ "type": "number",
56
+ "default": 8765,
57
+ "description": "Port for MCP server"
58
+ }
59
+ }
60
+ },
61
+ "commands": [
62
+ {
63
+ "command": "pi-companion.run",
64
+ "title": "PI Companion: Run PI"
65
+ },
66
+ {
67
+ "command": "pi-companion.start",
68
+ "title": "PI Companion: Start Server"
69
+ },
70
+ {
71
+ "command": "pi-companion.stop",
72
+ "title": "PI Companion: Stop Server"
73
+ },
74
+ {
75
+ "command": "pi-companion.status",
76
+ "title": "PI Companion: Show Status"
77
+ }
78
+ ],
79
+ "keybindings": [
80
+ {
81
+ "command": "pi-companion.run",
82
+ "key": "cmd+escape",
83
+ "mac": "cmd+escape",
84
+ "when": "editorTextFocus"
85
+ }
86
+ ]
87
+ }
88
+ }
package/screenshot.png ADDED
Binary file