cc-prompter 0.2.0 → 0.3.1

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,1047 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/webpack-plugin.ts
9
+ import { join as join3, dirname as dirname3 } from "path";
10
+ import { fileURLToPath as fileURLToPath3 } from "url";
11
+ import { codeInspectorPlugin } from "code-inspector-plugin";
12
+
13
+ // src/sidecar.ts
14
+ import express from "express";
15
+ import { createServer } from "http";
16
+
17
+ // src/pty-session.ts
18
+ import { createRequire } from "module";
19
+ import { fileURLToPath } from "url";
20
+ import { EventEmitter } from "events";
21
+ import * as fs from "fs";
22
+ import * as path from "path";
23
+ import * as os from "os";
24
+ var _metaUrl = typeof __filename !== "undefined" ? __filename : fileURLToPath(import.meta.url);
25
+ var require2 = createRequire(_metaUrl);
26
+ var PTY_PACKAGES = [
27
+ "node-pty-prebuilt-multiarch",
28
+ // macOS / Linux prebuilt
29
+ "@homebridge/node-pty-prebuilt-multiarch",
30
+ // Better Windows support
31
+ "node-pty"
32
+ // Original (needs build tools)
33
+ ];
34
+ var _ptyModule = null;
35
+ function loadPty() {
36
+ if (_ptyModule) return _ptyModule;
37
+ const errors = [];
38
+ for (const pkg of PTY_PACKAGES) {
39
+ try {
40
+ const mod = require2(pkg);
41
+ const modDir = path.dirname(require2.resolve(pkg + "/package.json"));
42
+ const buildDir = path.join(modDir, "build", "Release");
43
+ if (!fs.existsSync(buildDir) || fs.readdirSync(buildDir).filter((f) => f.endsWith(".node")).length === 0) {
44
+ errors.push(`${pkg}: no native binary in ${buildDir}`);
45
+ continue;
46
+ }
47
+ console.log(`[cc-prompter] Loaded PTY from: ${pkg}`);
48
+ _ptyModule = mod;
49
+ return _ptyModule;
50
+ } catch (err) {
51
+ errors.push(`${pkg}: ${err.message}`);
52
+ }
53
+ }
54
+ throw new Error(
55
+ `No PTY module available. Tried:
56
+ ${errors.map((e) => " " + e).join("\n")}
57
+ Install one: npm install node-pty-prebuilt-multiarch (macOS/Linux) or @homebridge/node-pty-prebuilt-multiarch (Windows)`
58
+ );
59
+ }
60
+ function resolveClaudeBin(cwd) {
61
+ if (process.platform === "win32") {
62
+ const localJs = path.resolve(cwd, "node_modules/@anthropic-ai/claude-code/bin/claude.js");
63
+ if (fs.existsSync(localJs)) {
64
+ return { file: process.execPath, args: [localJs] };
65
+ }
66
+ const globalPrefix = path.dirname(process.execPath);
67
+ const globalJs = path.join(globalPrefix, "node_modules/@anthropic-ai/claude-code/bin/claude.js");
68
+ if (fs.existsSync(globalJs)) {
69
+ return { file: process.execPath, args: [globalJs] };
70
+ }
71
+ const appDataJs = path.join(process.env.APPDATA || "", "npm/node_modules/@anthropic-ai/claude-code/bin/claude.js");
72
+ if (fs.existsSync(appDataJs)) {
73
+ return { file: process.execPath, args: [appDataJs] };
74
+ }
75
+ return { file: process.env.COMSPEC || "cmd.exe", args: ["/c", "claude"] };
76
+ }
77
+ const local = path.resolve(cwd, "node_modules/@anthropic-ai/claude-code/bin/claude");
78
+ if (fs.existsSync(local)) return { file: local, args: [] };
79
+ return { file: "claude", args: [] };
80
+ }
81
+ function findClaudeProjectsDir() {
82
+ return path.join(os.homedir(), ".claude", "projects");
83
+ }
84
+ function cwdToProjectDir(cwd) {
85
+ const normalized = cwd.replace(/\\/g, "/").replace(/^[A-Za-z]:/, (m) => "-" + m[0].toLowerCase());
86
+ return normalized.replace(/^\//, "").replace(/\/+/g, "-").replace(/^-+/, "");
87
+ }
88
+ function findRecentJsonl(cwd, afterMs) {
89
+ const projectsDir = findClaudeProjectsDir();
90
+ if (!fs.existsSync(projectsDir)) return null;
91
+ const candidates = [];
92
+ const collectFromDir = (dir) => {
93
+ try {
94
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
95
+ for (const f of files) {
96
+ const fp = path.join(dir, f);
97
+ try {
98
+ const stat = fs.statSync(fp);
99
+ if (stat.mtimeMs > afterMs) {
100
+ candidates.push({ path: fp, mtime: stat.mtimeMs });
101
+ }
102
+ } catch {
103
+ }
104
+ }
105
+ } catch {
106
+ }
107
+ };
108
+ const projectSubdir = cwdToProjectDir(cwd);
109
+ const targetDir = path.join(projectsDir, projectSubdir);
110
+ collectFromDir(targetDir);
111
+ if (candidates.length === 0) {
112
+ try {
113
+ const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
114
+ for (const entry of entries) {
115
+ if (entry.isDirectory() && entry.name !== projectSubdir) {
116
+ collectFromDir(path.join(projectsDir, entry.name));
117
+ }
118
+ }
119
+ } catch {
120
+ }
121
+ }
122
+ if (candidates.length === 0) return null;
123
+ candidates.sort((a, b) => b.mtime - a.mtime);
124
+ return candidates[0].path;
125
+ }
126
+ function sessionIdFromJsonlPath(jsonPath) {
127
+ const base = path.basename(jsonPath, ".jsonl");
128
+ const match = base.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
129
+ return match ? match[0] : null;
130
+ }
131
+ function stripAnsi(s) {
132
+ return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\].*?(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[\?]?[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b[^[\]()]?.?/g, "").replace(/\r/g, "");
133
+ }
134
+ function stripSpinner(s) {
135
+ return s.replace(/[✻✶✽✢·●✳◇◆▸▹⏵⏶]+/g, "").replace(/\w{3,}ing[…\s]*\(\d{1,3}s?\)?/g, "").replace(/\w{3,}ing…/g, "").replace(/\s{2,}/g, " ").trim();
136
+ }
137
+ var PtySession = class extends EventEmitter {
138
+ id;
139
+ cwd;
140
+ status = "spawning";
141
+ pty = null;
142
+ jsonlPath = null;
143
+ sessionId = null;
144
+ history = [];
145
+ jsonlOffset = 0;
146
+ jsonlWatcher = null;
147
+ spawnTime;
148
+ messageSentAt = 0;
149
+ busySince = 0;
150
+ // timestamp when last message was sent (for grace period)
151
+ title = "New Session";
152
+ lastActivityAt;
153
+ killed = false;
154
+ ptyBuffer = "";
155
+ // accumulated for prompt detection
156
+ jsonlDiscoverPromise = null;
157
+ // ── PTY streaming fields ──
158
+ busyBuffer = "";
159
+ // accumulated during busy state
160
+ lastUserContent = "";
161
+ // last user message text
162
+ ptyResponseText = "";
163
+ // extracted response text so far
164
+ ptyResponseEmitted = 0;
165
+ // chars already emitted
166
+ usedJsonl = false;
167
+ // JSONL events received this turn
168
+ ptyDoneEmitted = false;
169
+ lastProgress = "";
170
+ // last emitted progress text
171
+ interrupted = false;
172
+ // set when user sends interrupt
173
+ lastSpinnerSec = 0;
174
+ // highest spinner seconds counter seen
175
+ lastSpinnerAt = 0;
176
+ // timestamp when spinner was last active
177
+ emittedTools = /* @__PURE__ */ new Set();
178
+ // dedup tool call emissions
179
+ constructor(id, cwd) {
180
+ super();
181
+ this.id = id;
182
+ this.cwd = cwd;
183
+ this.spawnTime = Date.now();
184
+ this.lastActivityAt = this.spawnTime;
185
+ }
186
+ /** Spawn the claude process via PTY */
187
+ async spawn() {
188
+ const ptyModule = loadPty();
189
+ const { file, args } = resolveClaudeBin(this.cwd);
190
+ console.log(`[pty-session ${this.id}] spawning: ${file} ${args.join(" ")} cwd: ${this.cwd}`);
191
+ this.pty = ptyModule.spawn(file, args, {
192
+ name: "xterm-256color",
193
+ cols: 120,
194
+ rows: 30,
195
+ cwd: this.cwd,
196
+ env: { ...process.env }
197
+ });
198
+ console.log(`[pty-session ${this.id}] PID: ${this.pty.pid}`);
199
+ this.pty.onData((data) => {
200
+ if (this.status === "spawning") {
201
+ console.log(`[pty-session ${this.id}] output: ${JSON.stringify(data.substring(0, 500))}`);
202
+ }
203
+ const clean = stripAnsi(data);
204
+ this.ptyBuffer += clean;
205
+ this.detectPrompt();
206
+ if (this.status === "busy" && !this.usedJsonl) {
207
+ this.busyBuffer += clean;
208
+ this.parseBusyOutput();
209
+ }
210
+ });
211
+ this.pty.onExit(({ exitCode }) => {
212
+ console.log(`[pty-session ${this.id}] exited with code: ${exitCode}`);
213
+ this.status = "exited";
214
+ this.lastActivityAt = Date.now();
215
+ this.emit("exit", exitCode);
216
+ this.cleanup();
217
+ });
218
+ }
219
+ // ── Prompt Detection ──────────────────────────────────
220
+ detectPrompt() {
221
+ const indicators = [
222
+ /for shortcuts/,
223
+ /\/effort/,
224
+ /refactor/
225
+ ];
226
+ for (const re of indicators) {
227
+ if (re.test(this.ptyBuffer) && this.status === "spawning") {
228
+ console.log(`[pty-session ${this.id}] detected prompt \u2192 ready`);
229
+ this.status = "ready";
230
+ this.emit("ready");
231
+ return;
232
+ }
233
+ }
234
+ if (this.interrupted && this.status === "busy") {
235
+ for (const re of indicators) {
236
+ if (re.test(this.ptyBuffer)) {
237
+ console.log(`[pty-session ${this.id}] detected prompt after interrupt \u2192 done`);
238
+ this.interrupted = false;
239
+ this.ptyDoneEmitted = true;
240
+ this.status = "ready";
241
+ this.lastActivityAt = Date.now();
242
+ this.emit("message", { type: "done", durationMs: 0 });
243
+ return;
244
+ }
245
+ }
246
+ }
247
+ }
248
+ async waitUntilReady(timeoutMs = 3e4) {
249
+ if (this.status === "ready") return;
250
+ if (this.status === "exited") throw new Error("Session exited");
251
+ return new Promise((resolve2, reject) => {
252
+ const deadline = Date.now() + timeoutMs;
253
+ const timer = setInterval(() => {
254
+ if (this.status === "ready") {
255
+ clearInterval(timer);
256
+ resolve2();
257
+ } else if (this.status === "exited") {
258
+ clearInterval(timer);
259
+ reject(new Error("Session exited while waiting"));
260
+ } else if (Date.now() > deadline) {
261
+ clearInterval(timer);
262
+ reject(new Error("Timeout waiting for session to be ready"));
263
+ }
264
+ }, 100);
265
+ });
266
+ }
267
+ // ── Send Message ──────────────────────────────────────
268
+ async sendMessage(content) {
269
+ if (!this.pty || this.status === "exited") {
270
+ throw new Error("Session not active");
271
+ }
272
+ if (this.status === "busy") {
273
+ throw new Error("Session busy");
274
+ }
275
+ if (this.status !== "ready") {
276
+ console.log(`[pty-session ${this.id}] waiting for prompt before sending message...`);
277
+ await this.waitUntilReady();
278
+ }
279
+ await new Promise((r) => setTimeout(r, 200));
280
+ this.status = "busy";
281
+ this.lastActivityAt = Date.now();
282
+ this.busySince = Date.now();
283
+ this.busyBuffer = "";
284
+ this.ptyBuffer = "";
285
+ this.lastUserContent = content;
286
+ this.ptyResponseText = "";
287
+ this.ptyResponseEmitted = 0;
288
+ this.usedJsonl = false;
289
+ this.ptyDoneEmitted = false;
290
+ this.lastProgress = "";
291
+ this.interrupted = false;
292
+ this.lastSpinnerSec = 0;
293
+ this.lastSpinnerAt = 0;
294
+ this.emittedTools.clear();
295
+ if (!this.messageSentAt) {
296
+ this.messageSentAt = Date.now();
297
+ console.log(`[pty-session ${this.id}] first message, starting JSONL discovery`);
298
+ this.jsonlDiscoverPromise = this.discoverJsonl();
299
+ }
300
+ console.log(`[pty-session ${this.id}] writing to PTY: ${JSON.stringify(content.slice(0, 100))}`);
301
+ this.pty.write(content + "\r");
302
+ await new Promise((r) => setTimeout(r, 150));
303
+ this.pty.write("\r");
304
+ }
305
+ /** Send a slash command to the PTY */
306
+ sendCommand(command) {
307
+ if (!this.pty || this.status === "exited") {
308
+ throw new Error("Session not active");
309
+ }
310
+ this.pty.write(command + "\r");
311
+ if (command === "/new") {
312
+ this.history = [];
313
+ this.title = "New Session";
314
+ this.jsonlPath = null;
315
+ this.jsonlOffset = 0;
316
+ this.jsonlWatcher?.close();
317
+ this.jsonlWatcher = null;
318
+ this.sessionId = null;
319
+ this.messageSentAt = 0;
320
+ this.status = "ready";
321
+ this.ptyBuffer = "";
322
+ }
323
+ }
324
+ /** Send Escape to PTY to interrupt current generation */
325
+ interrupt() {
326
+ if (!this.pty || this.status === "exited") {
327
+ throw new Error("Session not active");
328
+ }
329
+ if (this.status !== "busy") return;
330
+ this.interrupted = true;
331
+ this.pty.write("\x1B");
332
+ setTimeout(() => {
333
+ if (this.interrupted && this.status === "busy") {
334
+ console.log(`[pty-session ${this.id}] interrupt timeout \u2192 force done`);
335
+ this.interrupted = false;
336
+ this.ptyDoneEmitted = true;
337
+ this.status = "ready";
338
+ this.lastActivityAt = Date.now();
339
+ this.emit("message", { type: "done", durationMs: 0 });
340
+ }
341
+ }, 5e3);
342
+ }
343
+ // ── PTY Output Parsing (streaming fallback) ───────────
344
+ /**
345
+ * Parse PTY output during busy state to extract streaming response.
346
+ *
347
+ * Claude Code TUI patterns:
348
+ * - Spinner frames: ✳ ✶ ✻ ✽ ✢ · (ignore — just animation)
349
+ * - Response text: ⏺<text> or ●<text>
350
+ * - Tool use: ⚡<tool_name> or ✢ editing <file>
351
+ * - Completion: "Brewed for Xs" (ONLY reliable indicator)
352
+ * - ⚠️ ❯ appears in input echo too — NOT a completion signal!
353
+ * - Timing: (Xs · ↓NNN tokens)
354
+ */
355
+ parseBusyOutput() {
356
+ if (this.busyBuffer.length > 3e3) {
357
+ this.busyBuffer = this.busyBuffer.slice(-2e3);
358
+ }
359
+ this.emitProgress();
360
+ const secMatches = [...this.busyBuffer.matchAll(/\w{3,}ing[…\s]*\((\d{1,3})s?/g)];
361
+ for (const m of secMatches) {
362
+ const sec = parseInt(m[1]);
363
+ if (sec > this.lastSpinnerSec) {
364
+ this.lastSpinnerSec = sec;
365
+ this.lastSpinnerAt = Date.now();
366
+ }
367
+ }
368
+ const respMatch = this.busyBuffer.match(/⏺([一-鿿 -〿＀-￯].+)/s);
369
+ if (respMatch) {
370
+ let raw = respMatch[1];
371
+ raw = raw.replace(/[✳✶✻✽✢·].*$/s, "").trim();
372
+ raw = raw.replace(/─{3,}.*$/s, "").trim();
373
+ raw = raw.replace(/\w{3,}ed (?:for|in) \d{1,4}s?.*$/s, "").trim();
374
+ raw = raw.replace(/esctointerrupt.*$/s, "").trim();
375
+ if (raw.length > this.ptyResponseText.length) {
376
+ this.ptyResponseText = raw;
377
+ this.emitIncrementalText();
378
+ }
379
+ }
380
+ const toolCallMatch = this.busyBuffer.match(/⏺(Update|Read|Edit|Write|Bash)\(([^)]+)\)/);
381
+ if (toolCallMatch) {
382
+ const sig = `${toolCallMatch[1]}:${toolCallMatch[2]}`;
383
+ if (!this.emittedTools.has(sig)) {
384
+ this.emittedTools.add(sig);
385
+ this.emit("message", {
386
+ type: "assistant_tool",
387
+ tool: { name: toolCallMatch[1], input: { file: toolCallMatch[2] } }
388
+ });
389
+ }
390
+ }
391
+ const winToolMatch = this.busyBuffer.match(/● (Reading|Editing|Writing|Searching|Bashing)[^\n]*\n\s*⎿\s*(.+)/);
392
+ if (!toolCallMatch && winToolMatch) {
393
+ const filePath = winToolMatch[2].trim();
394
+ const sig = `${winToolMatch[1]}:${filePath}`;
395
+ if (!this.emittedTools.has(sig)) {
396
+ this.emittedTools.add(sig);
397
+ this.emit("message", {
398
+ type: "assistant_tool",
399
+ tool: { name: winToolMatch[1], input: { file: filePath } }
400
+ });
401
+ }
402
+ }
403
+ if (this.ptyDoneEmitted) return;
404
+ const cleanBuf = stripSpinner(this.busyBuffer);
405
+ const hasDoneMarker = /\b\w+ed (?:for|in) \d{1,4}s?\b/i.test(cleanBuf);
406
+ const spinnerTimeout = this.lastSpinnerAt > 0 && Date.now() - this.lastSpinnerAt > 1e4 && this.ptyResponseEmitted > 0;
407
+ if (hasDoneMarker || spinnerTimeout) {
408
+ const reason = hasDoneMarker ? "completion marker" : `spinner timeout (${Math.round((Date.now() - this.lastSpinnerAt) / 1e3)}s since last activity)`;
409
+ console.log(`[pty-session ${this.id}] detected done via ${reason}`);
410
+ this.ptyDoneEmitted = true;
411
+ if (this.ptyResponseText.length > this.ptyResponseEmitted) {
412
+ this.emitIncrementalText();
413
+ }
414
+ if (this.ptyResponseEmitted === 0) {
415
+ const finalMatch = this.busyBuffer.match(/⏺(.+)/s);
416
+ if (finalMatch) {
417
+ let text = finalMatch[1].replace(/[✳✶✻✽✢·].*$/s, "").replace(/─{3,}.*$/s, "").replace(/\w{3,}ed (?:for|in) \d{1,4}s?.*$/s, "").replace(/esctointerrupt.*$/s, "").trim();
418
+ if (text.length > 0) {
419
+ this.emitUserIfNeeded();
420
+ this.history.push({
421
+ role: "assistant",
422
+ content: text,
423
+ timestamp: Date.now()
424
+ });
425
+ this.emit("message", { type: "assistant_text", content: text });
426
+ this.ptyResponseEmitted = text.length;
427
+ }
428
+ }
429
+ }
430
+ const durMatch = cleanBuf.match(/\w+ed (?:for|in) (\d{1,4})s?/i);
431
+ const durationMs = durMatch ? parseInt(durMatch[1]) * 1e3 : 0;
432
+ if (this.history.filter((m) => m.role === "user").length <= 1 && this.lastUserContent) {
433
+ this.title = this.lastUserContent.slice(0, 60);
434
+ this.emit("title-change", this.title);
435
+ }
436
+ this.status = "ready";
437
+ this.lastActivityAt = Date.now();
438
+ this.emit("message", { type: "done", durationMs });
439
+ }
440
+ }
441
+ /** Emit only the newly arrived characters (incremental streaming) */
442
+ emitIncrementalText() {
443
+ const newText = this.ptyResponseText.slice(this.ptyResponseEmitted);
444
+ if (newText.length === 0) return;
445
+ if (this.ptyResponseEmitted === 0) {
446
+ this.emitUserIfNeeded();
447
+ }
448
+ this.ptyResponseEmitted = this.ptyResponseText.length;
449
+ this.emit("message", { type: "assistant_text", content: newText });
450
+ }
451
+ /**
452
+ * Extract and emit progress updates from busyBuffer.
453
+ *
454
+ * Parses PTY output for Claude Code's progress indicators:
455
+ * - "Thinking for Xs, reading N files"
456
+ * - "Thought for Xs, read N files"
457
+ * - "Crafting… (Xs · ↓NN tokens)"
458
+ * - "Update(file)" / "Read(file)"
459
+ * - "⎿ Removed N lines"
460
+ * - "(Xs · ↓NN tokens)" timing
461
+ */
462
+ emitProgress() {
463
+ const text = this.busyBuffer.replace(/[✳✶✻✽✢·][a-zA-Z0-9…]{0,4}/g, "").replace(/\s+/g, " ");
464
+ const patterns = [
465
+ // Thinking phase
466
+ [/Thinking for (\d+s)[^─]{0,60}(reading \d+ file[^)]*)?/, (m) => {
467
+ return m[0].replace(/\s+/g, " ").replace(/\s*\(ctrl.*$/, "").trim();
468
+ }],
469
+ // Thought completed
470
+ [/Thought for (\d+s)[^─]{0,60}(read \d+ file[^)]*)?/, (m) => {
471
+ return m[0].replace(/\s+/g, " ").replace(/\s*\(ctrl.*$/, "").trim();
472
+ }],
473
+ // Tool call: Update(file) / Read(file)
474
+ [/⏺(Update|Read|Edit|Write|Bash)\(([^)]+)\)/, (m) => {
475
+ return m[1] + ": " + m[2].split("/").slice(-2).join("/");
476
+ }],
477
+ // Tool result: ⎿ Removed N lines
478
+ [/⎿\s*(Removed|Added|Modified|Created)\s+(\d+)\s+(lines?)/, (m) => {
479
+ return m[1] + " " + m[2] + " " + m[3];
480
+ }],
481
+ // Crafting with timing
482
+ [/Crafting[^─]{0,30}\(\d+s[^)]*\)/, (m) => {
483
+ return m[0].replace(/\s+/g, " ").trim();
484
+ }],
485
+ // Simple timing: (5s · ↓9 tokens)
486
+ [/\((\d+s)\s*·\s*[↓↑]\s*(\d+)\s*tokens?\)/, (m) => {
487
+ return m[1] + " \xB7 " + m[2] + " tokens";
488
+ }]
489
+ ];
490
+ let progress = "";
491
+ for (const [re, fn] of patterns) {
492
+ const m = text.match(re);
493
+ if (m) progress = fn(m);
494
+ }
495
+ if (progress && progress !== this.lastProgress) {
496
+ this.lastProgress = progress;
497
+ this.emit("message", { type: "progress", content: progress });
498
+ }
499
+ }
500
+ /** Emit user message event (only if not already emitted this turn) */
501
+ emitUserIfNeeded() {
502
+ if (!this.lastUserContent) return;
503
+ const already = this.history.some(
504
+ (m) => m.role === "user" && m.content === this.lastUserContent
505
+ );
506
+ if (!already) {
507
+ this.history.push({
508
+ role: "user",
509
+ content: this.lastUserContent,
510
+ timestamp: Date.now()
511
+ });
512
+ this.emit("message", { type: "user", content: this.lastUserContent });
513
+ }
514
+ }
515
+ // ── JSONL Discovery & Parsing (structured events) ─────
516
+ async discoverJsonl() {
517
+ const searchStart = this.messageSentAt - 2e3;
518
+ const projectsDir = findClaudeProjectsDir();
519
+ const expectedSubdir = cwdToProjectDir(this.cwd);
520
+ console.log(`[pty-session ${this.id}] JSONL discovery: projectsDir=${projectsDir}, expected=${expectedSubdir}, cwd=${this.cwd}`);
521
+ for (let i = 0; i < 120; i++) {
522
+ if (this.killed) return;
523
+ const jsonl = findRecentJsonl(this.cwd, searchStart);
524
+ if (jsonl) {
525
+ this.jsonlPath = jsonl;
526
+ this.sessionId = sessionIdFromJsonlPath(jsonl) || null;
527
+ console.log(`[pty-session ${this.id}] found JSONL: ${jsonl} (session: ${this.sessionId})`);
528
+ this.startTailingJsonl();
529
+ return;
530
+ }
531
+ if (i === 10) {
532
+ try {
533
+ if (fs.existsSync(projectsDir)) {
534
+ const dirs = fs.readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
535
+ console.log(`[pty-session ${this.id}] JSONL not found in expected dir. Available project dirs: ${dirs.join(", ")}`);
536
+ } else {
537
+ console.log(`[pty-session ${this.id}] projectsDir does not exist: ${projectsDir}`);
538
+ }
539
+ } catch {
540
+ }
541
+ }
542
+ await new Promise((r) => setTimeout(r, 500));
543
+ }
544
+ console.warn(`[pty-session ${this.id}] JSONL not found after 60s \u2014 using PTY output parsing`);
545
+ }
546
+ startTailingJsonl() {
547
+ if (!this.jsonlPath) return;
548
+ this.readNewJsonlLines();
549
+ try {
550
+ this.jsonlWatcher = fs.watch(
551
+ path.dirname(this.jsonlPath),
552
+ (eventType, filename) => {
553
+ if (filename === path.basename(this.jsonlPath)) {
554
+ this.readNewJsonlLines();
555
+ }
556
+ }
557
+ );
558
+ } catch (err) {
559
+ console.warn(`[pty-session ${this.id}] fs.watch failed:`, err);
560
+ }
561
+ }
562
+ readNewJsonlLines() {
563
+ if (!this.jsonlPath) return;
564
+ try {
565
+ const stat = fs.statSync(this.jsonlPath);
566
+ if (stat.size <= this.jsonlOffset) return;
567
+ const fd = fs.openSync(this.jsonlPath, "r");
568
+ const buf = Buffer.alloc(stat.size - this.jsonlOffset);
569
+ fs.readSync(fd, buf, 0, buf.length, this.jsonlOffset);
570
+ fs.closeSync(fd);
571
+ this.jsonlOffset = stat.size;
572
+ const text = buf.toString("utf8");
573
+ for (const line of text.split("\n")) {
574
+ const trimmed = line.trim();
575
+ if (!trimmed) continue;
576
+ try {
577
+ const evt = JSON.parse(trimmed);
578
+ this.processJsonlEvent(evt);
579
+ } catch {
580
+ }
581
+ }
582
+ } catch {
583
+ }
584
+ }
585
+ /** Process a JSONL event — marks usedJsonl to disable PTY parsing */
586
+ processJsonlEvent(evt) {
587
+ this.usedJsonl = true;
588
+ if (!this.sessionId && evt.sessionId) {
589
+ this.sessionId = evt.sessionId;
590
+ }
591
+ switch (evt.type) {
592
+ case "user": {
593
+ const text = typeof evt.message?.content === "string" ? evt.message.content : Array.isArray(evt.message?.content) ? evt.message.content.filter((c) => c.type === "text").map((c) => c.text).join("\n") : "";
594
+ this.history.push({
595
+ role: "user",
596
+ content: text,
597
+ timestamp: evt.timestamp ? new Date(evt.timestamp).getTime() : Date.now()
598
+ });
599
+ this.lastActivityAt = Date.now();
600
+ this.emit("message", { type: "user", content: text });
601
+ if (this.history.filter((m) => m.role === "user").length === 1 && text) {
602
+ this.title = text.slice(0, 60);
603
+ this.emit("title-change", this.title);
604
+ }
605
+ break;
606
+ }
607
+ case "assistant": {
608
+ const content = evt.message?.content;
609
+ if (!Array.isArray(content)) break;
610
+ const texts = content.filter((c) => c.type === "text").map((c) => c.text).join("\n").trim();
611
+ const tools = content.filter((c) => c.type === "tool_use").map((c) => ({
612
+ name: c.name,
613
+ input: c.input || {}
614
+ }));
615
+ if (texts || tools.length) {
616
+ this.history.push({
617
+ role: "assistant",
618
+ content: texts,
619
+ toolUse: tools.length ? tools : void 0,
620
+ timestamp: evt.timestamp ? new Date(evt.timestamp).getTime() : Date.now()
621
+ });
622
+ }
623
+ if (texts) {
624
+ this.emit("message", { type: "assistant_text", content: texts });
625
+ }
626
+ for (const t of tools) {
627
+ this.emit("message", { type: "assistant_tool", tool: t });
628
+ }
629
+ break;
630
+ }
631
+ case "system": {
632
+ if (evt.subtype === "tool_result") {
633
+ const lastAssistant = [...this.history].reverse().find((m) => m.role === "assistant" && m.toolUse?.length);
634
+ if (lastAssistant?.toolUse?.length) {
635
+ const lastTool = lastAssistant.toolUse[lastAssistant.toolUse.length - 1];
636
+ const resultText = typeof evt.message?.content === "string" ? evt.message.content : "";
637
+ lastTool.result = resultText.slice(0, 500);
638
+ }
639
+ const resultContent = typeof evt.message?.content === "string" ? evt.message.content : Array.isArray(evt.message?.content) ? evt.message.content.map((c) => c.text || "").join("") : "";
640
+ this.emit("message", { type: "system", content: resultContent.slice(0, 200) });
641
+ } else if (evt.subtype === "turn_duration") {
642
+ this.status = "ready";
643
+ this.lastActivityAt = Date.now();
644
+ this.emit("message", { type: "done", durationMs: evt.durationMs });
645
+ }
646
+ break;
647
+ }
648
+ }
649
+ }
650
+ // ── Lifecycle ─────────────────────────────────────────
651
+ kill() {
652
+ this.killed = true;
653
+ this.cleanup();
654
+ if (this.pty) {
655
+ try {
656
+ this.pty.kill();
657
+ } catch {
658
+ }
659
+ this.pty = null;
660
+ }
661
+ this.status = "exited";
662
+ }
663
+ cleanup() {
664
+ if (this.jsonlWatcher) {
665
+ this.jsonlWatcher.close();
666
+ this.jsonlWatcher = null;
667
+ }
668
+ }
669
+ getInfo() {
670
+ const lastMsg = this.history[this.history.length - 1];
671
+ return {
672
+ id: this.id,
673
+ title: this.title,
674
+ status: this.status,
675
+ createdAt: this.spawnTime,
676
+ lastActivityAt: this.lastActivityAt,
677
+ messageCount: this.history.length,
678
+ lastMessagePreview: lastMsg ? lastMsg.content.slice(0, 80) : "",
679
+ sessionId: this.sessionId
680
+ };
681
+ }
682
+ getHistory() {
683
+ return [...this.history];
684
+ }
685
+ };
686
+
687
+ // src/assets.ts
688
+ import { readFileSync, existsSync as existsSync2 } from "fs";
689
+ import { join as join2, dirname as dirname2 } from "path";
690
+ import { fileURLToPath as fileURLToPath2 } from "url";
691
+ var _dirname = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath2(import.meta.url));
692
+ function assetPath(filename) {
693
+ const here = join2(_dirname, filename);
694
+ if (existsSync2(here)) return here;
695
+ const parent = join2(_dirname, "..", filename);
696
+ if (existsSync2(parent)) return parent;
697
+ throw new Error(
698
+ `[cc-prompter] Asset not found: ${filename}
699
+ Tried: ${here}
700
+ Tried: ${parent}
701
+ __dirname: ${_dirname}`
702
+ );
703
+ }
704
+ function getPanelHtml() {
705
+ return readFileSync(assetPath("panel.html"), "utf8");
706
+ }
707
+ function getInjectScript() {
708
+ return readFileSync(assetPath("inject.js"), "utf8");
709
+ }
710
+
711
+ // src/sidecar.ts
712
+ var SessionManager = class {
713
+ sessions = /* @__PURE__ */ new Map();
714
+ counter = 0;
715
+ async create(cwd) {
716
+ const id = `s${++this.counter}-${Date.now().toString(36)}`;
717
+ const session = new PtySession(id, cwd);
718
+ this.sessions.set(id, session);
719
+ session.on("exit", () => {
720
+ });
721
+ await session.spawn();
722
+ return session;
723
+ }
724
+ get(id) {
725
+ return this.sessions.get(id);
726
+ }
727
+ list() {
728
+ return Array.from(this.sessions.values()).map((s) => {
729
+ const info = s.getInfo();
730
+ return {
731
+ id: info.id,
732
+ title: info.title,
733
+ status: info.status,
734
+ createdAt: info.createdAt,
735
+ lastActivityAt: info.lastActivityAt,
736
+ messageCount: info.messageCount,
737
+ lastMessagePreview: info.lastMessagePreview
738
+ };
739
+ });
740
+ }
741
+ destroy(id) {
742
+ const session = this.sessions.get(id);
743
+ if (!session) return false;
744
+ session.kill();
745
+ this.sessions.delete(id);
746
+ return true;
747
+ }
748
+ destroyAll() {
749
+ for (const session of this.sessions.values()) {
750
+ session.kill();
751
+ }
752
+ this.sessions.clear();
753
+ }
754
+ };
755
+ function startSidecar(projectRoot, options) {
756
+ const startPort = options?.startPort || 3456;
757
+ const app = express();
758
+ app.use(express.json());
759
+ const manager = new SessionManager();
760
+ app.use((req, res, next) => {
761
+ res.header("Access-Control-Allow-Origin", "*");
762
+ res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
763
+ res.header("Access-Control-Allow-Headers", "Content-Type");
764
+ if (req.method === "OPTIONS") {
765
+ res.sendStatus(204);
766
+ return;
767
+ }
768
+ next();
769
+ });
770
+ app.get("/__panel/", (_req, res) => {
771
+ const html = getPanelHtml();
772
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
773
+ res.setHeader("Content-Length", Buffer.byteLength(html));
774
+ res.end(html);
775
+ });
776
+ app.get("/favicon.ico", (_req, res) => {
777
+ res.sendStatus(204);
778
+ });
779
+ app.get("/api/sessions", (_req, res) => {
780
+ res.json(manager.list());
781
+ });
782
+ app.post("/api/sessions", async (req, res) => {
783
+ try {
784
+ const cwd = req.body?.cwd || projectRoot;
785
+ const session = await manager.create(cwd);
786
+ res.json(session.getInfo());
787
+ } catch (err) {
788
+ console.error("[cc-prompter] Failed to create session:", err);
789
+ res.status(500).json({ error: err.message, stack: err.stack });
790
+ }
791
+ });
792
+ app.post(
793
+ "/api/sessions/:id/message",
794
+ async (req, res) => {
795
+ const session = manager.get(req.params.id);
796
+ if (!session) {
797
+ res.status(404).json({ error: "Session not found" });
798
+ return;
799
+ }
800
+ if (session.status === "exited") {
801
+ res.status(410).json({ error: "Session exited" });
802
+ return;
803
+ }
804
+ if (session.status === "busy") {
805
+ res.status(409).json({ error: "Session busy" });
806
+ return;
807
+ }
808
+ const { content, sourceInfo } = req.body;
809
+ if (!content) {
810
+ res.status(400).json({ error: "Missing content" });
811
+ return;
812
+ }
813
+ let prompt = content;
814
+ if (sourceInfo) {
815
+ const relPath = sourceInfo.path;
816
+ const parts = [
817
+ `[source: ${relPath}:${sourceInfo.line}:${sourceInfo.column}]`
818
+ ];
819
+ if (sourceInfo.elementInfo) {
820
+ parts.push(`[element: ${sourceInfo.elementInfo}]`);
821
+ }
822
+ prompt = parts.join(" ") + " " + content;
823
+ }
824
+ res.setHeader("Content-Type", "text/event-stream");
825
+ res.setHeader("Cache-Control", "no-cache");
826
+ res.setHeader("Connection", "keep-alive");
827
+ res.setHeader("X-Accel-Buffering", "no");
828
+ res.flushHeaders();
829
+ const onMessage = (evt) => {
830
+ res.write(`data: ${JSON.stringify(evt)}
831
+
832
+ `);
833
+ if (evt.type === "done") {
834
+ cleanup();
835
+ }
836
+ };
837
+ const onError = (err) => {
838
+ res.write(`data: ${JSON.stringify({ type: "error", content: err.message })}
839
+
840
+ `);
841
+ cleanup();
842
+ };
843
+ const cleanup = () => {
844
+ if (cleanedUp) return;
845
+ cleanedUp = true;
846
+ session.removeListener("message", onMessage);
847
+ session.removeListener("error", onError);
848
+ res.end();
849
+ };
850
+ session.on("message", onMessage);
851
+ session.on("error", onError);
852
+ let cleanedUp = false;
853
+ setTimeout(() => {
854
+ req.on("close", () => {
855
+ if (!cleanedUp) {
856
+ cleanup();
857
+ }
858
+ });
859
+ }, 3e3);
860
+ try {
861
+ await session.sendMessage(prompt);
862
+ } catch (err) {
863
+ res.write(`data: ${JSON.stringify({ type: "error", content: err.message })}
864
+
865
+ `);
866
+ cleanup();
867
+ }
868
+ }
869
+ );
870
+ app.post(
871
+ "/api/sessions/:id/command",
872
+ (req, res) => {
873
+ const session = manager.get(req.params.id);
874
+ if (!session) {
875
+ res.status(404).json({ error: "Session not found" });
876
+ return;
877
+ }
878
+ const { command } = req.body;
879
+ if (!command) {
880
+ res.status(400).json({ error: "Missing command" });
881
+ return;
882
+ }
883
+ try {
884
+ session.sendCommand(command);
885
+ res.json({ ok: true });
886
+ } catch (err) {
887
+ res.status(500).json({ error: err.message });
888
+ }
889
+ }
890
+ );
891
+ app.post("/api/sessions/:id/interrupt", (req, res) => {
892
+ const session = manager.get(req.params.id);
893
+ if (!session) {
894
+ res.status(404).json({ error: "Session not found" });
895
+ return;
896
+ }
897
+ try {
898
+ session.interrupt();
899
+ res.json({ ok: true });
900
+ } catch (err) {
901
+ res.status(500).json({ error: err.message });
902
+ }
903
+ });
904
+ app.delete("/api/sessions/:id", (req, res) => {
905
+ if (manager.destroy(req.params.id)) {
906
+ res.json({ ok: true });
907
+ } else {
908
+ res.status(404).json({ error: "Session not found" });
909
+ }
910
+ });
911
+ app.get("/api/sessions/:id/history", (req, res) => {
912
+ const session = manager.get(req.params.id);
913
+ if (!session) {
914
+ res.status(404).json({ error: "Session not found" });
915
+ return;
916
+ }
917
+ res.json(session.getHistory());
918
+ });
919
+ const server = createServer(app);
920
+ let actualPort = startPort;
921
+ app.get("/__cc-port", (_req, res) => {
922
+ const addr = server.address();
923
+ const port = addr && typeof addr === "object" ? addr.port : actualPort;
924
+ res.setHeader("Access-Control-Allow-Origin", "*");
925
+ res.end(String(port));
926
+ });
927
+ const MAX_PORT = startPort + 10;
928
+ function tryListen(port) {
929
+ return new Promise((resolve2, reject) => {
930
+ server.listen(port, () => {
931
+ actualPort = port;
932
+ console.log(`[cc-prompter] Sidecar running on http://localhost:${port}`);
933
+ resolve2(server);
934
+ });
935
+ server.on("error", (err) => {
936
+ if (err.code === "EADDRINUSE" && port < MAX_PORT) {
937
+ console.log(`[cc-prompter] Port ${port} in use, trying ${port + 1}...`);
938
+ tryListen(port + 1).then(resolve2, reject);
939
+ } else {
940
+ reject(err);
941
+ }
942
+ });
943
+ });
944
+ }
945
+ tryListen(startPort).catch((err) => {
946
+ console.error(`[cc-prompter] Failed to start sidecar:`, err.message);
947
+ });
948
+ server.on("close", () => {
949
+ manager.destroyAll();
950
+ });
951
+ return server;
952
+ }
953
+
954
+ // src/webpack-plugin.ts
955
+ var _dirname2 = typeof __dirname !== "undefined" ? __dirname : dirname3(fileURLToPath3(import.meta.url));
956
+ var CcPromptWebpackPlugin = class {
957
+ options;
958
+ sidecarServer = null;
959
+ sidecarStarted = false;
960
+ cleanedUp = false;
961
+ constructor(options) {
962
+ this.options = options || {};
963
+ }
964
+ apply(compiler) {
965
+ const isDev = this.options.dev !== void 0 ? this.options.dev : process.env.NODE_ENV !== "production";
966
+ if (!isDev) return;
967
+ const startPort = this.options.port || 3456;
968
+ const clientEntryPath = join3(_dirname2, "client-entry.js");
969
+ if (!this.sidecarStarted) {
970
+ this.sidecarStarted = true;
971
+ const projectRoot = this.options.root || process.cwd();
972
+ this.sidecarServer = startSidecar(projectRoot, { startPort });
973
+ }
974
+ compiler.hooks.thisCompilation.tap("CcPromptWebpackPlugin", () => {
975
+ if (!this.cleanedUp) {
976
+ this.setupCleanup();
977
+ }
978
+ });
979
+ if (this.options.inspector !== false) {
980
+ const inspectorPlugin = codeInspectorPlugin({
981
+ bundler: "webpack",
982
+ behavior: {
983
+ locate: false,
984
+ copy: false
985
+ },
986
+ hideDomPathAttr: true,
987
+ hideConsole: true
988
+ });
989
+ if (inspectorPlugin && typeof inspectorPlugin.apply === "function") {
990
+ inspectorPlugin.apply(compiler);
991
+ } else if (Array.isArray(inspectorPlugin)) {
992
+ for (const p of inspectorPlugin) {
993
+ if (p && typeof p.apply === "function") p.apply(compiler);
994
+ }
995
+ }
996
+ }
997
+ const webpack = __require("webpack");
998
+ const injectScript = getInjectScript();
999
+ new webpack.DefinePlugin({
1000
+ "__CC_PROMPTER_INJECT_SCRIPT__": JSON.stringify(injectScript),
1001
+ "__CC_PROMPTER_PORT__": JSON.stringify(startPort)
1002
+ }).apply(compiler);
1003
+ const originalEntry = compiler.options.entry;
1004
+ compiler.options.entry = async () => {
1005
+ const entries = typeof originalEntry === "function" ? await originalEntry() : originalEntry;
1006
+ if (typeof entries === "string") {
1007
+ return { main: [clientEntryPath, entries] };
1008
+ }
1009
+ if (Array.isArray(entries)) {
1010
+ return { main: [clientEntryPath, ...entries] };
1011
+ }
1012
+ if (typeof entries === "object") {
1013
+ for (const key in entries) {
1014
+ if (Array.isArray(entries[key])) {
1015
+ entries[key].unshift(clientEntryPath);
1016
+ } else if (typeof entries[key] === "string") {
1017
+ entries[key] = [clientEntryPath, entries[key]];
1018
+ }
1019
+ }
1020
+ }
1021
+ return entries;
1022
+ };
1023
+ }
1024
+ setupCleanup() {
1025
+ const cleanup = () => {
1026
+ if (this.cleanedUp) return;
1027
+ this.cleanedUp = true;
1028
+ if (this.sidecarServer) {
1029
+ this.sidecarServer.close();
1030
+ this.sidecarServer = null;
1031
+ }
1032
+ };
1033
+ process.on("SIGTERM", () => {
1034
+ cleanup();
1035
+ process.exit(0);
1036
+ });
1037
+ process.on("SIGINT", () => {
1038
+ cleanup();
1039
+ process.exit(0);
1040
+ });
1041
+ process.on("exit", cleanup);
1042
+ }
1043
+ };
1044
+ export {
1045
+ CcPromptWebpackPlugin
1046
+ };
1047
+ //# sourceMappingURL=webpack-plugin.js.map