@dev-anywhere/proxy 0.1.6 → 0.1.8
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/{chunk-TX6HNHDB.js → chunk-2SBGSJLQ.js} +2 -2
- package/dist/{chunk-RFBTVZ2X.js → chunk-2XO3KLWW.js} +27 -338
- package/dist/chunk-2XO3KLWW.js.map +1 -0
- package/dist/chunk-BLWDLNT6.js +346 -0
- package/dist/chunk-BLWDLNT6.js.map +1 -0
- package/dist/chunk-GTTLWHIG.js +84 -0
- package/dist/chunk-GTTLWHIG.js.map +1 -0
- package/dist/{chunk-JGGDVMY5.js → chunk-ORZTFYXR.js} +2 -21
- package/dist/chunk-ORZTFYXR.js.map +1 -0
- package/dist/{chunk-ODK6N2NP.js → chunk-PDX6QFJ7.js} +2 -2
- package/dist/chunk-R4S6OFIZ.js +195 -0
- package/dist/chunk-R4S6OFIZ.js.map +1 -0
- package/dist/index.js +25 -13
- package/dist/index.js.map +1 -1
- package/dist/relay-token-KQPEQVP7.js +81 -0
- package/dist/relay-token-KQPEQVP7.js.map +1 -0
- package/dist/serve.js +54 -200
- package/dist/serve.js.map +1 -1
- package/dist/session-worker.js +3 -2
- package/dist/session-worker.js.map +1 -1
- package/dist/{terminal-ES6I5W32.js → terminal-4MDRBCL4.js} +13 -9
- package/dist/{terminal-ES6I5W32.js.map → terminal-4MDRBCL4.js.map} +1 -1
- package/package.json +3 -3
- package/dist/chunk-JGGDVMY5.js.map +0 -1
- package/dist/chunk-RFBTVZ2X.js.map +0 -1
- /package/dist/{chunk-TX6HNHDB.js.map → chunk-2SBGSJLQ.js.map} +0 -0
- /package/dist/{chunk-ODK6N2NP.js.map → chunk-PDX6QFJ7.js.map} +0 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
SessionState
|
|
4
|
+
} from "./chunk-2XO3KLWW.js";
|
|
5
|
+
|
|
6
|
+
// src/ipc/ipc-protocol.ts
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
// src/ipc/line-buffer.ts
|
|
10
|
+
import { Transform } from "stream";
|
|
11
|
+
var LineBuffer = class extends Transform {
|
|
12
|
+
buffer = "";
|
|
13
|
+
_transform(chunk, _encoding, callback) {
|
|
14
|
+
this.buffer += typeof chunk === "string" ? chunk : chunk.toString();
|
|
15
|
+
const segments = this.buffer.split("\n");
|
|
16
|
+
this.buffer = segments.pop();
|
|
17
|
+
for (const segment of segments) {
|
|
18
|
+
if (segment.length > 0) {
|
|
19
|
+
this.push(segment);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
callback();
|
|
23
|
+
}
|
|
24
|
+
_flush(callback) {
|
|
25
|
+
if (this.buffer.length > 0) {
|
|
26
|
+
this.push(this.buffer);
|
|
27
|
+
this.buffer = "";
|
|
28
|
+
}
|
|
29
|
+
callback();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/ipc/ipc-protocol.ts
|
|
34
|
+
var IPC_BINARY_MARKER = 0;
|
|
35
|
+
function encodeBinaryIpcFrame(sessionId, data, outputSeq) {
|
|
36
|
+
const sessionIdBuf = Buffer.from(sessionId, "utf-8");
|
|
37
|
+
const payloadLen = 1 + sessionIdBuf.length + 4 + data.length;
|
|
38
|
+
const frame = Buffer.alloc(1 + 4 + payloadLen);
|
|
39
|
+
let offset = 0;
|
|
40
|
+
frame[offset] = IPC_BINARY_MARKER;
|
|
41
|
+
offset += 1;
|
|
42
|
+
frame.writeUInt32LE(payloadLen, offset);
|
|
43
|
+
offset += 4;
|
|
44
|
+
frame[offset] = sessionIdBuf.length;
|
|
45
|
+
offset += 1;
|
|
46
|
+
sessionIdBuf.copy(frame, offset);
|
|
47
|
+
offset += sessionIdBuf.length;
|
|
48
|
+
frame.writeUInt32LE(outputSeq, offset);
|
|
49
|
+
offset += 4;
|
|
50
|
+
data.copy(frame, offset);
|
|
51
|
+
return frame;
|
|
52
|
+
}
|
|
53
|
+
var sessionStateValues = Object.values(SessionState);
|
|
54
|
+
var ProviderHookContextSchema = z.object({
|
|
55
|
+
provider: z.enum(["claude", "codex"]),
|
|
56
|
+
sessionId: z.string(),
|
|
57
|
+
hookUrl: z.string(),
|
|
58
|
+
marker: z.string(),
|
|
59
|
+
token: z.string()
|
|
60
|
+
});
|
|
61
|
+
var IpcMessageSchema = z.discriminatedUnion("type", [
|
|
62
|
+
// 客户端请求创建新会话,sessionId 可选用于重连时复用
|
|
63
|
+
z.object({
|
|
64
|
+
type: z.literal("session_create_request"),
|
|
65
|
+
name: z.string().optional(),
|
|
66
|
+
mode: z.enum(["pty", "json"]),
|
|
67
|
+
provider: z.enum(["claude", "codex"]),
|
|
68
|
+
cwd: z.string(),
|
|
69
|
+
pid: z.number(),
|
|
70
|
+
sessionId: z.string().optional()
|
|
71
|
+
}),
|
|
72
|
+
// 服务端响应创建会话
|
|
73
|
+
z.object({
|
|
74
|
+
type: z.literal("session_create_response"),
|
|
75
|
+
sessionId: z.string(),
|
|
76
|
+
error: z.string().optional(),
|
|
77
|
+
hook: ProviderHookContextSchema.optional()
|
|
78
|
+
}),
|
|
79
|
+
// 客户端请求终止会话
|
|
80
|
+
z.object({
|
|
81
|
+
type: z.literal("session_terminate_request"),
|
|
82
|
+
sessionId: z.string()
|
|
83
|
+
}),
|
|
84
|
+
// 服务端响应终止会话
|
|
85
|
+
z.object({
|
|
86
|
+
type: z.literal("session_terminate_response"),
|
|
87
|
+
sessionId: z.string(),
|
|
88
|
+
success: z.boolean()
|
|
89
|
+
}),
|
|
90
|
+
// 客户端向服务端注册 PTY 会话
|
|
91
|
+
z.object({
|
|
92
|
+
type: z.literal("pty_register"),
|
|
93
|
+
sessionId: z.string(),
|
|
94
|
+
pid: z.number()
|
|
95
|
+
}),
|
|
96
|
+
// 客户端取消注册 PTY 会话
|
|
97
|
+
z.object({
|
|
98
|
+
type: z.literal("pty_deregister"),
|
|
99
|
+
sessionId: z.string()
|
|
100
|
+
}),
|
|
101
|
+
// 输入,从服务端转发到客户端的 PTY stdin(手机远程输入注入)
|
|
102
|
+
z.object({
|
|
103
|
+
type: z.literal("pty_input"),
|
|
104
|
+
sessionId: z.string(),
|
|
105
|
+
data: z.string()
|
|
106
|
+
}),
|
|
107
|
+
// serve → terminal:Web 端移除本地终端会话时,只断开远程视图,不杀本地 CLI。
|
|
108
|
+
z.object({
|
|
109
|
+
type: z.literal("pty_detach"),
|
|
110
|
+
sessionId: z.string()
|
|
111
|
+
}),
|
|
112
|
+
// 服务端广播会话状态变更
|
|
113
|
+
z.object({
|
|
114
|
+
type: z.literal("session_status_update"),
|
|
115
|
+
sessionId: z.string(),
|
|
116
|
+
state: z.enum(sessionStateValues)
|
|
117
|
+
}),
|
|
118
|
+
// 错误响应
|
|
119
|
+
z.object({
|
|
120
|
+
type: z.literal("error"),
|
|
121
|
+
message: z.string(),
|
|
122
|
+
code: z.string().optional()
|
|
123
|
+
}),
|
|
124
|
+
// 客户端请求服务状态(含 relay 连接信息和 worker 状态)
|
|
125
|
+
z.object({
|
|
126
|
+
type: z.literal("service_status_request")
|
|
127
|
+
}),
|
|
128
|
+
// 服务端响应增强版服务状态
|
|
129
|
+
z.object({
|
|
130
|
+
type: z.literal("service_status_response"),
|
|
131
|
+
config: z.object({
|
|
132
|
+
profile: z.string().optional(),
|
|
133
|
+
relayName: z.string(),
|
|
134
|
+
relayNameSource: z.enum(["cli", "profile"]),
|
|
135
|
+
relayUrl: z.string().optional(),
|
|
136
|
+
relayUrlSource: z.enum(["env", "file", "none"]),
|
|
137
|
+
relayTokenSource: z.enum(["env", "file", "none"]),
|
|
138
|
+
hookPort: z.number(),
|
|
139
|
+
hookPortSource: z.enum(["env", "file", "default"])
|
|
140
|
+
}),
|
|
141
|
+
relay: z.object({
|
|
142
|
+
connected: z.boolean(),
|
|
143
|
+
proxyId: z.string(),
|
|
144
|
+
reconnectAttempt: z.number(),
|
|
145
|
+
queueDepth: z.number()
|
|
146
|
+
}).nullable(),
|
|
147
|
+
sessions: z.array(
|
|
148
|
+
z.object({
|
|
149
|
+
id: z.string(),
|
|
150
|
+
mode: z.enum(["pty", "json"]),
|
|
151
|
+
state: z.enum(sessionStateValues),
|
|
152
|
+
createdAt: z.string(),
|
|
153
|
+
name: z.string().optional(),
|
|
154
|
+
hasWorker: z.boolean()
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
}),
|
|
158
|
+
// terminal → serve:终端标题变化,由 xterm onTitleChange 触发
|
|
159
|
+
z.object({
|
|
160
|
+
type: z.literal("pty_title_change"),
|
|
161
|
+
sessionId: z.string(),
|
|
162
|
+
title: z.string()
|
|
163
|
+
}),
|
|
164
|
+
// terminal → serve:local runtime 观察到的 PTY 语义事件。
|
|
165
|
+
// 下面 state 枚举必须与 src/common/osc-extractor.ts 的 PtySemanticState 保持一致。
|
|
166
|
+
z.object({
|
|
167
|
+
type: z.literal("pty_semantic_event"),
|
|
168
|
+
sessionId: z.string(),
|
|
169
|
+
state: z.enum(["working", "turn_complete", "approval_wait", "mid_pause"]),
|
|
170
|
+
title: z.string().optional(),
|
|
171
|
+
tool: z.string().optional()
|
|
172
|
+
}),
|
|
173
|
+
// terminal → serve:终端尺寸变化
|
|
174
|
+
z.object({
|
|
175
|
+
type: z.literal("pty_resize"),
|
|
176
|
+
sessionId: z.string(),
|
|
177
|
+
cols: z.number(),
|
|
178
|
+
rows: z.number()
|
|
179
|
+
}),
|
|
180
|
+
// serve → terminal:请求 HeadlessTerminal serialize() 快照
|
|
181
|
+
z.object({
|
|
182
|
+
type: z.literal("pty_subscribe"),
|
|
183
|
+
sessionId: z.string(),
|
|
184
|
+
requestId: z.string().optional()
|
|
185
|
+
}),
|
|
186
|
+
// terminal → serve:serialize() 结果
|
|
187
|
+
z.object({
|
|
188
|
+
type: z.literal("pty_snapshot"),
|
|
189
|
+
sessionId: z.string(),
|
|
190
|
+
cols: z.number(),
|
|
191
|
+
rows: z.number(),
|
|
192
|
+
data: z.string(),
|
|
193
|
+
outputSeq: z.number().int().nonnegative(),
|
|
194
|
+
requestId: z.string().optional()
|
|
195
|
+
}),
|
|
196
|
+
// serve → terminal:relay 连接状态变更,供终端给用户显示 remote viewing 是否通畅
|
|
197
|
+
z.object({
|
|
198
|
+
type: z.literal("bridge_status"),
|
|
199
|
+
connected: z.boolean()
|
|
200
|
+
})
|
|
201
|
+
]);
|
|
202
|
+
var WorkerMessageSchema = z.discriminatedUnion("type", [
|
|
203
|
+
// serve → worker: 发送用户输入给 claude
|
|
204
|
+
z.object({
|
|
205
|
+
type: z.literal("worker_input"),
|
|
206
|
+
content: z.string()
|
|
207
|
+
}),
|
|
208
|
+
// serve → worker: 停止 claude 进程
|
|
209
|
+
z.object({
|
|
210
|
+
type: z.literal("worker_stop")
|
|
211
|
+
}),
|
|
212
|
+
// serve → worker: 工具审批响应
|
|
213
|
+
z.object({
|
|
214
|
+
type: z.literal("worker_approval_response"),
|
|
215
|
+
requestId: z.string(),
|
|
216
|
+
behavior: z.enum(["allow", "deny"]),
|
|
217
|
+
message: z.string().optional()
|
|
218
|
+
}),
|
|
219
|
+
// worker → serve: claude 输出事件(带序列号)
|
|
220
|
+
z.object({
|
|
221
|
+
type: z.literal("worker_event"),
|
|
222
|
+
seq: z.number(),
|
|
223
|
+
event: z.record(z.string(), z.unknown())
|
|
224
|
+
}),
|
|
225
|
+
// worker → serve: claude 进程退出
|
|
226
|
+
z.object({
|
|
227
|
+
type: z.literal("worker_exit"),
|
|
228
|
+
code: z.number()
|
|
229
|
+
}),
|
|
230
|
+
// worker → serve: 工具审批请求
|
|
231
|
+
z.object({
|
|
232
|
+
type: z.literal("worker_approval_request"),
|
|
233
|
+
requestId: z.string(),
|
|
234
|
+
toolName: z.string(),
|
|
235
|
+
input: z.record(z.string(), z.unknown())
|
|
236
|
+
}),
|
|
237
|
+
// worker → serve: worker 就绪,claude 已启动
|
|
238
|
+
z.object({
|
|
239
|
+
type: z.literal("worker_ready"),
|
|
240
|
+
pid: z.number()
|
|
241
|
+
}),
|
|
242
|
+
// worker → serve: 从 stream-json 的 system.init 事件捕获 Claude CLI 侧的 session ID
|
|
243
|
+
// proxy 拿它来读 ~/.claude/projects/.../<id>.jsonl 历史或后续 --resume
|
|
244
|
+
z.object({
|
|
245
|
+
type: z.literal("worker_claude_session_id"),
|
|
246
|
+
sessionId: z.string()
|
|
247
|
+
}),
|
|
248
|
+
// serve → worker: 将指定工具加入会话白名单,后续同名工具自动审批
|
|
249
|
+
z.object({
|
|
250
|
+
type: z.literal("worker_whitelist_add"),
|
|
251
|
+
toolName: z.string()
|
|
252
|
+
})
|
|
253
|
+
]);
|
|
254
|
+
function serializeWorkerMsg(msg) {
|
|
255
|
+
return JSON.stringify(msg) + "\n";
|
|
256
|
+
}
|
|
257
|
+
function createWorkerReader(stream, onMessage) {
|
|
258
|
+
const lineBuffer = new LineBuffer();
|
|
259
|
+
lineBuffer.on("data", (line) => {
|
|
260
|
+
const str = typeof line === "string" ? line : line.toString();
|
|
261
|
+
if (str.length === 0) return;
|
|
262
|
+
try {
|
|
263
|
+
const raw = JSON.parse(str);
|
|
264
|
+
const result = WorkerMessageSchema.safeParse(raw);
|
|
265
|
+
if (result.success) {
|
|
266
|
+
onMessage(result.data);
|
|
267
|
+
} else {
|
|
268
|
+
stream.emit(
|
|
269
|
+
"error",
|
|
270
|
+
new Error(`Worker message validation failed: ${result.error.message}`)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
stream.emit("error", new Error("Worker message parse error", { cause: err }));
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
stream.pipe(lineBuffer);
|
|
278
|
+
}
|
|
279
|
+
function serializeIpc(msg) {
|
|
280
|
+
return JSON.stringify(msg) + "\n";
|
|
281
|
+
}
|
|
282
|
+
function createIpcReader(stream, onMessage, onBinaryFrame) {
|
|
283
|
+
let buf = Buffer.alloc(0);
|
|
284
|
+
let disposed = false;
|
|
285
|
+
function drain() {
|
|
286
|
+
while (buf.length > 0) {
|
|
287
|
+
if (buf[0] === IPC_BINARY_MARKER) {
|
|
288
|
+
if (buf.length < 5) return;
|
|
289
|
+
const payloadLen = buf.readUInt32LE(1);
|
|
290
|
+
const totalFrameLen = 1 + 4 + payloadLen;
|
|
291
|
+
if (buf.length < totalFrameLen) return;
|
|
292
|
+
const payloadStart = 5;
|
|
293
|
+
const sessionIdLen = buf[payloadStart];
|
|
294
|
+
const sessionId = buf.subarray(payloadStart + 1, payloadStart + 1 + sessionIdLen).toString("utf-8");
|
|
295
|
+
const seqOffset = payloadStart + 1 + sessionIdLen;
|
|
296
|
+
const outputSeq = buf.readUInt32LE(seqOffset);
|
|
297
|
+
const ptyData = Buffer.from(buf.subarray(seqOffset + 4, totalFrameLen));
|
|
298
|
+
if (onBinaryFrame) {
|
|
299
|
+
onBinaryFrame(sessionId, ptyData, outputSeq);
|
|
300
|
+
}
|
|
301
|
+
buf = buf.subarray(totalFrameLen);
|
|
302
|
+
} else {
|
|
303
|
+
const newlineIdx = buf.indexOf(10);
|
|
304
|
+
if (newlineIdx === -1) return;
|
|
305
|
+
const line = buf.subarray(0, newlineIdx).toString("utf-8");
|
|
306
|
+
buf = buf.subarray(newlineIdx + 1);
|
|
307
|
+
if (line.length === 0) continue;
|
|
308
|
+
try {
|
|
309
|
+
const raw = JSON.parse(line);
|
|
310
|
+
const result = IpcMessageSchema.safeParse(raw);
|
|
311
|
+
if (result.success) {
|
|
312
|
+
onMessage(result.data);
|
|
313
|
+
} else {
|
|
314
|
+
stream.emit(
|
|
315
|
+
"error",
|
|
316
|
+
new Error(`IPC message validation failed: ${result.error.message}`)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
} catch (err) {
|
|
320
|
+
stream.emit("error", new Error("IPC message parse error", { cause: err }));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function onData(chunk) {
|
|
326
|
+
if (disposed) return;
|
|
327
|
+
const incoming = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
328
|
+
buf = Buffer.concat([buf, incoming]);
|
|
329
|
+
drain();
|
|
330
|
+
}
|
|
331
|
+
stream.on("data", onData);
|
|
332
|
+
return () => {
|
|
333
|
+
disposed = true;
|
|
334
|
+
stream.off("data", onData);
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export {
|
|
339
|
+
LineBuffer,
|
|
340
|
+
encodeBinaryIpcFrame,
|
|
341
|
+
serializeWorkerMsg,
|
|
342
|
+
createWorkerReader,
|
|
343
|
+
serializeIpc,
|
|
344
|
+
createIpcReader
|
|
345
|
+
};
|
|
346
|
+
//# sourceMappingURL=chunk-BLWDLNT6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ipc/ipc-protocol.ts","../src/ipc/line-buffer.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { SessionState } from \"@dev-anywhere/shared\";\nimport { LineBuffer } from \"./line-buffer.js\";\n\n// IPC binary 帧标记字节,0x00 不可能是 JSON 行的首字节(JSON 以 '{' 开头)\nexport const IPC_BINARY_MARKER = 0x00;\n\n// 编码 binary PTY 数据帧用于 IPC 传输\n// 格式: [1B 0x00 marker][4B payload_len uint32LE][1B sessionId_len][sessionId UTF-8][4B outputSeq uint32LE][PTY data]\nexport function encodeBinaryIpcFrame(sessionId: string, data: Buffer, outputSeq: number): Buffer {\n const sessionIdBuf = Buffer.from(sessionId, \"utf-8\");\n const payloadLen = 1 + sessionIdBuf.length + 4 + data.length;\n const frame = Buffer.alloc(1 + 4 + payloadLen);\n let offset = 0;\n frame[offset] = IPC_BINARY_MARKER;\n offset += 1;\n frame.writeUInt32LE(payloadLen, offset);\n offset += 4;\n frame[offset] = sessionIdBuf.length;\n offset += 1;\n sessionIdBuf.copy(frame, offset);\n offset += sessionIdBuf.length;\n frame.writeUInt32LE(outputSeq, offset);\n offset += 4;\n data.copy(frame, offset);\n return frame;\n}\n\nconst sessionStateValues = Object.values(SessionState) as [SessionState, ...SessionState[]];\n\nconst ProviderHookContextSchema = z.object({\n provider: z.enum([\"claude\", \"codex\"]),\n sessionId: z.string(),\n hookUrl: z.string(),\n marker: z.string(),\n token: z.string(),\n});\n\n// IPC 消息 schema,客户端与服务端通过 Unix domain socket 使用 NDJSON 通信\nexport const IpcMessageSchema = z.discriminatedUnion(\"type\", [\n // 客户端请求创建新会话,sessionId 可选用于重连时复用\n z.object({\n type: z.literal(\"session_create_request\"),\n name: z.string().optional(),\n mode: z.enum([\"pty\", \"json\"]),\n provider: z.enum([\"claude\", \"codex\"]),\n cwd: z.string(),\n pid: z.number(),\n sessionId: z.string().optional(),\n }),\n\n // 服务端响应创建会话\n z.object({\n type: z.literal(\"session_create_response\"),\n sessionId: z.string(),\n error: z.string().optional(),\n hook: ProviderHookContextSchema.optional(),\n }),\n\n // 客户端请求终止会话\n z.object({\n type: z.literal(\"session_terminate_request\"),\n sessionId: z.string(),\n }),\n\n // 服务端响应终止会话\n z.object({\n type: z.literal(\"session_terminate_response\"),\n sessionId: z.string(),\n success: z.boolean(),\n }),\n\n // 客户端向服务端注册 PTY 会话\n z.object({\n type: z.literal(\"pty_register\"),\n sessionId: z.string(),\n pid: z.number(),\n }),\n\n // 客户端取消注册 PTY 会话\n z.object({\n type: z.literal(\"pty_deregister\"),\n sessionId: z.string(),\n }),\n\n // 输入,从服务端转发到客户端的 PTY stdin(手机远程输入注入)\n z.object({\n type: z.literal(\"pty_input\"),\n sessionId: z.string(),\n data: z.string(),\n }),\n\n // serve → terminal:Web 端移除本地终端会话时,只断开远程视图,不杀本地 CLI。\n z.object({\n type: z.literal(\"pty_detach\"),\n sessionId: z.string(),\n }),\n\n // 服务端广播会话状态变更\n z.object({\n type: z.literal(\"session_status_update\"),\n sessionId: z.string(),\n state: z.enum(sessionStateValues),\n }),\n\n // 错误响应\n z.object({\n type: z.literal(\"error\"),\n message: z.string(),\n code: z.string().optional(),\n }),\n\n // 客户端请求服务状态(含 relay 连接信息和 worker 状态)\n z.object({\n type: z.literal(\"service_status_request\"),\n }),\n\n // 服务端响应增强版服务状态\n z.object({\n type: z.literal(\"service_status_response\"),\n config: z.object({\n profile: z.string().optional(),\n relayName: z.string(),\n relayNameSource: z.enum([\"cli\", \"profile\"]),\n relayUrl: z.string().optional(),\n relayUrlSource: z.enum([\"env\", \"file\", \"none\"]),\n relayTokenSource: z.enum([\"env\", \"file\", \"none\"]),\n hookPort: z.number(),\n hookPortSource: z.enum([\"env\", \"file\", \"default\"]),\n }),\n relay: z\n .object({\n connected: z.boolean(),\n proxyId: z.string(),\n reconnectAttempt: z.number(),\n queueDepth: z.number(),\n })\n .nullable(),\n sessions: z.array(\n z.object({\n id: z.string(),\n mode: z.enum([\"pty\", \"json\"]),\n state: z.enum(sessionStateValues),\n createdAt: z.string(),\n name: z.string().optional(),\n hasWorker: z.boolean(),\n }),\n ),\n }),\n\n // terminal → serve:终端标题变化,由 xterm onTitleChange 触发\n z.object({\n type: z.literal(\"pty_title_change\"),\n sessionId: z.string(),\n title: z.string(),\n }),\n\n // terminal → serve:local runtime 观察到的 PTY 语义事件。\n // 下面 state 枚举必须与 src/common/osc-extractor.ts 的 PtySemanticState 保持一致。\n z.object({\n type: z.literal(\"pty_semantic_event\"),\n sessionId: z.string(),\n state: z.enum([\"working\", \"turn_complete\", \"approval_wait\", \"mid_pause\"]),\n title: z.string().optional(),\n tool: z.string().optional(),\n }),\n\n // terminal → serve:终端尺寸变化\n z.object({\n type: z.literal(\"pty_resize\"),\n sessionId: z.string(),\n cols: z.number(),\n rows: z.number(),\n }),\n\n // serve → terminal:请求 HeadlessTerminal serialize() 快照\n z.object({\n type: z.literal(\"pty_subscribe\"),\n sessionId: z.string(),\n requestId: z.string().optional(),\n }),\n\n // terminal → serve:serialize() 结果\n z.object({\n type: z.literal(\"pty_snapshot\"),\n sessionId: z.string(),\n cols: z.number(),\n rows: z.number(),\n data: z.string(),\n outputSeq: z.number().int().nonnegative(),\n requestId: z.string().optional(),\n }),\n\n // serve → terminal:relay 连接状态变更,供终端给用户显示 remote viewing 是否通畅\n z.object({\n type: z.literal(\"bridge_status\"),\n connected: z.boolean(),\n }),\n]);\n\n// serve 与 session-worker 之间的通信协议\nexport const WorkerMessageSchema = z.discriminatedUnion(\"type\", [\n // serve → worker: 发送用户输入给 claude\n z.object({\n type: z.literal(\"worker_input\"),\n content: z.string(),\n }),\n\n // serve → worker: 停止 claude 进程\n z.object({\n type: z.literal(\"worker_stop\"),\n }),\n\n // serve → worker: 工具审批响应\n z.object({\n type: z.literal(\"worker_approval_response\"),\n requestId: z.string(),\n behavior: z.enum([\"allow\", \"deny\"]),\n message: z.string().optional(),\n }),\n\n // worker → serve: claude 输出事件(带序列号)\n z.object({\n type: z.literal(\"worker_event\"),\n seq: z.number(),\n event: z.record(z.string(), z.unknown()),\n }),\n\n // worker → serve: claude 进程退出\n z.object({\n type: z.literal(\"worker_exit\"),\n code: z.number(),\n }),\n\n // worker → serve: 工具审批请求\n z.object({\n type: z.literal(\"worker_approval_request\"),\n requestId: z.string(),\n toolName: z.string(),\n input: z.record(z.string(), z.unknown()),\n }),\n\n // worker → serve: worker 就绪,claude 已启动\n z.object({\n type: z.literal(\"worker_ready\"),\n pid: z.number(),\n }),\n\n // worker → serve: 从 stream-json 的 system.init 事件捕获 Claude CLI 侧的 session ID\n // proxy 拿它来读 ~/.claude/projects/.../<id>.jsonl 历史或后续 --resume\n z.object({\n type: z.literal(\"worker_claude_session_id\"),\n sessionId: z.string(),\n }),\n\n // serve → worker: 将指定工具加入会话白名单,后续同名工具自动审批\n z.object({\n type: z.literal(\"worker_whitelist_add\"),\n toolName: z.string(),\n }),\n]);\n\nexport type WorkerMessage = z.infer<typeof WorkerMessageSchema>;\n\nexport function serializeWorkerMsg(msg: WorkerMessage): string {\n return JSON.stringify(msg) + \"\\n\";\n}\n\nexport function createWorkerReader(\n stream: NodeJS.ReadableStream,\n onMessage: (msg: WorkerMessage) => void,\n): void {\n const lineBuffer = new LineBuffer();\n lineBuffer.on(\"data\", (line: Buffer | string) => {\n const str = typeof line === \"string\" ? line : line.toString();\n if (str.length === 0) return;\n try {\n const raw = JSON.parse(str);\n const result = WorkerMessageSchema.safeParse(raw);\n if (result.success) {\n onMessage(result.data);\n } else {\n stream.emit(\n \"error\",\n new Error(`Worker message validation failed: ${result.error.message}`),\n );\n }\n } catch (err) {\n stream.emit(\"error\", new Error(\"Worker message parse error\", { cause: err }));\n }\n });\n (stream as NodeJS.ReadableStream).pipe(lineBuffer);\n}\n\nexport type IpcMessage = z.infer<typeof IpcMessageSchema>;\n\n// 将 IPC 消息序列化为 NDJSON 格式的字符串\nexport function serializeIpc(msg: IpcMessage): string {\n return JSON.stringify(msg) + \"\\n\";\n}\n\n// 混合协议 IPC 读取器,支持 NDJSON 控制消息和 binary PTY 帧。\n// binary 帧以 0x00 开头,NDJSON 行以 '{' 开头,通过首字节区分。\n// 返回 dispose 函数用于摘掉 'data' 监听,长连接可以忽略,一次性等待(如 waitForMessage)必须调用避免累积 listener 重复解析每条消息。\nexport function createIpcReader(\n stream: NodeJS.ReadableStream,\n onMessage: (msg: IpcMessage) => void,\n onBinaryFrame?: (sessionId: string, data: Buffer, outputSeq: number) => void,\n): () => void {\n let buf = Buffer.alloc(0);\n let disposed = false;\n\n // 解析状态机:不断消费 buf 中的完整消息\n function drain(): void {\n while (buf.length > 0) {\n if (buf[0] === IPC_BINARY_MARKER) {\n // binary 帧: [1B marker][4B payload_len LE][payload]\n // 需要至少 5 字节才能读取 header\n if (buf.length < 5) return;\n const payloadLen = buf.readUInt32LE(1);\n const totalFrameLen = 1 + 4 + payloadLen;\n if (buf.length < totalFrameLen) return;\n\n // 解析 payload: [1B sessionId_len][sessionId][4B outputSeq][pty data]\n const payloadStart = 5;\n const sessionIdLen = buf[payloadStart];\n const sessionId = buf\n .subarray(payloadStart + 1, payloadStart + 1 + sessionIdLen)\n .toString(\"utf-8\");\n const seqOffset = payloadStart + 1 + sessionIdLen;\n const outputSeq = buf.readUInt32LE(seqOffset);\n const ptyData = Buffer.from(buf.subarray(seqOffset + 4, totalFrameLen));\n\n if (onBinaryFrame) {\n onBinaryFrame(sessionId, ptyData, outputSeq);\n }\n\n buf = buf.subarray(totalFrameLen);\n } else {\n // NDJSON 行: 找 \\n 分隔符\n const newlineIdx = buf.indexOf(0x0a); // '\\n'\n if (newlineIdx === -1) return;\n\n const line = buf.subarray(0, newlineIdx).toString(\"utf-8\");\n buf = buf.subarray(newlineIdx + 1);\n\n if (line.length === 0) continue;\n\n try {\n const raw = JSON.parse(line);\n const result = IpcMessageSchema.safeParse(raw);\n if (result.success) {\n onMessage(result.data);\n } else {\n stream.emit(\n \"error\",\n new Error(`IPC message validation failed: ${result.error.message}`),\n );\n }\n } catch (err) {\n stream.emit(\"error\", new Error(\"IPC message parse error\", { cause: err }));\n }\n }\n }\n }\n\n function onData(chunk: Buffer | string): void {\n if (disposed) return;\n const incoming = typeof chunk === \"string\" ? Buffer.from(chunk) : chunk;\n buf = Buffer.concat([buf, incoming]);\n drain();\n }\n\n stream.on(\"data\", onData);\n\n return () => {\n disposed = true;\n stream.off(\"data\", onData);\n };\n}\n","import { Transform, type TransformCallback } from \"node:stream\";\n\n// 将任意 data 事件分割为完整的 \\n 分隔行\n// Node.js data 事件不保证按行边界分割,此 Transform 保证每次 push 一个完整行\nexport class LineBuffer extends Transform {\n private buffer = \"\";\n\n _transform(chunk: Buffer | string, _encoding: BufferEncoding, callback: TransformCallback): void {\n this.buffer += typeof chunk === \"string\" ? chunk : chunk.toString();\n const segments = this.buffer.split(\"\\n\");\n // 最后一段可能是不完整行,保留到 buffer\n this.buffer = segments.pop()!;\n\n for (const segment of segments) {\n if (segment.length > 0) {\n this.push(segment);\n }\n }\n callback();\n }\n\n _flush(callback: TransformCallback): void {\n if (this.buffer.length > 0) {\n this.push(this.buffer);\n this.buffer = \"\";\n }\n callback();\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CONFIG_PATH,
|
|
4
|
+
LOG_DIR,
|
|
5
|
+
createLogger
|
|
6
|
+
} from "./chunk-2XO3KLWW.js";
|
|
7
|
+
|
|
8
|
+
// src/common/logger.ts
|
|
9
|
+
import { existsSync, readFileSync } from "fs";
|
|
10
|
+
|
|
11
|
+
// src/common/runtime-env.ts
|
|
12
|
+
var VALID_LOG_LEVELS = [
|
|
13
|
+
"trace",
|
|
14
|
+
"debug",
|
|
15
|
+
"info",
|
|
16
|
+
"warn",
|
|
17
|
+
"error",
|
|
18
|
+
"fatal",
|
|
19
|
+
"silent"
|
|
20
|
+
];
|
|
21
|
+
function parseLogLevel(value) {
|
|
22
|
+
if (!value) return void 0;
|
|
23
|
+
if (VALID_LOG_LEVELS.includes(value)) return value;
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid LOG_LEVEL=${JSON.stringify(value)}; expected one of ${VALID_LOG_LEVELS.join(", ")}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function parsePort(value, source) {
|
|
29
|
+
if (!value) return void 0;
|
|
30
|
+
const port = Number(value);
|
|
31
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
32
|
+
throw new Error(`Invalid ${source}=${JSON.stringify(value)}: expected TCP port 1-65535`);
|
|
33
|
+
}
|
|
34
|
+
return port;
|
|
35
|
+
}
|
|
36
|
+
function nonEmpty(value) {
|
|
37
|
+
return value && value.length > 0 ? value : void 0;
|
|
38
|
+
}
|
|
39
|
+
function loadProxyRuntimeEnv(env2 = process.env) {
|
|
40
|
+
return {
|
|
41
|
+
relayUrl: nonEmpty(env2.RELAY_URL),
|
|
42
|
+
relayProxyToken: nonEmpty(env2.RELAY_PROXY_TOKEN),
|
|
43
|
+
hookPort: parsePort(env2.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT"),
|
|
44
|
+
claudeBin: nonEmpty(env2.CLAUDE_BIN),
|
|
45
|
+
codexBin: nonEmpty(env2.CODEX_BIN),
|
|
46
|
+
logLevel: parseLogLevel(env2.LOG_LEVEL),
|
|
47
|
+
isVitest: !!env2.VITEST
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/common/logger.ts
|
|
52
|
+
var env = loadProxyRuntimeEnv();
|
|
53
|
+
function readConfigLogLevel() {
|
|
54
|
+
if (env.logLevel) return void 0;
|
|
55
|
+
if (!existsSync(CONFIG_PATH)) return void 0;
|
|
56
|
+
try {
|
|
57
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
58
|
+
if (typeof raw.logLevel !== "string") return void 0;
|
|
59
|
+
return VALID_LOG_LEVELS.includes(raw.logLevel) ? raw.logLevel : void 0;
|
|
60
|
+
} catch {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var overrideLevel = env.logLevel ?? readConfigLogLevel();
|
|
65
|
+
var serviceLogger = createLogger({
|
|
66
|
+
name: "service",
|
|
67
|
+
level: overrideLevel ?? "info",
|
|
68
|
+
logDir: LOG_DIR,
|
|
69
|
+
silent: env.isVitest
|
|
70
|
+
});
|
|
71
|
+
var terminalLogger = createLogger({
|
|
72
|
+
name: "terminal",
|
|
73
|
+
level: overrideLevel ?? "debug",
|
|
74
|
+
logDir: LOG_DIR,
|
|
75
|
+
silent: env.isVitest
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
VALID_LOG_LEVELS,
|
|
80
|
+
loadProxyRuntimeEnv,
|
|
81
|
+
serviceLogger,
|
|
82
|
+
terminalLogger
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=chunk-GTTLWHIG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/common/logger.ts","../src/common/runtime-env.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { createLogger } from \"@dev-anywhere/shared\";\nimport { CONFIG_PATH, LOG_DIR } from \"./paths.js\";\nimport { loadProxyRuntimeEnv, VALID_LOG_LEVELS } from \"./runtime-env.js\";\n\nconst env = loadProxyRuntimeEnv();\n\n// 直接读 config.json 而不走 config.ts loadConfig:loadConfig 依赖 logger.ts,\n// 反向引用会形成循环。这里只取 logLevel 字段做 best-effort 降级,全量 schema 校验仍由\n// config.ts 在第一次 loadConfig 时执行。\nfunction readConfigLogLevel(): string | undefined {\n if (env.logLevel) return undefined;\n if (!existsSync(CONFIG_PATH)) return undefined;\n try {\n const raw = JSON.parse(readFileSync(CONFIG_PATH, \"utf-8\")) as { logLevel?: unknown };\n if (typeof raw.logLevel !== \"string\") return undefined;\n return (VALID_LOG_LEVELS as readonly string[]).includes(raw.logLevel)\n ? raw.logLevel\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n// 三级 precedence:LOG_LEVEL env > config.logLevel > 各 logger 自己的默认。\nconst overrideLevel = env.logLevel ?? readConfigLogLevel();\n\nexport const serviceLogger = createLogger({\n name: \"service\",\n level: overrideLevel ?? \"info\",\n logDir: LOG_DIR,\n silent: env.isVitest,\n});\n\nexport const terminalLogger = createLogger({\n name: \"terminal\",\n level: overrideLevel ?? \"debug\",\n logDir: LOG_DIR,\n silent: env.isVitest,\n});\n","// 单一入口集中读取 proxy 运行时关心的环境变量,类型化输出,避免 process.env.X\n// 散落在多个文件里各自做 parseInt / 空串判断 / undefined 兜底。\n//\n// 范围:用户面对的运行时旋钮 + 构建/测试模式标志。\n// 不包含:proxy → provider/hook 子进程之间的 plumbing(DEV_ANYWHERE_HOOK_TOKEN /\n// HOOK_URL / HOOK_MARKER / HOOK_EVENT / SESSION_ID 等),这些是内部传参,不是用户旋钮,\n// 留在各自消费点。\n\nexport const VALID_LOG_LEVELS = [\n \"trace\",\n \"debug\",\n \"info\",\n \"warn\",\n \"error\",\n \"fatal\",\n \"silent\",\n] as const;\nexport type LogLevel = (typeof VALID_LOG_LEVELS)[number];\n\nexport interface ProxyRuntimeEnv {\n // RELAY_URL —— 覆盖 config.relays[name].url;用于一次性指向另一个 relay。\n relayUrl: string | undefined;\n // RELAY_PROXY_TOKEN —— 覆盖 config.relays[name].proxyToken。\n relayProxyToken: string | undefined;\n // DEV_ANYWHERE_HOOK_PORT —— 覆盖按 profile 推导的 hook server 端口。\n hookPort: number | undefined;\n // CLAUDE_BIN / CODEX_BIN —— 覆盖 config.agentCli 里的 CLI 可执行文件路径。\n claudeBin: string | undefined;\n codexBin: string | undefined;\n // LOG_LEVEL —— 用户最高优先级;config.logLevel 是次优先;都缺则各 logger 自己 default。\n logLevel: LogLevel | undefined;\n // VITEST —— 测试运行器存在则把 logger 静默,避免污染 vitest 输出。\n isVitest: boolean;\n // NODE_ENV 故意不在这里:env.ts 把它读成 top-level const 让 tsup 静态折叠 + dead-code\n // elimination dev 分支。走函数调用会破坏这个 build-time 优化。\n}\n\nfunction parseLogLevel(value: string | undefined): LogLevel | undefined {\n if (!value) return undefined;\n if ((VALID_LOG_LEVELS as readonly string[]).includes(value)) return value as LogLevel;\n throw new Error(\n `Invalid LOG_LEVEL=${JSON.stringify(value)}; expected one of ${VALID_LOG_LEVELS.join(\", \")}`,\n );\n}\n\nfunction parsePort(value: string | undefined, source: string): number | undefined {\n if (!value) return undefined;\n const port = Number(value);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`Invalid ${source}=${JSON.stringify(value)}: expected TCP port 1-65535`);\n }\n return port;\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n return value && value.length > 0 ? value : undefined;\n}\n\nexport function loadProxyRuntimeEnv(env: NodeJS.ProcessEnv = process.env): ProxyRuntimeEnv {\n return {\n relayUrl: nonEmpty(env.RELAY_URL),\n relayProxyToken: nonEmpty(env.RELAY_PROXY_TOKEN),\n hookPort: parsePort(env.DEV_ANYWHERE_HOOK_PORT, \"DEV_ANYWHERE_HOOK_PORT\"),\n claudeBin: nonEmpty(env.CLAUDE_BIN),\n codexBin: nonEmpty(env.CODEX_BIN),\n logLevel: parseLogLevel(env.LOG_LEVEL),\n isVitest: !!env.VITEST,\n };\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,YAAY,oBAAoB;;;ACQlC,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAqBA,SAAS,cAAc,OAAiD;AACtE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAK,iBAAuC,SAAS,KAAK,EAAG,QAAO;AACpE,QAAM,IAAI;AAAA,IACR,qBAAqB,KAAK,UAAU,KAAK,CAAC,qBAAqB,iBAAiB,KAAK,IAAI,CAAC;AAAA,EAC5F;AACF;AAEA,SAAS,UAAU,OAA2B,QAAoC;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,OAAO,KAAK;AACzB,MAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAAO;AACvD,UAAM,IAAI,MAAM,WAAW,MAAM,IAAI,KAAK,UAAU,KAAK,CAAC,6BAA6B;AAAA,EACzF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,OAA+C;AAC/D,SAAO,SAAS,MAAM,SAAS,IAAI,QAAQ;AAC7C;AAEO,SAAS,oBAAoBA,OAAyB,QAAQ,KAAsB;AACzF,SAAO;AAAA,IACL,UAAU,SAASA,KAAI,SAAS;AAAA,IAChC,iBAAiB,SAASA,KAAI,iBAAiB;AAAA,IAC/C,UAAU,UAAUA,KAAI,wBAAwB,wBAAwB;AAAA,IACxE,WAAW,SAASA,KAAI,UAAU;AAAA,IAClC,UAAU,SAASA,KAAI,SAAS;AAAA,IAChC,UAAU,cAAcA,KAAI,SAAS;AAAA,IACrC,UAAU,CAAC,CAACA,KAAI;AAAA,EAClB;AACF;;;AD/DA,IAAM,MAAM,oBAAoB;AAKhC,SAAS,qBAAyC;AAChD,MAAI,IAAI,SAAU,QAAO;AACzB,MAAI,CAAC,WAAW,WAAW,EAAG,QAAO;AACrC,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,aAAa,aAAa,OAAO,CAAC;AACzD,QAAI,OAAO,IAAI,aAAa,SAAU,QAAO;AAC7C,WAAQ,iBAAuC,SAAS,IAAI,QAAQ,IAChE,IAAI,WACJ;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,IAAM,gBAAgB,IAAI,YAAY,mBAAmB;AAElD,IAAM,gBAAgB,aAAa;AAAA,EACxC,MAAM;AAAA,EACN,OAAO,iBAAiB;AAAA,EACxB,QAAQ;AAAA,EACR,QAAQ,IAAI;AACd,CAAC;AAEM,IAAM,iBAAiB,aAAa;AAAA,EACzC,MAAM;AAAA,EACN,OAAO,iBAAiB;AAAA,EACxB,QAAQ;AAAA,EACR,QAAQ,IAAI;AACd,CAAC;","names":["env"]}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
LOG_DIR,
|
|
4
|
-
createLogger
|
|
5
|
-
} from "./chunk-RFBTVZ2X.js";
|
|
6
2
|
|
|
7
3
|
// src/common/osc-extractor.ts
|
|
8
4
|
var OSC_PATTERN = /\x1b\](\d+);([^\x07\x1b]*?)(?:\x07|\x1b\\)/g;
|
|
@@ -116,27 +112,12 @@ function defineFSM(transitions) {
|
|
|
116
112
|
};
|
|
117
113
|
}
|
|
118
114
|
|
|
119
|
-
// src/common/logger.ts
|
|
120
|
-
var serviceLogger = createLogger({
|
|
121
|
-
name: "service",
|
|
122
|
-
logDir: LOG_DIR,
|
|
123
|
-
silent: !!process.env.VITEST
|
|
124
|
-
});
|
|
125
|
-
var terminalLogger = createLogger({
|
|
126
|
-
name: "terminal",
|
|
127
|
-
level: "debug",
|
|
128
|
-
logDir: LOG_DIR,
|
|
129
|
-
silent: !!process.env.VITEST
|
|
130
|
-
});
|
|
131
|
-
|
|
132
115
|
export {
|
|
133
116
|
extractOscSequences,
|
|
134
117
|
extractOscSignals,
|
|
135
118
|
shouldReleaseApprovalWait,
|
|
136
119
|
stateAfterApprovalRelease,
|
|
137
120
|
createFSM,
|
|
138
|
-
defineFSM
|
|
139
|
-
serviceLogger,
|
|
140
|
-
terminalLogger
|
|
121
|
+
defineFSM
|
|
141
122
|
};
|
|
142
|
-
//# sourceMappingURL=chunk-
|
|
123
|
+
//# sourceMappingURL=chunk-ORZTFYXR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/common/osc-extractor.ts","../src/common/pty-approval-state.ts","../src/common/state-machine.ts"],"sourcesContent":["// OSC 0: 窗口标题 -- ESC ] 0 ; <title> BEL/ST\n// OSC 9: 通知 -- ESC ] 9 ; <text> BEL/ST\n// 每次调用创建新的 regex 实例避免 g flag 导致的 lastIndex 状态泄漏\n// eslint-disable-next-line no-control-regex\nconst OSC_PATTERN = /\\x1b\\](\\d+);([^\\x07\\x1b]*?)(?:\\x07|\\x1b\\\\)/g;\n\nexport type PtySemanticState = \"working\" | \"turn_complete\" | \"approval_wait\" | \"mid_pause\";\ntype PtySignalProvider = \"claude\" | \"codex\";\n\ninterface PtyStateEvent {\n state: PtySemanticState;\n title?: string;\n tool?: string;\n}\n\ninterface OscSequence {\n code: number;\n text: string;\n}\n\nexport function extractOscSequences(rawData: string): OscSequence[] {\n const regex = new RegExp(OSC_PATTERN.source, OSC_PATTERN.flags);\n const matches: OscSequence[] = [];\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(rawData)) !== null) {\n matches.push({ code: parseInt(match[1], 10), text: match[2] });\n }\n\n return matches;\n}\n\nfunction lastSequence(matches: OscSequence[], code: number): OscSequence | undefined {\n for (let i = matches.length - 1; i >= 0; i -= 1) {\n if (matches[i].code === code) return matches[i];\n }\n return undefined;\n}\n\nfunction isCodexActionRequiredTitle(title: string): boolean {\n return /\\bAction Required\\b/i.test(title);\n}\n\n// 从 PTY 原始数据中提取 OSC 语义信号。\n// OSC 9 优先级高于 OSC 0,无匹配时返回 null。\nexport function extractOscSignals(\n rawData: string,\n provider?: PtySignalProvider,\n): PtyStateEvent | null {\n const matches = extractOscSequences(rawData);\n\n if (matches.length === 0) return null;\n\n const osc0 = lastSequence(matches, 0);\n\n // OSC 9 优先级更高,包含具体的语义信号;同帧 OSC 0 仍保留 title 给 UI。\n const osc9 = lastSequence(matches, 9);\n if (osc9) {\n if (osc9.text.includes(\"waiting for your input\") || osc9.text.trim() === \"4;0;\") {\n return { state: \"turn_complete\", ...(osc0 ? { title: osc0.text } : {}) };\n }\n if (osc9.text.includes(\"needs your permission\")) {\n const toolMatch = osc9.text.match(/permission.*?:\\s*(\\S+)/);\n return {\n state: \"approval_wait\",\n ...(osc0 ? { title: osc0.text } : {}),\n ...(toolMatch?.[1] ? { tool: toolMatch[1] } : {}),\n };\n }\n }\n\n if (provider === \"codex\" && osc0 && isCodexActionRequiredTitle(osc0.text)) {\n return { state: \"approval_wait\", title: osc0.text };\n }\n\n // 仅有 OSC 0(标题/spinner 变化)时视为 MID_PAUSE\n if (osc0 && !osc9) {\n return { state: \"mid_pause\", title: osc0.text };\n }\n\n return null;\n}\n","import type { PtySemanticState } from \"./osc-extractor.js\";\n\nexport function shouldReleaseApprovalWait(options: {\n currentState: PtySemanticState;\n signalState?: PtySemanticState;\n}): boolean {\n if (options.currentState !== \"approval_wait\") return false;\n return options.signalState !== undefined && options.signalState !== \"approval_wait\";\n}\n\nexport function stateAfterApprovalRelease(signalState?: PtySemanticState): PtySemanticState {\n return signalState && signalState !== \"approval_wait\" ? signalState : \"working\";\n}\n","// 有限状态机 helper\n//\n// 提供显式转换表的小型 FSM,区分两类调用模式:\n// - transitionTo(throw):同步确定性流程里调用,非法转移代表 bug,立即暴露\n// - tryTransitionTo(bool):异步事件回调里调用,非法转移可能是吸收态残余事件,调用方按\n// isInAbsorbingState() 分级日志\n//\n// 吸收态采用传递闭包定义:终态(transitions=[])或所有出边都指向吸收态的状态都算吸收。\n// 这样 SessionState.ERROR (→[TERMINATED]) 和 RelayConnectionState.CLOSED ([]) 都被\n// 自动识别为吸收态,不用在 caller 里硬编码状态名。\n//\n// onTransition/onRejected 仅做日志/观测,不应 throw。\n\ninterface FSMDef<S extends string> {\n initial: S;\n // from-state → 允许转入的 to-state 列表;终态对应空数组\n transitions: Record<S, readonly S[]>;\n // 合法转换发生后触发,典型用法是结构化日志\n onTransition?: (from: S, to: S) => void;\n // tryTransitionTo 非法转移时触发;isAbsorbing 指示 from 是否吸收态(晚到残余 vs. 真非法)\n onRejected?: (from: S, to: S, isAbsorbing: boolean) => void;\n}\n\ninterface FSM<S extends string> {\n current(): S;\n is(state: S): boolean;\n isIn(states: readonly S[]): boolean;\n canTransitionTo(to: S): boolean;\n // 非法转换抛 Error;同步流程里当 assert 用\n transitionTo(to: S): void;\n // 非法转换返回 false,不抛;异步回调里用,配合 isInAbsorbingState 分级日志\n tryTransitionTo(to: S): boolean;\n // 当前是否在吸收态(传递闭包)\n isInAbsorbingState(): boolean;\n}\n\n// 计算吸收态集合:终态 + 所有出边都指向吸收态的状态,迭代至不动点\nfunction computeAbsorbingSet<S extends string>(transitions: Record<S, readonly S[]>): Set<S> {\n const absorbing = new Set<S>();\n const entries = Object.entries(transitions) as Array<[S, readonly S[]]>;\n for (const [s, outs] of entries) {\n if (outs.length === 0) absorbing.add(s);\n }\n let changed = true;\n while (changed) {\n changed = false;\n for (const [s, outs] of entries) {\n if (absorbing.has(s)) continue;\n if (outs.length > 0 && outs.every((t) => absorbing.has(t))) {\n absorbing.add(s);\n changed = true;\n }\n }\n }\n return absorbing;\n}\n\nexport function createFSM<S extends string>(def: FSMDef<S>): FSM<S> {\n let state = def.initial;\n const absorbing = computeAbsorbingSet(def.transitions);\n const tryTransitionTo = (to: S): boolean => {\n const allowed = def.transitions[state];\n if (!allowed?.includes(to)) {\n def.onRejected?.(state, to, absorbing.has(state));\n return false;\n }\n const from = state;\n state = to;\n def.onTransition?.(from, to);\n return true;\n };\n return {\n current: () => state,\n is: (s) => state === s,\n isIn: (ss) => ss.includes(state),\n canTransitionTo: (to) => def.transitions[state]?.includes(to) ?? false,\n transitionTo: (to) => {\n if (!tryTransitionTo(to)) {\n throw new Error(`Invalid FSM transition: ${state} -> ${to}`);\n }\n },\n tryTransitionTo,\n isInAbsorbingState: () => absorbing.has(state),\n };\n}\n\n// 无内部 state 的 FSM 视图,供 state 存在外部(如 SessionInfo、DB 行)的 per-instance 场景使用。\n// 调用方自行传入 from,canTransition 校验;吸收态判定通过 isAbsorbing(state) 提供。\ninterface StatelessFSM<S extends string> {\n canTransition(from: S, to: S): boolean;\n isAbsorbing(state: S): boolean;\n}\n\nexport function defineFSM<S extends string>(transitions: Record<S, readonly S[]>): StatelessFSM<S> {\n const absorbing = computeAbsorbingSet(transitions);\n return {\n canTransition: (from, to) => transitions[from]?.includes(to) ?? false,\n isAbsorbing: (state) => absorbing.has(state),\n };\n}\n"],"mappings":";;;AAIA,IAAM,cAAc;AAgBb,SAAS,oBAAoB,SAAgC;AAClE,QAAM,QAAQ,IAAI,OAAO,YAAY,QAAQ,YAAY,KAAK;AAC9D,QAAM,UAAyB,CAAC;AAEhC,MAAI;AACJ,UAAQ,QAAQ,MAAM,KAAK,OAAO,OAAO,MAAM;AAC7C,YAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC;AAAA,EAC/D;AAEA,SAAO;AACT;AAEA,SAAS,aAAa,SAAwB,MAAuC;AACnF,WAAS,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC/C,QAAI,QAAQ,CAAC,EAAE,SAAS,KAAM,QAAO,QAAQ,CAAC;AAAA,EAChD;AACA,SAAO;AACT;AAEA,SAAS,2BAA2B,OAAwB;AAC1D,SAAO,uBAAuB,KAAK,KAAK;AAC1C;AAIO,SAAS,kBACd,SACA,UACsB;AACtB,QAAM,UAAU,oBAAoB,OAAO;AAE3C,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,OAAO,aAAa,SAAS,CAAC;AAGpC,QAAM,OAAO,aAAa,SAAS,CAAC;AACpC,MAAI,MAAM;AACR,QAAI,KAAK,KAAK,SAAS,wBAAwB,KAAK,KAAK,KAAK,KAAK,MAAM,QAAQ;AAC/E,aAAO,EAAE,OAAO,iBAAiB,GAAI,OAAO,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC,EAAG;AAAA,IACzE;AACA,QAAI,KAAK,KAAK,SAAS,uBAAuB,GAAG;AAC/C,YAAM,YAAY,KAAK,KAAK,MAAM,wBAAwB;AAC1D,aAAO;AAAA,QACL,OAAO;AAAA,QACP,GAAI,OAAO,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC;AAAA,QACnC,GAAI,YAAY,CAAC,IAAI,EAAE,MAAM,UAAU,CAAC,EAAE,IAAI,CAAC;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AAEA,MAAI,aAAa,WAAW,QAAQ,2BAA2B,KAAK,IAAI,GAAG;AACzE,WAAO,EAAE,OAAO,iBAAiB,OAAO,KAAK,KAAK;AAAA,EACpD;AAGA,MAAI,QAAQ,CAAC,MAAM;AACjB,WAAO,EAAE,OAAO,aAAa,OAAO,KAAK,KAAK;AAAA,EAChD;AAEA,SAAO;AACT;;;AC/EO,SAAS,0BAA0B,SAG9B;AACV,MAAI,QAAQ,iBAAiB,gBAAiB,QAAO;AACrD,SAAO,QAAQ,gBAAgB,UAAa,QAAQ,gBAAgB;AACtE;AAEO,SAAS,0BAA0B,aAAkD;AAC1F,SAAO,eAAe,gBAAgB,kBAAkB,cAAc;AACxE;;;ACyBA,SAAS,oBAAsC,aAA8C;AAC3F,QAAM,YAAY,oBAAI,IAAO;AAC7B,QAAM,UAAU,OAAO,QAAQ,WAAW;AAC1C,aAAW,CAAC,GAAG,IAAI,KAAK,SAAS;AAC/B,QAAI,KAAK,WAAW,EAAG,WAAU,IAAI,CAAC;AAAA,EACxC;AACA,MAAI,UAAU;AACd,SAAO,SAAS;AACd,cAAU;AACV,eAAW,CAAC,GAAG,IAAI,KAAK,SAAS;AAC/B,UAAI,UAAU,IAAI,CAAC,EAAG;AACtB,UAAI,KAAK,SAAS,KAAK,KAAK,MAAM,CAAC,MAAM,UAAU,IAAI,CAAC,CAAC,GAAG;AAC1D,kBAAU,IAAI,CAAC;AACf,kBAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,UAA4B,KAAwB;AAClE,MAAI,QAAQ,IAAI;AAChB,QAAM,YAAY,oBAAoB,IAAI,WAAW;AACrD,QAAM,kBAAkB,CAAC,OAAmB;AAC1C,UAAM,UAAU,IAAI,YAAY,KAAK;AACrC,QAAI,CAAC,SAAS,SAAS,EAAE,GAAG;AAC1B,UAAI,aAAa,OAAO,IAAI,UAAU,IAAI,KAAK,CAAC;AAChD,aAAO;AAAA,IACT;AACA,UAAM,OAAO;AACb,YAAQ;AACR,QAAI,eAAe,MAAM,EAAE;AAC3B,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,SAAS,MAAM;AAAA,IACf,IAAI,CAAC,MAAM,UAAU;AAAA,IACrB,MAAM,CAAC,OAAO,GAAG,SAAS,KAAK;AAAA,IAC/B,iBAAiB,CAAC,OAAO,IAAI,YAAY,KAAK,GAAG,SAAS,EAAE,KAAK;AAAA,IACjE,cAAc,CAAC,OAAO;AACpB,UAAI,CAAC,gBAAgB,EAAE,GAAG;AACxB,cAAM,IAAI,MAAM,2BAA2B,KAAK,OAAO,EAAE,EAAE;AAAA,MAC7D;AAAA,IACF;AAAA,IACA;AAAA,IACA,oBAAoB,MAAM,UAAU,IAAI,KAAK;AAAA,EAC/C;AACF;AASO,SAAS,UAA4B,aAAuD;AACjG,QAAM,YAAY,oBAAoB,WAAW;AACjD,SAAO;AAAA,IACL,eAAe,CAAC,MAAM,OAAO,YAAY,IAAI,GAAG,SAAS,EAAE,KAAK;AAAA,IAChE,aAAa,CAAC,UAAU,UAAU,IAAI,KAAK;AAAA,EAC7C;AACF;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
sessionPaths
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2XO3KLWW.js";
|
|
5
5
|
|
|
6
6
|
// src/common/seq-counter.ts
|
|
7
7
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
@@ -152,4 +152,4 @@ export {
|
|
|
152
152
|
IGNORED_EVENT_TYPES,
|
|
153
153
|
SeqCounter
|
|
154
154
|
};
|
|
155
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-PDX6QFJ7.js.map
|