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