@pinfix/plugin 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.
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../shared/dist/index.js
4
+ var WS_PORT_DEFAULT = 24816;
5
+ function isSessionStartMessage(msg) {
6
+ if (typeof msg !== "object" || msg === null) return false;
7
+ const m = msg;
8
+ return m.type === "session:start" && typeof m.pinId === "string" && typeof m.source === "string";
9
+ }
10
+ function isChatSendMessage(msg) {
11
+ if (typeof msg !== "object" || msg === null) return false;
12
+ const m = msg;
13
+ return m.type === "chat:send" && typeof m.pinId === "string" && typeof m.content === "string";
14
+ }
15
+ function isSessionEndMessage(msg) {
16
+ if (typeof msg !== "object" || msg === null) return false;
17
+ const m = msg;
18
+ return m.type === "session:end" && typeof m.pinId === "string";
19
+ }
20
+ function isWorkspaceResetMessage(msg) {
21
+ if (typeof msg !== "object" || msg === null) return false;
22
+ const m = msg;
23
+ return m.type === "workspace:reset" && (m.prompt === void 0 || typeof m.prompt === "string");
24
+ }
25
+
26
+ // ../channel-server/src/ws-server.ts
27
+ import { WebSocketServer, WebSocket } from "ws";
28
+
29
+ // ../channel-server/src/claude-provider.ts
30
+ import { query } from "@anthropic-ai/claude-agent-sdk";
31
+ function log(msg) {
32
+ process.stderr.write(`[pinfix:claude] ${msg}
33
+ `);
34
+ }
35
+ function createClaudeSession(contextPrefix, cwd) {
36
+ let chunkCb = null;
37
+ let toolCb = null;
38
+ let doneCb = null;
39
+ let errorCb = null;
40
+ let killed = false;
41
+ let startupError = null;
42
+ const abortController = new AbortController();
43
+ let resolveNextMessage = null;
44
+ let firstMessage = true;
45
+ async function* userMessages() {
46
+ while (!killed) {
47
+ const msg = await new Promise((resolve2) => {
48
+ resolveNextMessage = resolve2;
49
+ });
50
+ if (killed) break;
51
+ yield msg;
52
+ }
53
+ }
54
+ async function startSession() {
55
+ try {
56
+ log("starting claude query...");
57
+ const stream = query({
58
+ prompt: userMessages(),
59
+ options: {
60
+ permissionMode: "auto",
61
+ includePartialMessages: true,
62
+ abortController,
63
+ ...cwd ? { cwd } : {}
64
+ }
65
+ });
66
+ for await (const message of stream) {
67
+ if (killed) break;
68
+ if (message.type === "stream_event" && message.event) {
69
+ const event = message.event;
70
+ if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
71
+ if (chunkCb) chunkCb(event.delta.text);
72
+ }
73
+ if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
74
+ const toolName = event.content_block.name;
75
+ log(`tool_use: ${toolName}`);
76
+ if (toolCb) toolCb(toolName);
77
+ }
78
+ }
79
+ if (message.type === "tool_progress") {
80
+ }
81
+ if (message.type === "result") {
82
+ log("claude query complete");
83
+ if (doneCb) doneCb();
84
+ }
85
+ }
86
+ } catch (err) {
87
+ if (!killed) {
88
+ const errorMessage = err.message || "Unknown error";
89
+ startupError = errorMessage;
90
+ log(`claude error: ${errorMessage}`);
91
+ if (errorCb) errorCb(errorMessage);
92
+ }
93
+ }
94
+ }
95
+ startSession();
96
+ function sendMessage(content) {
97
+ if (killed) return;
98
+ if (startupError) {
99
+ if (errorCb) errorCb(startupError);
100
+ return;
101
+ }
102
+ let prompt = content;
103
+ if (firstMessage) {
104
+ if (contextPrefix) {
105
+ prompt = `${contextPrefix}
106
+
107
+ ${content}`;
108
+ }
109
+ firstMessage = false;
110
+ }
111
+ log(`sending to claude: "${prompt.slice(0, 100)}"`);
112
+ if (resolveNextMessage) {
113
+ const resolve2 = resolveNextMessage;
114
+ resolveNextMessage = null;
115
+ resolve2({
116
+ type: "user",
117
+ message: { role: "user", content: prompt },
118
+ parent_tool_use_id: null
119
+ });
120
+ } else {
121
+ log("warning: no pending resolve, message may be lost");
122
+ }
123
+ }
124
+ function kill() {
125
+ log("session killed");
126
+ killed = true;
127
+ chunkCb = null;
128
+ toolCb = null;
129
+ doneCb = null;
130
+ errorCb = null;
131
+ abortController.abort();
132
+ if (resolveNextMessage) {
133
+ resolveNextMessage({
134
+ type: "user",
135
+ message: { role: "user", content: "" },
136
+ parent_tool_use_id: null
137
+ });
138
+ resolveNextMessage = null;
139
+ }
140
+ }
141
+ return {
142
+ sendMessage,
143
+ kill,
144
+ onChunk: (cb) => {
145
+ chunkCb = cb;
146
+ },
147
+ onTool: (cb) => {
148
+ toolCb = cb;
149
+ },
150
+ onDone: (cb) => {
151
+ doneCb = cb;
152
+ },
153
+ onError: (cb) => {
154
+ errorCb = cb;
155
+ }
156
+ };
157
+ }
158
+ var claudeProvider = {
159
+ name: "claude",
160
+ createSession: createClaudeSession
161
+ };
162
+
163
+ // ../channel-server/src/ws-server.ts
164
+ function log2(msg) {
165
+ process.stderr.write(`[pinfix:ws] ${msg}
166
+ `);
167
+ }
168
+ var HEARTBEAT_INTERVAL = 3e4;
169
+ async function tryListen(port, maxRetries) {
170
+ let lastError = null;
171
+ for (let i = 0; i <= maxRetries; i++) {
172
+ const candidatePort = port + i;
173
+ try {
174
+ const wss = new WebSocketServer({ port: candidatePort });
175
+ await new Promise((resolve2, reject) => {
176
+ wss.once("listening", resolve2);
177
+ wss.once("error", reject);
178
+ });
179
+ return wss;
180
+ } catch (err) {
181
+ lastError = err;
182
+ if (err.code !== "EADDRINUSE") throw err;
183
+ }
184
+ }
185
+ throw lastError;
186
+ }
187
+ var DEFAULT_SYSTEM_PROMPT = `You are PinFix, a coding assistant. The user has annotated a UI element in their running app. You will receive the source location (file:line:column) and their request.
188
+
189
+ Rules:
190
+ - Read the relevant file first to understand context before making changes
191
+ - Keep changes minimal and focused on what was asked
192
+ - Reply concisely, explain what was changed
193
+ - If the request is ambiguous, ask before modifying`;
194
+ async function createWsServer(options) {
195
+ const { port, cwd, maxPortRetries = 5 } = options;
196
+ const pinContexts = /* @__PURE__ */ new Map();
197
+ let workspaceSession = null;
198
+ let workspacePrompt;
199
+ let activeTurn = null;
200
+ const wss = await tryListen(port, maxPortRetries);
201
+ const actualPort = wss.address().port;
202
+ const heartbeatInterval = setInterval(() => {
203
+ for (const ws of wss.clients) {
204
+ const client = ws;
205
+ if (client.isAlive === false) {
206
+ client.terminate();
207
+ continue;
208
+ }
209
+ client.isAlive = false;
210
+ client.ping();
211
+ if (client.readyState === WebSocket.OPEN) {
212
+ client.send(JSON.stringify({ type: "ping" }));
213
+ }
214
+ }
215
+ }, HEARTBEAT_INTERVAL);
216
+ wss.on("connection", (ws) => {
217
+ const client = ws;
218
+ client.isAlive = true;
219
+ log2("client connected");
220
+ ws.on("pong", () => {
221
+ client.isAlive = true;
222
+ });
223
+ ws.on("message", (raw) => {
224
+ try {
225
+ const msg = JSON.parse(raw.toString());
226
+ if (msg.type === "pong") return;
227
+ if (isSessionStartMessage(msg)) {
228
+ log2(`session:start pinId=${msg.pinId} source=${msg.source}`);
229
+ handleSessionStart(ws, msg.pinId, msg.source, msg.prompt);
230
+ } else if (isChatSendMessage(msg)) {
231
+ log2(`chat:send pinId=${msg.pinId} content="${msg.content.slice(0, 80)}"`);
232
+ handleChatSend(ws, msg.pinId, msg.content);
233
+ } else if (isSessionEndMessage(msg)) {
234
+ log2(`session:end pinId=${msg.pinId}`);
235
+ handleSessionEnd(msg.pinId);
236
+ } else if (isWorkspaceResetMessage(msg)) {
237
+ log2("workspace:reset");
238
+ handleWorkspaceReset(msg.prompt);
239
+ }
240
+ } catch {
241
+ }
242
+ });
243
+ ws.on("close", () => {
244
+ log2("client disconnected");
245
+ });
246
+ });
247
+ function handleSessionStart(_ws, pinId, source, _prompt) {
248
+ pinContexts.set(pinId, { source });
249
+ ensureWorkspaceSession();
250
+ }
251
+ function ensureWorkspaceSession() {
252
+ if (workspaceSession) return workspaceSession;
253
+ const systemPrompt = workspacePrompt || DEFAULT_SYSTEM_PROMPT;
254
+ const session = claudeProvider.createSession(systemPrompt, cwd);
255
+ session.onChunk((text) => {
256
+ if (!activeTurn) return;
257
+ send(activeTurn.ws, { type: "chat:chunk", pinId: activeTurn.pinId, text });
258
+ });
259
+ session.onTool((tool) => {
260
+ if (!activeTurn) return;
261
+ send(activeTurn.ws, { type: "chat:tool", pinId: activeTurn.pinId, tool });
262
+ });
263
+ session.onDone(() => {
264
+ if (!activeTurn) return;
265
+ const turn = activeTurn;
266
+ activeTurn = null;
267
+ send(turn.ws, { type: "chat:done", pinId: turn.pinId });
268
+ });
269
+ session.onError((error) => {
270
+ const turn = activeTurn;
271
+ activeTurn = null;
272
+ workspaceSession = null;
273
+ if (turn) {
274
+ send(turn.ws, { type: "chat:error", pinId: turn.pinId, error });
275
+ }
276
+ });
277
+ workspaceSession = session;
278
+ return session;
279
+ }
280
+ function handleChatSend(ws, pinId, content) {
281
+ const pinContext = pinContexts.get(pinId);
282
+ if (!pinContext) {
283
+ send(ws, { type: "chat:error", pinId, error: "No active session" });
284
+ return;
285
+ }
286
+ if (activeTurn) {
287
+ send(ws, { type: "chat:error", pinId, error: "Workspace session is busy" });
288
+ return;
289
+ }
290
+ const session = ensureWorkspaceSession();
291
+ activeTurn = { pinId, ws };
292
+ session.sendMessage(buildTurnPrompt(pinContext.source, content));
293
+ }
294
+ function handleSessionEnd(pinId) {
295
+ pinContexts.delete(pinId);
296
+ if (activeTurn?.pinId === pinId) {
297
+ activeTurn = null;
298
+ if (workspaceSession) {
299
+ workspaceSession.kill();
300
+ workspaceSession = null;
301
+ }
302
+ }
303
+ }
304
+ function handleWorkspaceReset(prompt) {
305
+ workspacePrompt = prompt;
306
+ activeTurn = null;
307
+ if (workspaceSession) {
308
+ workspaceSession.kill();
309
+ workspaceSession = null;
310
+ }
311
+ ensureWorkspaceSession();
312
+ }
313
+ const close = () => {
314
+ clearInterval(heartbeatInterval);
315
+ if (workspaceSession) {
316
+ workspaceSession.kill();
317
+ workspaceSession = null;
318
+ }
319
+ pinContexts.clear();
320
+ activeTurn = null;
321
+ wss.close();
322
+ };
323
+ return { port: actualPort, close };
324
+ }
325
+ function send(ws, msg) {
326
+ if (ws.readyState === WebSocket.OPEN) {
327
+ ws.send(JSON.stringify(msg));
328
+ }
329
+ }
330
+ function buildTurnPrompt(source, content) {
331
+ return `[source: ${source}]
332
+
333
+ ${content}`;
334
+ }
335
+
336
+ // ../channel-server/src/index.ts
337
+ import { writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
338
+ import { resolve } from "path";
339
+ async function main() {
340
+ const port = parseInt(process.env.PINFIX_PORT || String(WS_PORT_DEFAULT), 10);
341
+ const cwd = process.env.PINFIX_CWD || process.cwd();
342
+ const parentPid = process.ppid;
343
+ const server = await createWsServer({ port, cwd });
344
+ const pidDir = resolve(cwd, "node_modules", ".cache", "pinfix");
345
+ if (!existsSync(pidDir)) mkdirSync(pidDir, { recursive: true });
346
+ const pidFile = resolve(pidDir, "server.pid");
347
+ writeFileSync(pidFile, String(process.pid));
348
+ function cleanup() {
349
+ try {
350
+ unlinkSync(pidFile);
351
+ } catch {
352
+ }
353
+ server.close();
354
+ process.exit(0);
355
+ }
356
+ process.on("SIGTERM", cleanup);
357
+ process.on("SIGINT", cleanup);
358
+ process.on("disconnect", cleanup);
359
+ const parentCheck = setInterval(() => {
360
+ try {
361
+ process.kill(parentPid, 0);
362
+ } catch {
363
+ clearInterval(parentCheck);
364
+ cleanup();
365
+ }
366
+ }, 5e3);
367
+ parentCheck.unref();
368
+ process.stderr.write(`[pinfix] channel server ready, ws://localhost:${server.port}
369
+ `);
370
+ }
371
+ main().catch((err) => {
372
+ process.stderr.write(`[pinfix] fatal: ${err.message}
373
+ `);
374
+ process.exit(1);
375
+ });