copilot-proxy-web 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ function createDaemonService({
2
+ daemonStateStore,
3
+ buildArgs,
4
+ spawnFn,
5
+ isProcessAlive,
6
+ sleepFn,
7
+ processLike,
8
+ consoleLike,
9
+ httpLike,
10
+ resolveHostForPrint,
11
+ resolveApiPortForPrint,
12
+ hasAuthConfigured,
13
+ port,
14
+ log,
15
+ ptyCols,
16
+ ptyRows,
17
+ noDefaultSession,
18
+ noStdin,
19
+ noStdout,
20
+ passthrough,
21
+ restoreTerminal,
22
+ }) {
23
+ function runForeground() {
24
+ const child = spawnFn(processLike.execPath, buildArgs(), { stdio: "inherit" });
25
+ processLike.on("SIGINT", () => {
26
+ restoreTerminal();
27
+ });
28
+ child.on("exit", (code, signal) => {
29
+ restoreTerminal();
30
+ if (signal) {
31
+ processLike.kill(processLike.pid, signal);
32
+ return;
33
+ }
34
+ processLike.exit(code ?? 0);
35
+ });
36
+ }
37
+
38
+ function startBackground() {
39
+ const existingPid = daemonStateStore.readPid();
40
+ if (existingPid && isProcessAlive(existingPid)) {
41
+ consoleLike.log(`Already running (pid ${existingPid}).`);
42
+ processLike.exit(0);
43
+ return;
44
+ }
45
+ if (existingPid && !isProcessAlive(existingPid)) {
46
+ daemonStateStore.clearPid();
47
+ }
48
+ const child = spawnFn(processLike.execPath, buildArgs(), {
49
+ stdio: "ignore",
50
+ detached: true,
51
+ });
52
+ child.unref();
53
+ daemonStateStore.writePid(child.pid);
54
+ daemonStateStore.writeState({
55
+ pid: child.pid,
56
+ startedAt: new Date().toISOString(),
57
+ port,
58
+ log,
59
+ ptyCols,
60
+ ptyRows,
61
+ noDefaultSession,
62
+ noStdin,
63
+ noStdout,
64
+ passthrough,
65
+ });
66
+ const hostForPrint = resolveHostForPrint();
67
+ const apiPortForPrint = resolveApiPortForPrint();
68
+ const isLoopbackHost =
69
+ hostForPrint === "127.0.0.1" || hostForPrint === "localhost" || hostForPrint === "::1";
70
+ if (!hasAuthConfigured()) {
71
+ consoleLike.log(
72
+ `[${new Date().toISOString()}] WARNING: AUTH_TOKEN not set; API/WS are unauthenticated. Use --auth-token or AUTH_TOKEN env.`
73
+ );
74
+ if (!isLoopbackHost) {
75
+ consoleLike.log(
76
+ `[${new Date().toISOString()}] WARNING: host=${hostForPrint} is not loopback; unauthenticated API/WS may be exposed.`
77
+ );
78
+ }
79
+ }
80
+ consoleLike.log(
81
+ `[${new Date().toISOString()}] Web UI listening on http://${hostForPrint}:${port}`
82
+ );
83
+ consoleLike.log(
84
+ `[${new Date().toISOString()}] API listening on http://${hostForPrint}:${apiPortForPrint}/api`
85
+ );
86
+ consoleLike.log(`Started (pid ${child.pid}).`);
87
+ }
88
+
89
+ function stopBackground() {
90
+ const pid = daemonStateStore.readPid();
91
+ if (!pid) {
92
+ consoleLike.log("Not running.");
93
+ processLike.exit(0);
94
+ return;
95
+ }
96
+ if (!isProcessAlive(pid)) {
97
+ daemonStateStore.clearPid();
98
+ consoleLike.log("Not running.");
99
+ processLike.exit(0);
100
+ return;
101
+ }
102
+ try {
103
+ processLike.kill(pid, "SIGTERM");
104
+ } catch {
105
+ daemonStateStore.clearPid();
106
+ consoleLike.error("Failed to stop process.");
107
+ processLike.exit(1);
108
+ return;
109
+ }
110
+ for (let i = 0; i < 20; i += 1) {
111
+ if (!isProcessAlive(pid)) break;
112
+ sleepFn(100);
113
+ }
114
+ if (isProcessAlive(pid)) {
115
+ try {
116
+ processLike.kill(pid, "SIGKILL");
117
+ } catch {
118
+ // ignore
119
+ }
120
+ }
121
+ daemonStateStore.clearPid();
122
+ consoleLike.log("Stopped.");
123
+ }
124
+
125
+ function status() {
126
+ const pid = daemonStateStore.readPid();
127
+ if (pid && isProcessAlive(pid)) {
128
+ const state = daemonStateStore.readState();
129
+ if (state?.port) {
130
+ consoleLike.log(`Running (pid ${pid}) on port ${state.port}.`);
131
+ } else {
132
+ consoleLike.log(`Running (pid ${pid}).`);
133
+ }
134
+ processLike.exit(0);
135
+ return;
136
+ }
137
+ consoleLike.log("Stopped.");
138
+ processLike.exit(1);
139
+ }
140
+
141
+ function check() {
142
+ const state = daemonStateStore.readState();
143
+ const targetPort = state?.port || port;
144
+ function get(pathname, onDone) {
145
+ const req = httpLike.get(
146
+ {
147
+ host: "127.0.0.1",
148
+ port: targetPort,
149
+ path: pathname,
150
+ timeout: 3000,
151
+ },
152
+ (res) => {
153
+ res.resume();
154
+ onDone(null, res.statusCode);
155
+ }
156
+ );
157
+ req.on("timeout", () => {
158
+ req.destroy();
159
+ onDone(new Error("timeout"));
160
+ });
161
+ req.on("error", (err) => {
162
+ onDone(err);
163
+ });
164
+ }
165
+
166
+ get("/api/status", (err, statusCode) => {
167
+ if (err) {
168
+ consoleLike.error("Unhealthy (connection failed)");
169
+ processLike.exit(1);
170
+ return;
171
+ }
172
+ if (statusCode >= 200 && statusCode < 300) {
173
+ consoleLike.log(`OK (http://127.0.0.1:${targetPort}/api/status)`);
174
+ processLike.exit(0);
175
+ return;
176
+ }
177
+ if (statusCode === 404) {
178
+ get("/api/sessions", (err2, statusCode2) => {
179
+ if (err2) {
180
+ consoleLike.error("Unhealthy (connection failed)");
181
+ processLike.exit(1);
182
+ return;
183
+ }
184
+ if (statusCode2 >= 200 && statusCode2 < 300) {
185
+ consoleLike.log(`OK (no default session) http://127.0.0.1:${targetPort}`);
186
+ processLike.exit(0);
187
+ return;
188
+ }
189
+ consoleLike.error(`Unhealthy (HTTP ${statusCode2})`);
190
+ processLike.exit(1);
191
+ });
192
+ return;
193
+ }
194
+ consoleLike.error(`Unhealthy (HTTP ${statusCode})`);
195
+ processLike.exit(1);
196
+ });
197
+ }
198
+
199
+ return {
200
+ runForeground,
201
+ startBackground,
202
+ stopBackground,
203
+ status,
204
+ check,
205
+ };
206
+ }
207
+
208
+ module.exports = {
209
+ createDaemonService,
210
+ };
@@ -0,0 +1,55 @@
1
+ function createDaemonStateStore({ fs, homeRoot, pidFile, stateFile }) {
2
+ function ensureHome() {
3
+ fs.mkdirSync(homeRoot, { recursive: true });
4
+ }
5
+
6
+ function readState() {
7
+ try {
8
+ const raw = fs.readFileSync(stateFile, "utf8");
9
+ return JSON.parse(raw);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function writeState(state) {
16
+ ensureHome();
17
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
18
+ }
19
+
20
+ function writePid(pid) {
21
+ ensureHome();
22
+ fs.writeFileSync(pidFile, String(pid));
23
+ }
24
+
25
+ function readPid() {
26
+ try {
27
+ const raw = fs.readFileSync(pidFile, "utf8");
28
+ const pid = Number(raw.trim());
29
+ return Number.isFinite(pid) ? pid : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function clearPid() {
36
+ try {
37
+ fs.unlinkSync(pidFile);
38
+ } catch {
39
+ // ignore
40
+ }
41
+ }
42
+
43
+ return {
44
+ ensureHome,
45
+ readState,
46
+ writeState,
47
+ writePid,
48
+ readPid,
49
+ clearPid,
50
+ };
51
+ }
52
+
53
+ module.exports = {
54
+ createDaemonStateStore,
55
+ };
package/lib/format.js ADDED
@@ -0,0 +1,29 @@
1
+ const ansiRegex = new RegExp(
2
+ "[\\u001B\\u009B][[\\]()#;?]*(?:" +
3
+ "(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?" +
4
+ "[0-9A-ORZcf-nqry=><~])",
5
+ "g"
6
+ );
7
+
8
+ function stripAnsi(text) {
9
+ return text.replace(ansiRegex, "");
10
+ }
11
+
12
+ function toMarkdownBlock(text, info = "text") {
13
+ return `\n\n\`\`\`${info}\n${text}\n\`\`\`\n`;
14
+ }
15
+
16
+ function formatEventMarkdown(event) {
17
+ const header = `\n\n### ${event.type}\n\n- ts: ${event.ts}\n`;
18
+ if (event.content) {
19
+ return header + toMarkdownBlock(event.content);
20
+ }
21
+ return header;
22
+ }
23
+
24
+ module.exports = {
25
+ ansiRegex,
26
+ stripAnsi,
27
+ toMarkdownBlock,
28
+ formatEventMarkdown,
29
+ };
package/lib/hooks.js ADDED
@@ -0,0 +1,29 @@
1
+ function createHookRegistry(options = {}) {
2
+ const { onError } = options;
3
+ const hooks = [];
4
+
5
+ function addHook(fn) {
6
+ hooks.push(fn);
7
+ }
8
+
9
+ function emit(event) {
10
+ for (const hook of hooks) {
11
+ try {
12
+ hook(event);
13
+ } catch (err) {
14
+ if (onError) onError(err, event);
15
+ // isolate hook errors
16
+ }
17
+ }
18
+ }
19
+
20
+ return {
21
+ addHook,
22
+ emit,
23
+ hooks,
24
+ };
25
+ }
26
+
27
+ module.exports = {
28
+ createHookRegistry,
29
+ };
@@ -0,0 +1,109 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ function pad2(value) {
5
+ return String(value).padStart(2, "0");
6
+ }
7
+
8
+ function formatTimestamp(date) {
9
+ const yyyy = date.getFullYear();
10
+ const mm = pad2(date.getMonth() + 1);
11
+ const dd = pad2(date.getDate());
12
+ const hh = pad2(date.getHours());
13
+ const min = pad2(date.getMinutes());
14
+ return `${yyyy}${mm}${dd}_${hh}${min}`;
15
+ }
16
+
17
+ function parseTimestamp(value) {
18
+ if (!/^\d{8}_\d{4}$/.test(value)) return null;
19
+ const year = Number(value.slice(0, 4));
20
+ const month = Number(value.slice(4, 6));
21
+ const day = Number(value.slice(6, 8));
22
+ const hour = Number(value.slice(9, 11));
23
+ const minute = Number(value.slice(11, 13));
24
+ if (!Number.isFinite(year)) return null;
25
+ if (!Number.isFinite(month) || month < 1 || month > 12) return null;
26
+ if (!Number.isFinite(day) || day < 1 || day > 31) return null;
27
+ if (!Number.isFinite(hour) || hour < 0 || hour > 23) return null;
28
+ if (!Number.isFinite(minute) || minute < 0 || minute > 59) return null;
29
+ return new Date(year, month - 1, day, hour, minute, 0, 0).getTime();
30
+ }
31
+
32
+ function getRotatePattern(filePath) {
33
+ const filename = path.basename(filePath);
34
+ if (filename.endsWith(".log")) {
35
+ return {
36
+ prefix: filename.slice(0, -4) + ".",
37
+ suffix: ".log",
38
+ };
39
+ }
40
+ return {
41
+ prefix: filename + ".",
42
+ suffix: "",
43
+ };
44
+ }
45
+
46
+ function buildRotatedPath(filePath, timestamp) {
47
+ if (filePath.endsWith(".log")) {
48
+ return filePath.slice(0, -4) + `.${timestamp}.log`;
49
+ }
50
+ return `${filePath}.${timestamp}`;
51
+ }
52
+
53
+ function cleanupOldRotations(filePath, retainDays, nowTs) {
54
+ if (!Number.isFinite(retainDays) || retainDays <= 0) return;
55
+ const dir = path.dirname(filePath);
56
+ let entries;
57
+ try {
58
+ entries = fs.readdirSync(dir);
59
+ } catch {
60
+ return;
61
+ }
62
+ const { prefix, suffix } = getRotatePattern(filePath);
63
+ const cutoff = nowTs - retainDays * 24 * 60 * 60 * 1000;
64
+ for (const entry of entries) {
65
+ if (!entry.startsWith(prefix) || !entry.endsWith(suffix)) continue;
66
+ const tsPart = entry.slice(prefix.length, entry.length - suffix.length);
67
+ const parsed = parseTimestamp(tsPart);
68
+ if (parsed === null) continue;
69
+ if (parsed < cutoff) {
70
+ try {
71
+ fs.unlinkSync(path.join(dir, entry));
72
+ } catch {
73
+ // ignore cleanup errors
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ function rotateLogFile(filePath, { maxBytes, retainDays, now = new Date() } = {}) {
80
+ if (!filePath) return;
81
+ const resolved = path.resolve(filePath);
82
+ const nowTs = now instanceof Date ? now.getTime() : Date.now();
83
+ cleanupOldRotations(resolved, retainDays, nowTs);
84
+
85
+ let stat;
86
+ try {
87
+ stat = fs.statSync(resolved);
88
+ } catch {
89
+ return;
90
+ }
91
+ if (!stat.isFile()) return;
92
+ if (Number.isFinite(maxBytes) && stat.size <= maxBytes) return;
93
+
94
+ const ts = formatTimestamp(now instanceof Date ? now : new Date(nowTs));
95
+ const rotatedPath = buildRotatedPath(resolved, ts);
96
+ try {
97
+ fs.renameSync(resolved, rotatedPath);
98
+ } catch {
99
+ // ignore rotate errors
100
+ }
101
+ }
102
+
103
+ module.exports = {
104
+ rotateLogFile,
105
+ formatTimestamp,
106
+ parseTimestamp,
107
+ buildRotatedPath,
108
+ cleanupOldRotations,
109
+ };
@@ -0,0 +1,40 @@
1
+ const { stripAnsi, toMarkdownBlock } = require("./format");
2
+
3
+ function sliceText(text, maxChars) {
4
+ if (!Number.isFinite(maxChars) || maxChars <= 0) return [text];
5
+ const chunks = [];
6
+ let start = 0;
7
+ while (start < text.length) {
8
+ chunks.push(text.slice(start, start + maxChars));
9
+ start += maxChars;
10
+ }
11
+ return chunks;
12
+ }
13
+
14
+ function formatMarkdownEvent(event, options = {}) {
15
+ const info = options.info || "text";
16
+ const maxChars = options.maxChars ?? 0;
17
+ const clean = options.clean !== false;
18
+ const content = event.content ?? "";
19
+ const body = clean ? stripAnsi(content) : content;
20
+ const chunks = sliceText(body, maxChars);
21
+
22
+ const meta = [
23
+ `- type: ${event.type}`,
24
+ `- ts: ${event.ts || new Date().toISOString()}`,
25
+ ];
26
+ if (event.sessionId) meta.push(`- session: ${event.sessionId}`);
27
+ if (typeof event.index === "number") meta.push(`- index: ${event.index}`);
28
+ if (typeof event.total === "number") meta.push(`- total: ${event.total}`);
29
+
30
+ return chunks.map((chunk, idx) => {
31
+ const header = `\n\n### ${event.type}\n\n${meta.join("\n")}\n`;
32
+ const tag = info + (chunks.length > 1 ? `-${idx + 1}` : "");
33
+ return header + toMarkdownBlock(chunk, tag);
34
+ });
35
+ }
36
+
37
+ module.exports = {
38
+ sliceText,
39
+ formatMarkdownEvent,
40
+ };
package/lib/pty.js ADDED
@@ -0,0 +1,13 @@
1
+ function normalizeText(text) {
2
+ return text.replace(/\r?\n/g, "\n");
3
+ }
4
+
5
+ function makePasteSeq(text) {
6
+ const normalized = normalizeText(text);
7
+ return `\u001b[200~${normalized}\u001b[201~`;
8
+ }
9
+
10
+ module.exports = {
11
+ normalizeText,
12
+ makePasteSeq,
13
+ };
@@ -0,0 +1,124 @@
1
+ const https = require("node:https");
2
+ let HttpsProxyAgent;
3
+
4
+ function escapeMarkdownV2(text) {
5
+ return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, "\\$&");
6
+ }
7
+
8
+ function escapeMarkdownV2Code(text) {
9
+ return text.replace(/[\\`]/g, "\\$&");
10
+ }
11
+
12
+ function sliceText(text, maxChars) {
13
+ if (!Number.isFinite(maxChars) || maxChars <= 0) return [text];
14
+ const chunks = [];
15
+ let start = 0;
16
+ while (start < text.length) {
17
+ chunks.push(text.slice(start, start + maxChars));
18
+ start += maxChars;
19
+ }
20
+ return chunks;
21
+ }
22
+
23
+ function formatTelegramMarkdownEvent(event, options = {}) {
24
+ const maxChars = options.maxChars ?? 3500;
25
+ const content = event.content ?? "";
26
+ const chunks = sliceText(content, maxChars);
27
+ const ts = event.ts || new Date().toISOString();
28
+ const type = event.type || "event";
29
+ const sessionId = event.sessionId ? String(event.sessionId) : "";
30
+
31
+ return chunks.map((chunk, idx) => {
32
+ const header = `*${escapeMarkdownV2(type)}*`;
33
+ const meta = [
34
+ `- ts: ${escapeMarkdownV2(ts)}`,
35
+ ...(sessionId ? [`- session: ${escapeMarkdownV2(sessionId)}`] : []),
36
+ ...(chunks.length > 1
37
+ ? [`- part: ${escapeMarkdownV2(String(idx + 1))}/${escapeMarkdownV2(String(chunks.length))}`]
38
+ : []),
39
+ ];
40
+ const body = "```\n" + escapeMarkdownV2Code(chunk) + "\n```";
41
+ return `${header}\n${meta.join("\n")}\n${body}`;
42
+ });
43
+ }
44
+
45
+ function getProxyAgent(proxyUrl) {
46
+ if (!proxyUrl) return undefined;
47
+ if (!HttpsProxyAgent) {
48
+ // lazy require to avoid dependency when unused
49
+ HttpsProxyAgent = require("https-proxy-agent");
50
+ }
51
+ return new HttpsProxyAgent(proxyUrl);
52
+ }
53
+
54
+ async function sendTelegramMessage({
55
+ token,
56
+ chatId,
57
+ text,
58
+ parseMode = "MarkdownV2",
59
+ proxy,
60
+ timeoutMs = 10000,
61
+ }) {
62
+ const payload = JSON.stringify({
63
+ chat_id: chatId,
64
+ text,
65
+ parse_mode: parseMode,
66
+ disable_web_page_preview: true,
67
+ });
68
+
69
+ const options = {
70
+ method: "POST",
71
+ hostname: "api.telegram.org",
72
+ path: `/bot${token}/sendMessage`,
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ "Content-Length": Buffer.byteLength(payload),
76
+ },
77
+ timeout: timeoutMs,
78
+ agent: getProxyAgent(proxy),
79
+ };
80
+
81
+ return new Promise((resolve, reject) => {
82
+ const req = https.request(options, (res) => {
83
+ let data = "";
84
+ res.setEncoding("utf8");
85
+ res.on("data", (chunk) => {
86
+ data += chunk;
87
+ });
88
+ res.on("end", () => {
89
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
90
+ resolve({ ok: true, status: res.statusCode });
91
+ return;
92
+ }
93
+ reject(new Error(`telegram status ${res.statusCode}: ${data}`));
94
+ });
95
+ });
96
+ req.on("error", reject);
97
+ req.on("timeout", () => {
98
+ req.destroy(new Error("telegram timeout"));
99
+ });
100
+ req.write(payload);
101
+ req.end();
102
+ });
103
+ }
104
+
105
+ async function sendWithRetry(options, retry = 2, backoffMs = 1000, sendFn = sendTelegramMessage) {
106
+ let attempt = 0;
107
+ while (true) {
108
+ try {
109
+ return await sendFn(options);
110
+ } catch (err) {
111
+ attempt += 1;
112
+ if (attempt > retry) throw err;
113
+ await new Promise((resolve) => setTimeout(resolve, backoffMs * attempt));
114
+ }
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ escapeMarkdownV2,
120
+ escapeMarkdownV2Code,
121
+ formatTelegramMarkdownEvent,
122
+ sendTelegramMessage,
123
+ sendWithRetry,
124
+ };