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