@fleetagent/pi-daemon 0.0.12

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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - Added the initial Pi remote commander daemon package.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @fleetagent/pi-daemon
2
+
3
+ Remote commander daemon for Pi. It listens for Pi's WebSocket remote tool protocol and executes file and shell operations in its working directory.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @fleetagent/pi-daemon
9
+ ```
10
+
11
+ ## Run
12
+
13
+ ```bash
14
+ PI_DAEMON_TOKEN=secret \
15
+ PI_DAEMON_CWD=/workspace \
16
+ PI_DAEMON_PORT=8787 \
17
+ pi-daemon
18
+ ```
19
+
20
+ Defaults:
21
+
22
+ - `PI_DAEMON_HOST` / `HOST`: `127.0.0.1`
23
+ - `PI_DAEMON_PORT` / `PORT`: `8787`
24
+ - `PI_DAEMON_CWD`: current directory
25
+ - `PI_DAEMON_TOKEN`: optional bearer token
26
+
27
+ ## Connect from Pi
28
+
29
+ Direct:
30
+
31
+ ```bash
32
+ pi --remote 'ws://127.0.0.1:8787?token=secret'
33
+ ```
34
+
35
+ Deferred:
36
+
37
+ ```bash
38
+ pi --remote-deferred --remote-cwd /workspace
39
+ ```
40
+
41
+ Then inside Pi:
42
+
43
+ ```text
44
+ /remote daemon ws://127.0.0.1:8787?token=secret
45
+ ```
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { type ChildProcessWithoutNullStreams, spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { constants } from \"node:fs\";\nimport { access, mkdir, readdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { createServer, type IncomingMessage } from \"node:http\";\nimport type { Socket } from \"node:net\";\nimport { resolve } from \"node:path\";\n\ninterface JsonRpcMessage {\n\tid?: unknown;\n\tmethod?: unknown;\n\tparams?: unknown;\n}\n\ninterface ClientConnection {\n\tsocket: Socket;\n\tbuffer: Buffer;\n\texecs: Map<string, ChildProcessWithoutNullStreams>;\n}\n\nconst port = Number(process.env.PORT ?? process.env.PI_DAEMON_PORT ?? \"8787\");\nconst host = process.env.HOST ?? process.env.PI_DAEMON_HOST ?? \"127.0.0.1\";\nconst cwd = resolve(process.env.PI_DAEMON_CWD ?? process.cwd());\nconst token = process.env.PI_DAEMON_TOKEN;\n\nfunction sendFrame(socket: Socket, payload: unknown): void {\n\tconst data = Buffer.from(JSON.stringify(payload));\n\tlet header: Buffer;\n\tif (data.length < 126) {\n\t\theader = Buffer.from([0x81, data.length]);\n\t} else if (data.length <= 0xffff) {\n\t\theader = Buffer.alloc(4);\n\t\theader[0] = 0x81;\n\t\theader[1] = 126;\n\t\theader.writeUInt16BE(data.length, 2);\n\t} else {\n\t\theader = Buffer.alloc(10);\n\t\theader[0] = 0x81;\n\t\theader[1] = 127;\n\t\theader.writeBigUInt64BE(BigInt(data.length), 2);\n\t}\n\tsocket.write(Buffer.concat([header, data]));\n}\n\nfunction sendResult(connection: ClientConnection, id: string, result: unknown): void {\n\tsendFrame(connection.socket, { id, result });\n}\n\nfunction sendError(connection: ClientConnection, id: string, error: unknown): void {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tsendFrame(connection.socket, { id, error: { message } });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction requireString(value: unknown, name: string): string {\n\tif (typeof value !== \"string\") throw new Error(`Missing string param: ${name}`);\n\treturn value;\n}\n\nfunction optionalString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction optionalNumber(value: unknown): number | undefined {\n\treturn typeof value === \"number\" ? value : undefined;\n}\n\nfunction accessModeToFsMode(mode: unknown): number {\n\tswitch (mode) {\n\t\tcase \"read\":\n\t\t\treturn constants.R_OK;\n\t\tcase \"write\":\n\t\t\treturn constants.W_OK;\n\t\tcase \"readwrite\":\n\t\t\treturn constants.R_OK | constants.W_OK;\n\t\tcase \"exists\":\n\t\tcase undefined:\n\t\t\treturn constants.F_OK;\n\t\tdefault:\n\t\t\tthrow new Error(`Invalid access mode: ${String(mode)}`);\n\t}\n}\n\nfunction buildFdArgs(pattern: string, searchPath: string, limit: number): string[] {\n\tconst args: string[] = [\"--glob\", \"--color=never\", \"--hidden\", \"--no-require-git\", \"--max-results\", String(limit)];\n\tlet effectivePattern = pattern;\n\tif (pattern.includes(\"/\")) {\n\t\targs.push(\"--full-path\");\n\t\tif (!pattern.startsWith(\"/\") && !pattern.startsWith(\"**/\") && pattern !== \"**\") {\n\t\t\teffectivePattern = `**/${pattern}`;\n\t\t}\n\t}\n\targs.push(\"--\", effectivePattern, searchPath);\n\treturn args;\n}\n\nfunction buildRgArgs(params: Record<string, unknown>): string[] {\n\tconst pattern = requireString(params.pattern, \"pattern\");\n\tconst path = requireString(params.path, \"path\");\n\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\tif (params.ignoreCase === true) args.push(\"--ignore-case\");\n\tif (params.literal === true) args.push(\"--fixed-strings\");\n\tconst glob = optionalString(params.glob);\n\tif (glob) args.push(\"--glob\", glob);\n\targs.push(\"--\", pattern, path);\n\treturn args;\n}\n\nasync function runBuffered(command: string, args: string[], runCwd: string): Promise<Buffer> {\n\treturn new Promise((resolvePromise, reject) => {\n\t\tconst child = spawn(command, args, { cwd: runCwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tconst stdout: Buffer[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tchild.stdout.on(\"data\", (data: Buffer) => stdout.push(data));\n\t\tchild.stderr.on(\"data\", (data: Buffer) => stderr.push(data));\n\t\tchild.on(\"error\", reject);\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolvePromise(Buffer.concat(stdout));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treject(new Error(Buffer.concat(stderr).toString(\"utf-8\").trim() || `${command} exited with code ${code}`));\n\t\t});\n\t});\n}\n\nfunction handleExec(connection: ClientConnection, id: string, params: Record<string, unknown>): void {\n\tconst command = requireString(params.command, \"command\");\n\tconst runCwd = optionalString(params.cwd) ?? cwd;\n\tconst timeout = optionalNumber(params.timeout);\n\tconst env = isRecord(params.env)\n\t\t? Object.fromEntries(\n\t\t\t\tObject.entries(params.env).filter((entry): entry is [string, string] => typeof entry[1] === \"string\"),\n\t\t\t)\n\t\t: undefined;\n\tconst child = spawn(\"bash\", [\"-lc\", command], {\n\t\tcwd: runCwd,\n\t\tdetached: process.platform !== \"win32\",\n\t\tenv: env ? { ...process.env, ...env } : process.env,\n\t});\n\tconnection.execs.set(id, child);\n\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\tif (timeout !== undefined && timeout > 0) {\n\t\ttimeoutHandle = setTimeout(() => {\n\t\t\tif (process.platform !== \"win32\" && child.pid) {\n\t\t\t\ttry {\n\t\t\t\t\tprocess.kill(-child.pid);\n\t\t\t\t} catch {\n\t\t\t\t\tchild.kill();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tchild.kill();\n\t\t\t}\n\t\t}, timeout * 1000);\n\t}\n\tchild.stdout.on(\"data\", (data: Buffer) => {\n\t\tsendFrame(connection.socket, { id, event: \"data\", stream: \"stdout\", dataBase64: data.toString(\"base64\") });\n\t});\n\tchild.stderr.on(\"data\", (data: Buffer) => {\n\t\tsendFrame(connection.socket, { id, event: \"data\", stream: \"stderr\", dataBase64: data.toString(\"base64\") });\n\t});\n\tchild.on(\"error\", (error) => {\n\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\tconnection.execs.delete(id);\n\t\tsendFrame(connection.socket, { id, event: \"error\", error: { message: error.message } });\n\t});\n\tchild.on(\"close\", (code) => {\n\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\tconnection.execs.delete(id);\n\t\tsendFrame(connection.socket, { id, event: \"exit\", exitCode: code });\n\t});\n}\n\nfunction cancelExec(connection: ClientConnection, id: string): void {\n\tconst child = connection.execs.get(id);\n\tif (!child) return;\n\tconnection.execs.delete(id);\n\tif (process.platform !== \"win32\" && child.pid) {\n\t\ttry {\n\t\t\tprocess.kill(-child.pid);\n\t\t} catch {\n\t\t\tchild.kill();\n\t\t}\n\t} else {\n\t\tchild.kill();\n\t}\n\tsendFrame(connection.socket, { id, event: \"exit\", exitCode: null, cancelled: true });\n}\n\nasync function handleRequest(connection: ClientConnection, message: JsonRpcMessage): Promise<void> {\n\tconst id = requireString(message.id, \"id\");\n\tconst method = requireString(message.method, \"method\");\n\tconst params = isRecord(message.params) ? message.params : {};\n\ttry {\n\t\tif (method === \"cancel\") {\n\t\t\tcancelExec(connection, id);\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"exec\") {\n\t\t\thandleExec(connection, id, params);\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"capabilities\") {\n\t\t\tsendResult(connection, id, {\n\t\t\t\tcwd,\n\t\t\t\tfeatures: { exec: true, files: true, glob: true, grep: true, instructions: false },\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"access\") {\n\t\t\tawait access(requireString(params.path, \"path\"), accessModeToFsMode(params.mode));\n\t\t\tsendResult(connection, id, {});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"readFile\") {\n\t\t\tconst content = await readFile(requireString(params.path, \"path\"));\n\t\t\tsendResult(connection, id, { contentBase64: content.toString(\"base64\") });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"writeFile\") {\n\t\t\tawait writeFile(\n\t\t\t\trequireString(params.path, \"path\"),\n\t\t\t\tBuffer.from(requireString(params.contentBase64, \"contentBase64\"), \"base64\"),\n\t\t\t);\n\t\t\tsendResult(connection, id, {});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"mkdir\") {\n\t\t\tawait mkdir(requireString(params.path, \"path\"), { recursive: params.recursive === true });\n\t\t\tsendResult(connection, id, {});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"stat\") {\n\t\t\tconst result = await stat(requireString(params.path, \"path\"));\n\t\t\tsendResult(connection, id, { isDirectory: result.isDirectory(), isFile: result.isFile() });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"readdir\") {\n\t\t\tsendResult(connection, id, { entries: await readdir(requireString(params.path, \"path\")) });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"glob\") {\n\t\t\tconst pattern = requireString(params.pattern, \"pattern\");\n\t\t\tconst runCwd = requireString(params.cwd, \"cwd\");\n\t\t\tconst limit = optionalNumber(params.limit) ?? 1000;\n\t\t\tconst output = await runBuffered(\"fd\", buildFdArgs(pattern, runCwd, limit), runCwd);\n\t\t\tsendResult(connection, id, { matches: output.toString(\"utf-8\").split(\"\\n\").filter(Boolean) });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"grep\") {\n\t\t\tconst pathParam = requireString(params.path, \"path\");\n\t\t\tconst isDirectory = (await stat(pathParam)).isDirectory();\n\t\t\tconst output = await runBuffered(\"rg\", buildRgArgs(params), cwd);\n\t\t\tconst matches = output\n\t\t\t\t.toString(\"utf-8\")\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.flatMap((line) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst data = JSON.parse(line) as unknown;\n\t\t\t\t\t\tif (!isRecord(data) || data.type !== \"match\" || !isRecord(data.data)) return [];\n\t\t\t\t\t\tconst filePath = isRecord(data.data.path) ? optionalString(data.data.path.text) : undefined;\n\t\t\t\t\t\tconst lineNumber = optionalNumber(data.data.line_number);\n\t\t\t\t\t\tconst lineText = isRecord(data.data.lines) ? optionalString(data.data.lines.text) : undefined;\n\t\t\t\t\t\treturn filePath && lineNumber !== undefined ? [{ filePath, lineNumber, lineText }] : [];\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn [];\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\tsendResult(connection, id, { isDirectory, matches });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"detectImageMimeType\") {\n\t\t\tconst output = await runBuffered(\"file\", [\"--mime-type\", \"-b\", requireString(params.path, \"path\")], cwd);\n\t\t\tconst mimeType = output.toString(\"utf-8\").trim();\n\t\t\tsendResult(connection, id, {\n\t\t\t\tmimeType: [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(`Unknown method: ${method}`);\n\t} catch (error) {\n\t\tsendError(connection, id, error);\n\t}\n}\n\nfunction parseFrames(connection: ClientConnection): void {\n\twhile (connection.buffer.length >= 2) {\n\t\tconst first = connection.buffer[0];\n\t\tconst second = connection.buffer[1];\n\t\tconst opcode = first & 0x0f;\n\t\tconst masked = (second & 0x80) !== 0;\n\t\tlet payloadLength = second & 0x7f;\n\t\tlet offset = 2;\n\t\tif (payloadLength === 126) {\n\t\t\tif (connection.buffer.length < offset + 2) return;\n\t\t\tpayloadLength = connection.buffer.readUInt16BE(offset);\n\t\t\toffset += 2;\n\t\t} else if (payloadLength === 127) {\n\t\t\tif (connection.buffer.length < offset + 8) return;\n\t\t\tconst largeLength = connection.buffer.readBigUInt64BE(offset);\n\t\t\tif (largeLength > BigInt(Number.MAX_SAFE_INTEGER)) {\n\t\t\t\tconnection.socket.destroy(new Error(\"WebSocket frame too large\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpayloadLength = Number(largeLength);\n\t\t\toffset += 8;\n\t\t}\n\t\tif (!masked) {\n\t\t\tconnection.socket.destroy(new Error(\"Client WebSocket frames must be masked\"));\n\t\t\treturn;\n\t\t}\n\t\tif (connection.buffer.length < offset + 4 + payloadLength) return;\n\t\tconst mask = connection.buffer.subarray(offset, offset + 4);\n\t\toffset += 4;\n\t\tconst payload = Buffer.from(connection.buffer.subarray(offset, offset + payloadLength));\n\t\tconnection.buffer = connection.buffer.subarray(offset + payloadLength);\n\t\tfor (let index = 0; index < payload.length; index++) {\n\t\t\tpayload[index] ^= mask[index % 4];\n\t\t}\n\t\tif (opcode === 0x8) {\n\t\t\tconnection.socket.end();\n\t\t\treturn;\n\t\t}\n\t\tif (opcode !== 0x1) continue;\n\t\tlet message: unknown;\n\t\ttry {\n\t\t\tmessage = JSON.parse(payload.toString(\"utf-8\"));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (isRecord(message) && message.type === \"ping\") {\n\t\t\tsendFrame(connection.socket, { type: \"pong\", timestamp: message.timestamp });\n\t\t\tcontinue;\n\t\t}\n\t\tvoid handleRequest(connection, message as JsonRpcMessage);\n\t}\n}\n\nfunction isAuthorized(request: IncomingMessage): boolean {\n\tif (!token) return true;\n\tif (request.headers.authorization === `Bearer ${token}`) return true;\n\ttry {\n\t\tconst url = new URL(request.url ?? \"/\", `http://${request.headers.host ?? \"localhost\"}`);\n\t\treturn url.searchParams.get(\"token\") === token;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nconst server = createServer((_request, response) => {\n\tresponse.writeHead(404);\n\tresponse.end(\"pi-daemon only serves WebSocket remote commander connections\\n\");\n});\n\nserver.on(\"upgrade\", (request, socket) => {\n\tconst netSocket = socket as Socket;\n\tif (!isAuthorized(request)) {\n\t\tnetSocket.write(\"HTTP/1.1 401 Unauthorized\\r\\n\\r\\n\");\n\t\tnetSocket.destroy();\n\t\treturn;\n\t}\n\tconst key = request.headers[\"sec-websocket-key\"];\n\tif (typeof key !== \"string\") {\n\t\tnetSocket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n\t\tnetSocket.destroy();\n\t\treturn;\n\t}\n\tconst accept = createHash(\"sha1\").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest(\"base64\");\n\tnetSocket.write(\n\t\t[\n\t\t\t\"HTTP/1.1 101 Switching Protocols\",\n\t\t\t\"Upgrade: websocket\",\n\t\t\t\"Connection: Upgrade\",\n\t\t\t`Sec-WebSocket-Accept: ${accept}`,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t].join(\"\\r\\n\"),\n\t);\n\tconst connection: ClientConnection = { socket: netSocket, buffer: Buffer.alloc(0), execs: new Map() };\n\tnetSocket.on(\"data\", (chunk: Buffer) => {\n\t\tconnection.buffer = Buffer.concat([connection.buffer, chunk]);\n\t\tparseFrames(connection);\n\t});\n\tnetSocket.on(\"close\", () => {\n\t\tfor (const child of connection.execs.values()) {\n\t\t\tchild.kill();\n\t\t}\n\t\tconnection.execs.clear();\n\t});\n});\n\nserver.listen(port, host, () => {\n\tconsole.log(`pi-daemon listening on ws://${host}:${port} cwd=${cwd}`);\n});\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { constants } from "node:fs";
5
+ import { access, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
+ import { createServer } from "node:http";
7
+ import { resolve } from "node:path";
8
+ const port = Number(process.env.PORT ?? process.env.PI_DAEMON_PORT ?? "8787");
9
+ const host = process.env.HOST ?? process.env.PI_DAEMON_HOST ?? "127.0.0.1";
10
+ const cwd = resolve(process.env.PI_DAEMON_CWD ?? process.cwd());
11
+ const token = process.env.PI_DAEMON_TOKEN;
12
+ function sendFrame(socket, payload) {
13
+ const data = Buffer.from(JSON.stringify(payload));
14
+ let header;
15
+ if (data.length < 126) {
16
+ header = Buffer.from([0x81, data.length]);
17
+ }
18
+ else if (data.length <= 0xffff) {
19
+ header = Buffer.alloc(4);
20
+ header[0] = 0x81;
21
+ header[1] = 126;
22
+ header.writeUInt16BE(data.length, 2);
23
+ }
24
+ else {
25
+ header = Buffer.alloc(10);
26
+ header[0] = 0x81;
27
+ header[1] = 127;
28
+ header.writeBigUInt64BE(BigInt(data.length), 2);
29
+ }
30
+ socket.write(Buffer.concat([header, data]));
31
+ }
32
+ function sendResult(connection, id, result) {
33
+ sendFrame(connection.socket, { id, result });
34
+ }
35
+ function sendError(connection, id, error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ sendFrame(connection.socket, { id, error: { message } });
38
+ }
39
+ function isRecord(value) {
40
+ return typeof value === "object" && value !== null;
41
+ }
42
+ function requireString(value, name) {
43
+ if (typeof value !== "string")
44
+ throw new Error(`Missing string param: ${name}`);
45
+ return value;
46
+ }
47
+ function optionalString(value) {
48
+ return typeof value === "string" ? value : undefined;
49
+ }
50
+ function optionalNumber(value) {
51
+ return typeof value === "number" ? value : undefined;
52
+ }
53
+ function accessModeToFsMode(mode) {
54
+ switch (mode) {
55
+ case "read":
56
+ return constants.R_OK;
57
+ case "write":
58
+ return constants.W_OK;
59
+ case "readwrite":
60
+ return constants.R_OK | constants.W_OK;
61
+ case "exists":
62
+ case undefined:
63
+ return constants.F_OK;
64
+ default:
65
+ throw new Error(`Invalid access mode: ${String(mode)}`);
66
+ }
67
+ }
68
+ function buildFdArgs(pattern, searchPath, limit) {
69
+ const args = ["--glob", "--color=never", "--hidden", "--no-require-git", "--max-results", String(limit)];
70
+ let effectivePattern = pattern;
71
+ if (pattern.includes("/")) {
72
+ args.push("--full-path");
73
+ if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") {
74
+ effectivePattern = `**/${pattern}`;
75
+ }
76
+ }
77
+ args.push("--", effectivePattern, searchPath);
78
+ return args;
79
+ }
80
+ function buildRgArgs(params) {
81
+ const pattern = requireString(params.pattern, "pattern");
82
+ const path = requireString(params.path, "path");
83
+ const args = ["--json", "--line-number", "--color=never", "--hidden"];
84
+ if (params.ignoreCase === true)
85
+ args.push("--ignore-case");
86
+ if (params.literal === true)
87
+ args.push("--fixed-strings");
88
+ const glob = optionalString(params.glob);
89
+ if (glob)
90
+ args.push("--glob", glob);
91
+ args.push("--", pattern, path);
92
+ return args;
93
+ }
94
+ async function runBuffered(command, args, runCwd) {
95
+ return new Promise((resolvePromise, reject) => {
96
+ const child = spawn(command, args, { cwd: runCwd, stdio: ["ignore", "pipe", "pipe"] });
97
+ const stdout = [];
98
+ const stderr = [];
99
+ child.stdout.on("data", (data) => stdout.push(data));
100
+ child.stderr.on("data", (data) => stderr.push(data));
101
+ child.on("error", reject);
102
+ child.on("close", (code) => {
103
+ if (code === 0) {
104
+ resolvePromise(Buffer.concat(stdout));
105
+ return;
106
+ }
107
+ reject(new Error(Buffer.concat(stderr).toString("utf-8").trim() || `${command} exited with code ${code}`));
108
+ });
109
+ });
110
+ }
111
+ function handleExec(connection, id, params) {
112
+ const command = requireString(params.command, "command");
113
+ const runCwd = optionalString(params.cwd) ?? cwd;
114
+ const timeout = optionalNumber(params.timeout);
115
+ const env = isRecord(params.env)
116
+ ? Object.fromEntries(Object.entries(params.env).filter((entry) => typeof entry[1] === "string"))
117
+ : undefined;
118
+ const child = spawn("bash", ["-lc", command], {
119
+ cwd: runCwd,
120
+ detached: process.platform !== "win32",
121
+ env: env ? { ...process.env, ...env } : process.env,
122
+ });
123
+ connection.execs.set(id, child);
124
+ let timeoutHandle;
125
+ if (timeout !== undefined && timeout > 0) {
126
+ timeoutHandle = setTimeout(() => {
127
+ if (process.platform !== "win32" && child.pid) {
128
+ try {
129
+ process.kill(-child.pid);
130
+ }
131
+ catch {
132
+ child.kill();
133
+ }
134
+ }
135
+ else {
136
+ child.kill();
137
+ }
138
+ }, timeout * 1000);
139
+ }
140
+ child.stdout.on("data", (data) => {
141
+ sendFrame(connection.socket, { id, event: "data", stream: "stdout", dataBase64: data.toString("base64") });
142
+ });
143
+ child.stderr.on("data", (data) => {
144
+ sendFrame(connection.socket, { id, event: "data", stream: "stderr", dataBase64: data.toString("base64") });
145
+ });
146
+ child.on("error", (error) => {
147
+ if (timeoutHandle)
148
+ clearTimeout(timeoutHandle);
149
+ connection.execs.delete(id);
150
+ sendFrame(connection.socket, { id, event: "error", error: { message: error.message } });
151
+ });
152
+ child.on("close", (code) => {
153
+ if (timeoutHandle)
154
+ clearTimeout(timeoutHandle);
155
+ connection.execs.delete(id);
156
+ sendFrame(connection.socket, { id, event: "exit", exitCode: code });
157
+ });
158
+ }
159
+ function cancelExec(connection, id) {
160
+ const child = connection.execs.get(id);
161
+ if (!child)
162
+ return;
163
+ connection.execs.delete(id);
164
+ if (process.platform !== "win32" && child.pid) {
165
+ try {
166
+ process.kill(-child.pid);
167
+ }
168
+ catch {
169
+ child.kill();
170
+ }
171
+ }
172
+ else {
173
+ child.kill();
174
+ }
175
+ sendFrame(connection.socket, { id, event: "exit", exitCode: null, cancelled: true });
176
+ }
177
+ async function handleRequest(connection, message) {
178
+ const id = requireString(message.id, "id");
179
+ const method = requireString(message.method, "method");
180
+ const params = isRecord(message.params) ? message.params : {};
181
+ try {
182
+ if (method === "cancel") {
183
+ cancelExec(connection, id);
184
+ return;
185
+ }
186
+ if (method === "exec") {
187
+ handleExec(connection, id, params);
188
+ return;
189
+ }
190
+ if (method === "capabilities") {
191
+ sendResult(connection, id, {
192
+ cwd,
193
+ features: { exec: true, files: true, glob: true, grep: true, instructions: false },
194
+ });
195
+ return;
196
+ }
197
+ if (method === "access") {
198
+ await access(requireString(params.path, "path"), accessModeToFsMode(params.mode));
199
+ sendResult(connection, id, {});
200
+ return;
201
+ }
202
+ if (method === "readFile") {
203
+ const content = await readFile(requireString(params.path, "path"));
204
+ sendResult(connection, id, { contentBase64: content.toString("base64") });
205
+ return;
206
+ }
207
+ if (method === "writeFile") {
208
+ await writeFile(requireString(params.path, "path"), Buffer.from(requireString(params.contentBase64, "contentBase64"), "base64"));
209
+ sendResult(connection, id, {});
210
+ return;
211
+ }
212
+ if (method === "mkdir") {
213
+ await mkdir(requireString(params.path, "path"), { recursive: params.recursive === true });
214
+ sendResult(connection, id, {});
215
+ return;
216
+ }
217
+ if (method === "stat") {
218
+ const result = await stat(requireString(params.path, "path"));
219
+ sendResult(connection, id, { isDirectory: result.isDirectory(), isFile: result.isFile() });
220
+ return;
221
+ }
222
+ if (method === "readdir") {
223
+ sendResult(connection, id, { entries: await readdir(requireString(params.path, "path")) });
224
+ return;
225
+ }
226
+ if (method === "glob") {
227
+ const pattern = requireString(params.pattern, "pattern");
228
+ const runCwd = requireString(params.cwd, "cwd");
229
+ const limit = optionalNumber(params.limit) ?? 1000;
230
+ const output = await runBuffered("fd", buildFdArgs(pattern, runCwd, limit), runCwd);
231
+ sendResult(connection, id, { matches: output.toString("utf-8").split("\n").filter(Boolean) });
232
+ return;
233
+ }
234
+ if (method === "grep") {
235
+ const pathParam = requireString(params.path, "path");
236
+ const isDirectory = (await stat(pathParam)).isDirectory();
237
+ const output = await runBuffered("rg", buildRgArgs(params), cwd);
238
+ const matches = output
239
+ .toString("utf-8")
240
+ .split("\n")
241
+ .filter(Boolean)
242
+ .flatMap((line) => {
243
+ try {
244
+ const data = JSON.parse(line);
245
+ if (!isRecord(data) || data.type !== "match" || !isRecord(data.data))
246
+ return [];
247
+ const filePath = isRecord(data.data.path) ? optionalString(data.data.path.text) : undefined;
248
+ const lineNumber = optionalNumber(data.data.line_number);
249
+ const lineText = isRecord(data.data.lines) ? optionalString(data.data.lines.text) : undefined;
250
+ return filePath && lineNumber !== undefined ? [{ filePath, lineNumber, lineText }] : [];
251
+ }
252
+ catch {
253
+ return [];
254
+ }
255
+ });
256
+ sendResult(connection, id, { isDirectory, matches });
257
+ return;
258
+ }
259
+ if (method === "detectImageMimeType") {
260
+ const output = await runBuffered("file", ["--mime-type", "-b", requireString(params.path, "path")], cwd);
261
+ const mimeType = output.toString("utf-8").trim();
262
+ sendResult(connection, id, {
263
+ mimeType: ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(mimeType) ? mimeType : null,
264
+ });
265
+ return;
266
+ }
267
+ throw new Error(`Unknown method: ${method}`);
268
+ }
269
+ catch (error) {
270
+ sendError(connection, id, error);
271
+ }
272
+ }
273
+ function parseFrames(connection) {
274
+ while (connection.buffer.length >= 2) {
275
+ const first = connection.buffer[0];
276
+ const second = connection.buffer[1];
277
+ const opcode = first & 0x0f;
278
+ const masked = (second & 0x80) !== 0;
279
+ let payloadLength = second & 0x7f;
280
+ let offset = 2;
281
+ if (payloadLength === 126) {
282
+ if (connection.buffer.length < offset + 2)
283
+ return;
284
+ payloadLength = connection.buffer.readUInt16BE(offset);
285
+ offset += 2;
286
+ }
287
+ else if (payloadLength === 127) {
288
+ if (connection.buffer.length < offset + 8)
289
+ return;
290
+ const largeLength = connection.buffer.readBigUInt64BE(offset);
291
+ if (largeLength > BigInt(Number.MAX_SAFE_INTEGER)) {
292
+ connection.socket.destroy(new Error("WebSocket frame too large"));
293
+ return;
294
+ }
295
+ payloadLength = Number(largeLength);
296
+ offset += 8;
297
+ }
298
+ if (!masked) {
299
+ connection.socket.destroy(new Error("Client WebSocket frames must be masked"));
300
+ return;
301
+ }
302
+ if (connection.buffer.length < offset + 4 + payloadLength)
303
+ return;
304
+ const mask = connection.buffer.subarray(offset, offset + 4);
305
+ offset += 4;
306
+ const payload = Buffer.from(connection.buffer.subarray(offset, offset + payloadLength));
307
+ connection.buffer = connection.buffer.subarray(offset + payloadLength);
308
+ for (let index = 0; index < payload.length; index++) {
309
+ payload[index] ^= mask[index % 4];
310
+ }
311
+ if (opcode === 0x8) {
312
+ connection.socket.end();
313
+ return;
314
+ }
315
+ if (opcode !== 0x1)
316
+ continue;
317
+ let message;
318
+ try {
319
+ message = JSON.parse(payload.toString("utf-8"));
320
+ }
321
+ catch {
322
+ continue;
323
+ }
324
+ if (isRecord(message) && message.type === "ping") {
325
+ sendFrame(connection.socket, { type: "pong", timestamp: message.timestamp });
326
+ continue;
327
+ }
328
+ void handleRequest(connection, message);
329
+ }
330
+ }
331
+ function isAuthorized(request) {
332
+ if (!token)
333
+ return true;
334
+ if (request.headers.authorization === `Bearer ${token}`)
335
+ return true;
336
+ try {
337
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
338
+ return url.searchParams.get("token") === token;
339
+ }
340
+ catch {
341
+ return false;
342
+ }
343
+ }
344
+ const server = createServer((_request, response) => {
345
+ response.writeHead(404);
346
+ response.end("pi-daemon only serves WebSocket remote commander connections\n");
347
+ });
348
+ server.on("upgrade", (request, socket) => {
349
+ const netSocket = socket;
350
+ if (!isAuthorized(request)) {
351
+ netSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
352
+ netSocket.destroy();
353
+ return;
354
+ }
355
+ const key = request.headers["sec-websocket-key"];
356
+ if (typeof key !== "string") {
357
+ netSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
358
+ netSocket.destroy();
359
+ return;
360
+ }
361
+ const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
362
+ netSocket.write([
363
+ "HTTP/1.1 101 Switching Protocols",
364
+ "Upgrade: websocket",
365
+ "Connection: Upgrade",
366
+ `Sec-WebSocket-Accept: ${accept}`,
367
+ "",
368
+ "",
369
+ ].join("\r\n"));
370
+ const connection = { socket: netSocket, buffer: Buffer.alloc(0), execs: new Map() };
371
+ netSocket.on("data", (chunk) => {
372
+ connection.buffer = Buffer.concat([connection.buffer, chunk]);
373
+ parseFrames(connection);
374
+ });
375
+ netSocket.on("close", () => {
376
+ for (const child of connection.execs.values()) {
377
+ child.kill();
378
+ }
379
+ connection.execs.clear();
380
+ });
381
+ });
382
+ server.listen(port, host, () => {
383
+ console.log(`pi-daemon listening on ws://${host}:${port} cwd=${cwd}`);
384
+ });
385
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAuC,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAwB,MAAM,WAAW,CAAC;AAE/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,MAAM,CAAC,CAAC;AAC9E,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,WAAW,CAAC;AAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAChE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;AAE1C,SAAS,SAAS,CAAC,MAAc,EAAE,OAAgB,EAAQ;IAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IAClD,IAAI,MAAc,CAAC;IACnB,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3C,CAAC;SAAM,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;QAClC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QACjB,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;QAChB,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACtC,CAAC;SAAM,CAAC;QACP,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1B,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QACjB,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;QAChB,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AAAA,CAC5C;AAED,SAAS,UAAU,CAAC,UAA4B,EAAE,EAAU,EAAE,MAAe,EAAQ;IACpF,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAAA,CAC7C;AAED,SAAS,SAAS,CAAC,UAA4B,EAAE,EAAU,EAAE,KAAc,EAAQ;IAClF,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;AAAA,CACzD;AAED,SAAS,QAAQ,CAAC,KAAc,EAAoC;IACnE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AAAA,CACnD;AAED,SAAS,aAAa,CAAC,KAAc,EAAE,IAAY,EAAU;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;IAChF,OAAO,KAAK,CAAC;AAAA,CACb;AAED,SAAS,cAAc,CAAC,KAAc,EAAsB;IAC3D,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACrD;AAED,SAAS,cAAc,CAAC,KAAc,EAAsB;IAC3D,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CACrD;AAED,SAAS,kBAAkB,CAAC,IAAa,EAAU;IAClD,QAAQ,IAAI,EAAE,CAAC;QACd,KAAK,MAAM;YACV,OAAO,SAAS,CAAC,IAAI,CAAC;QACvB,KAAK,OAAO;YACX,OAAO,SAAS,CAAC,IAAI,CAAC;QACvB,KAAK,WAAW;YACf,OAAO,SAAS,CAAC,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC;QACxC,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS;YACb,OAAO,SAAS,CAAC,IAAI,CAAC;QACvB;YACC,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1D,CAAC;AAAA,CACD;AAED,SAAS,WAAW,CAAC,OAAe,EAAE,UAAkB,EAAE,KAAa,EAAY;IAClF,MAAM,IAAI,GAAa,CAAC,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACnH,IAAI,gBAAgB,GAAG,OAAO,CAAC;IAC/B,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YAChF,gBAAgB,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,CAAC;IACF,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,UAAU,CAAC,CAAC;IAC9C,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,WAAW,CAAC,MAA+B,EAAY;IAC/D,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAChD,MAAM,IAAI,GAAa,CAAC,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;IAChF,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/B,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,KAAK,UAAU,WAAW,CAAC,OAAe,EAAE,IAAc,EAAE,MAAc,EAAmB;IAC5F,OAAO,IAAI,OAAO,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QACvF,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7D,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3B,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBAChB,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;gBACtC,OAAO;YACR,CAAC;YACD,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,qBAAqB,IAAI,EAAE,CAAC,CAAC,CAAC;QAAA,CAC3G,CAAC,CAAC;IAAA,CACH,CAAC,CAAC;AAAA,CACH;AAED,SAAS,UAAU,CAAC,UAA4B,EAAE,EAAU,EAAE,MAA+B,EAAQ;IACpG,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;IACjD,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC;QAC/B,CAAC,CAAC,MAAM,CAAC,WAAW,CAClB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAA6B,EAAE,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CACrG;QACF,CAAC,CAAC,SAAS,CAAC;IACb,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE;QAC7C,GAAG,EAAE,MAAM;QACX,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;QACtC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG;KACnD,CAAC,CAAC;IACH,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAChC,IAAI,aAAyC,CAAC;IAC9C,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAC1C,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YAChC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;gBAC/C,IAAI,CAAC;oBACJ,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC1B,CAAC;gBAAC,MAAM,CAAC;oBACR,KAAK,CAAC,IAAI,EAAE,CAAC;gBACd,CAAC;YACF,CAAC;iBAAM,CAAC;gBACP,KAAK,CAAC,IAAI,EAAE,CAAC;YACd,CAAC;QAAA,CACD,EAAE,OAAO,GAAG,IAAI,CAAC,CAAC;IACpB,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC;QACzC,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAAA,CAC3G,CAAC,CAAC;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC;QACzC,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAAA,CAC3G,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;QAC5B,IAAI,aAAa;YAAE,YAAY,CAAC,aAAa,CAAC,CAAC;QAC/C,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5B,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAAA,CACxF,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;QAC3B,IAAI,aAAa;YAAE,YAAY,CAAC,aAAa,CAAC,CAAC;QAC/C,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5B,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAAA,CACpE,CAAC,CAAC;AAAA,CACH;AAED,SAAS,UAAU,CAAC,UAA4B,EAAE,EAAU,EAAQ;IACnE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC5B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QAC/C,IAAI,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACR,KAAK,CAAC,IAAI,EAAE,CAAC;QACd,CAAC;IACF,CAAC;SAAM,CAAC;QACP,KAAK,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IACD,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAAA,CACrF;AAED,KAAK,UAAU,aAAa,CAAC,UAA4B,EAAE,OAAuB,EAAiB;IAClG,MAAM,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,IAAI,CAAC;QACJ,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACzB,UAAU,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAC3B,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;YACnC,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,cAAc,EAAE,CAAC;YAC/B,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE;gBAC1B,GAAG;gBACH,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE;aAClF,CAAC,CAAC;YACH,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACzB,MAAM,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAClF,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YAC/B,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YACnE,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YAC1E,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;YAC5B,MAAM,SAAS,CACd,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAClC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,EAAE,eAAe,CAAC,EAAE,QAAQ,CAAC,CAC3E,CAAC;YACF,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YAC/B,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACxB,MAAM,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC,CAAC;YAC1F,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YAC/B,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YAC9D,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAC3F,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;YAC3F,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YACzD,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAChD,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;YACnD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;YACpF,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC9F,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACrD,MAAM,WAAW,GAAG,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;YAC1D,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;YACjE,MAAM,OAAO,GAAG,MAAM;iBACpB,QAAQ,CAAC,OAAO,CAAC;iBACjB,KAAK,CAAC,IAAI,CAAC;iBACX,MAAM,CAAC,OAAO,CAAC;iBACf,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;gBAClB,IAAI,CAAC;oBACJ,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;oBACzC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;wBAAE,OAAO,EAAE,CAAC;oBAChF,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;oBAC5F,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;oBACzD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;oBAC9F,OAAO,QAAQ,IAAI,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzF,CAAC;gBAAC,MAAM,CAAC;oBACR,OAAO,EAAE,CAAC;gBACX,CAAC;YAAA,CACD,CAAC,CAAC;YACJ,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;YACrD,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,qBAAqB,EAAE,CAAC;YACtC,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,CAAC,aAAa,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACzG,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YACjD,UAAU,CAAC,UAAU,EAAE,EAAE,EAAE;gBAC1B,QAAQ,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;aACrG,CAAC,CAAC;YACH,OAAO;QACR,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,SAAS,CAAC,UAAU,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;IAClC,CAAC;AAAA,CACD;AAED,SAAS,WAAW,CAAC,UAA4B,EAAQ;IACxD,OAAO,UAAU,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC;QAC5B,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,aAAa,GAAG,MAAM,GAAG,IAAI,CAAC;QAClC,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,aAAa,KAAK,GAAG,EAAE,CAAC;YAC3B,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC;gBAAE,OAAO;YAClD,aAAa,GAAG,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YACvD,MAAM,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,aAAa,KAAK,GAAG,EAAE,CAAC;YAClC,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC;gBAAE,OAAO;YAClD,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;YAC9D,IAAI,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACnD,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC,CAAC;gBAClE,OAAO;YACR,CAAC;YACD,aAAa,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;YACpC,MAAM,IAAI,CAAC,CAAC;QACb,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC,CAAC;YAC/E,OAAO;QACR,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,MAAM,GAAG,CAAC,GAAG,aAAa;YAAE,OAAO;QAClE,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5D,MAAM,IAAI,CAAC,CAAC;QACZ,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC;QACxF,UAAU,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,aAAa,CAAC,CAAC;QACvE,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YACrD,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACpB,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;YACxB,OAAO;QACR,CAAC;QACD,IAAI,MAAM,KAAK,GAAG;YAAE,SAAS;QAC7B,IAAI,OAAgB,CAAC;QACrB,IAAI,CAAC;YACJ,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACR,SAAS;QACV,CAAC;QACD,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAClD,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;YAC7E,SAAS;QACV,CAAC;QACD,KAAK,aAAa,CAAC,UAAU,EAAE,OAAyB,CAAC,CAAC;IAC3D,CAAC;AAAA,CACD;AAED,SAAS,YAAY,CAAC,OAAwB,EAAW;IACxD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,OAAO,CAAC,OAAO,CAAC,aAAa,KAAK,UAAU,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACrE,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,UAAU,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,CAAC,CAAC;QACzF,OAAO,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AAAA,CACD;AAED,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC;IACnD,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC;AAAA,CAC/E,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,MAAgB,CAAC;IACnC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACrD,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO;IACR,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACjD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC7B,SAAS,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACpD,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO;IACR,CAAC;IACD,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,sCAAsC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxG,SAAS,CAAC,KAAK,CACd;QACC,kCAAkC;QAClC,oBAAoB;QACpB,qBAAqB;QACrB,yBAAyB,MAAM,EAAE;QACjC,EAAE;QACF,EAAE;KACF,CAAC,IAAI,CAAC,MAAM,CAAC,CACd,CAAC;IACF,MAAM,UAAU,GAAqB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;IACtG,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;QACvC,UAAU,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QAC9D,WAAW,CAAC,UAAU,CAAC,CAAC;IAAA,CACxB,CAAC,CAAC;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YAC/C,KAAK,CAAC,IAAI,EAAE,CAAC;QACd,CAAC;QACD,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IAAA,CACzB,CAAC,CAAC;AAAA,CACH,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;IAC/B,OAAO,CAAC,GAAG,CAAC,+BAA+B,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,CAAC,CAAC;AAAA,CACtE,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport { type ChildProcessWithoutNullStreams, spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { constants } from \"node:fs\";\nimport { access, mkdir, readdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { createServer, type IncomingMessage } from \"node:http\";\nimport type { Socket } from \"node:net\";\nimport { resolve } from \"node:path\";\n\ninterface JsonRpcMessage {\n\tid?: unknown;\n\tmethod?: unknown;\n\tparams?: unknown;\n}\n\ninterface ClientConnection {\n\tsocket: Socket;\n\tbuffer: Buffer;\n\texecs: Map<string, ChildProcessWithoutNullStreams>;\n}\n\nconst port = Number(process.env.PORT ?? process.env.PI_DAEMON_PORT ?? \"8787\");\nconst host = process.env.HOST ?? process.env.PI_DAEMON_HOST ?? \"127.0.0.1\";\nconst cwd = resolve(process.env.PI_DAEMON_CWD ?? process.cwd());\nconst token = process.env.PI_DAEMON_TOKEN;\n\nfunction sendFrame(socket: Socket, payload: unknown): void {\n\tconst data = Buffer.from(JSON.stringify(payload));\n\tlet header: Buffer;\n\tif (data.length < 126) {\n\t\theader = Buffer.from([0x81, data.length]);\n\t} else if (data.length <= 0xffff) {\n\t\theader = Buffer.alloc(4);\n\t\theader[0] = 0x81;\n\t\theader[1] = 126;\n\t\theader.writeUInt16BE(data.length, 2);\n\t} else {\n\t\theader = Buffer.alloc(10);\n\t\theader[0] = 0x81;\n\t\theader[1] = 127;\n\t\theader.writeBigUInt64BE(BigInt(data.length), 2);\n\t}\n\tsocket.write(Buffer.concat([header, data]));\n}\n\nfunction sendResult(connection: ClientConnection, id: string, result: unknown): void {\n\tsendFrame(connection.socket, { id, result });\n}\n\nfunction sendError(connection: ClientConnection, id: string, error: unknown): void {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tsendFrame(connection.socket, { id, error: { message } });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction requireString(value: unknown, name: string): string {\n\tif (typeof value !== \"string\") throw new Error(`Missing string param: ${name}`);\n\treturn value;\n}\n\nfunction optionalString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction optionalNumber(value: unknown): number | undefined {\n\treturn typeof value === \"number\" ? value : undefined;\n}\n\nfunction accessModeToFsMode(mode: unknown): number {\n\tswitch (mode) {\n\t\tcase \"read\":\n\t\t\treturn constants.R_OK;\n\t\tcase \"write\":\n\t\t\treturn constants.W_OK;\n\t\tcase \"readwrite\":\n\t\t\treturn constants.R_OK | constants.W_OK;\n\t\tcase \"exists\":\n\t\tcase undefined:\n\t\t\treturn constants.F_OK;\n\t\tdefault:\n\t\t\tthrow new Error(`Invalid access mode: ${String(mode)}`);\n\t}\n}\n\nfunction buildFdArgs(pattern: string, searchPath: string, limit: number): string[] {\n\tconst args: string[] = [\"--glob\", \"--color=never\", \"--hidden\", \"--no-require-git\", \"--max-results\", String(limit)];\n\tlet effectivePattern = pattern;\n\tif (pattern.includes(\"/\")) {\n\t\targs.push(\"--full-path\");\n\t\tif (!pattern.startsWith(\"/\") && !pattern.startsWith(\"**/\") && pattern !== \"**\") {\n\t\t\teffectivePattern = `**/${pattern}`;\n\t\t}\n\t}\n\targs.push(\"--\", effectivePattern, searchPath);\n\treturn args;\n}\n\nfunction buildRgArgs(params: Record<string, unknown>): string[] {\n\tconst pattern = requireString(params.pattern, \"pattern\");\n\tconst path = requireString(params.path, \"path\");\n\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\tif (params.ignoreCase === true) args.push(\"--ignore-case\");\n\tif (params.literal === true) args.push(\"--fixed-strings\");\n\tconst glob = optionalString(params.glob);\n\tif (glob) args.push(\"--glob\", glob);\n\targs.push(\"--\", pattern, path);\n\treturn args;\n}\n\nasync function runBuffered(command: string, args: string[], runCwd: string): Promise<Buffer> {\n\treturn new Promise((resolvePromise, reject) => {\n\t\tconst child = spawn(command, args, { cwd: runCwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tconst stdout: Buffer[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tchild.stdout.on(\"data\", (data: Buffer) => stdout.push(data));\n\t\tchild.stderr.on(\"data\", (data: Buffer) => stderr.push(data));\n\t\tchild.on(\"error\", reject);\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) {\n\t\t\t\tresolvePromise(Buffer.concat(stdout));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treject(new Error(Buffer.concat(stderr).toString(\"utf-8\").trim() || `${command} exited with code ${code}`));\n\t\t});\n\t});\n}\n\nfunction handleExec(connection: ClientConnection, id: string, params: Record<string, unknown>): void {\n\tconst command = requireString(params.command, \"command\");\n\tconst runCwd = optionalString(params.cwd) ?? cwd;\n\tconst timeout = optionalNumber(params.timeout);\n\tconst env = isRecord(params.env)\n\t\t? Object.fromEntries(\n\t\t\t\tObject.entries(params.env).filter((entry): entry is [string, string] => typeof entry[1] === \"string\"),\n\t\t\t)\n\t\t: undefined;\n\tconst child = spawn(\"bash\", [\"-lc\", command], {\n\t\tcwd: runCwd,\n\t\tdetached: process.platform !== \"win32\",\n\t\tenv: env ? { ...process.env, ...env } : process.env,\n\t});\n\tconnection.execs.set(id, child);\n\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\tif (timeout !== undefined && timeout > 0) {\n\t\ttimeoutHandle = setTimeout(() => {\n\t\t\tif (process.platform !== \"win32\" && child.pid) {\n\t\t\t\ttry {\n\t\t\t\t\tprocess.kill(-child.pid);\n\t\t\t\t} catch {\n\t\t\t\t\tchild.kill();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tchild.kill();\n\t\t\t}\n\t\t}, timeout * 1000);\n\t}\n\tchild.stdout.on(\"data\", (data: Buffer) => {\n\t\tsendFrame(connection.socket, { id, event: \"data\", stream: \"stdout\", dataBase64: data.toString(\"base64\") });\n\t});\n\tchild.stderr.on(\"data\", (data: Buffer) => {\n\t\tsendFrame(connection.socket, { id, event: \"data\", stream: \"stderr\", dataBase64: data.toString(\"base64\") });\n\t});\n\tchild.on(\"error\", (error) => {\n\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\tconnection.execs.delete(id);\n\t\tsendFrame(connection.socket, { id, event: \"error\", error: { message: error.message } });\n\t});\n\tchild.on(\"close\", (code) => {\n\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\tconnection.execs.delete(id);\n\t\tsendFrame(connection.socket, { id, event: \"exit\", exitCode: code });\n\t});\n}\n\nfunction cancelExec(connection: ClientConnection, id: string): void {\n\tconst child = connection.execs.get(id);\n\tif (!child) return;\n\tconnection.execs.delete(id);\n\tif (process.platform !== \"win32\" && child.pid) {\n\t\ttry {\n\t\t\tprocess.kill(-child.pid);\n\t\t} catch {\n\t\t\tchild.kill();\n\t\t}\n\t} else {\n\t\tchild.kill();\n\t}\n\tsendFrame(connection.socket, { id, event: \"exit\", exitCode: null, cancelled: true });\n}\n\nasync function handleRequest(connection: ClientConnection, message: JsonRpcMessage): Promise<void> {\n\tconst id = requireString(message.id, \"id\");\n\tconst method = requireString(message.method, \"method\");\n\tconst params = isRecord(message.params) ? message.params : {};\n\ttry {\n\t\tif (method === \"cancel\") {\n\t\t\tcancelExec(connection, id);\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"exec\") {\n\t\t\thandleExec(connection, id, params);\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"capabilities\") {\n\t\t\tsendResult(connection, id, {\n\t\t\t\tcwd,\n\t\t\t\tfeatures: { exec: true, files: true, glob: true, grep: true, instructions: false },\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"access\") {\n\t\t\tawait access(requireString(params.path, \"path\"), accessModeToFsMode(params.mode));\n\t\t\tsendResult(connection, id, {});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"readFile\") {\n\t\t\tconst content = await readFile(requireString(params.path, \"path\"));\n\t\t\tsendResult(connection, id, { contentBase64: content.toString(\"base64\") });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"writeFile\") {\n\t\t\tawait writeFile(\n\t\t\t\trequireString(params.path, \"path\"),\n\t\t\t\tBuffer.from(requireString(params.contentBase64, \"contentBase64\"), \"base64\"),\n\t\t\t);\n\t\t\tsendResult(connection, id, {});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"mkdir\") {\n\t\t\tawait mkdir(requireString(params.path, \"path\"), { recursive: params.recursive === true });\n\t\t\tsendResult(connection, id, {});\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"stat\") {\n\t\t\tconst result = await stat(requireString(params.path, \"path\"));\n\t\t\tsendResult(connection, id, { isDirectory: result.isDirectory(), isFile: result.isFile() });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"readdir\") {\n\t\t\tsendResult(connection, id, { entries: await readdir(requireString(params.path, \"path\")) });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"glob\") {\n\t\t\tconst pattern = requireString(params.pattern, \"pattern\");\n\t\t\tconst runCwd = requireString(params.cwd, \"cwd\");\n\t\t\tconst limit = optionalNumber(params.limit) ?? 1000;\n\t\t\tconst output = await runBuffered(\"fd\", buildFdArgs(pattern, runCwd, limit), runCwd);\n\t\t\tsendResult(connection, id, { matches: output.toString(\"utf-8\").split(\"\\n\").filter(Boolean) });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"grep\") {\n\t\t\tconst pathParam = requireString(params.path, \"path\");\n\t\t\tconst isDirectory = (await stat(pathParam)).isDirectory();\n\t\t\tconst output = await runBuffered(\"rg\", buildRgArgs(params), cwd);\n\t\t\tconst matches = output\n\t\t\t\t.toString(\"utf-8\")\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.flatMap((line) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst data = JSON.parse(line) as unknown;\n\t\t\t\t\t\tif (!isRecord(data) || data.type !== \"match\" || !isRecord(data.data)) return [];\n\t\t\t\t\t\tconst filePath = isRecord(data.data.path) ? optionalString(data.data.path.text) : undefined;\n\t\t\t\t\t\tconst lineNumber = optionalNumber(data.data.line_number);\n\t\t\t\t\t\tconst lineText = isRecord(data.data.lines) ? optionalString(data.data.lines.text) : undefined;\n\t\t\t\t\t\treturn filePath && lineNumber !== undefined ? [{ filePath, lineNumber, lineText }] : [];\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn [];\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\tsendResult(connection, id, { isDirectory, matches });\n\t\t\treturn;\n\t\t}\n\t\tif (method === \"detectImageMimeType\") {\n\t\t\tconst output = await runBuffered(\"file\", [\"--mime-type\", \"-b\", requireString(params.path, \"path\")], cwd);\n\t\t\tconst mimeType = output.toString(\"utf-8\").trim();\n\t\t\tsendResult(connection, id, {\n\t\t\t\tmimeType: [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(`Unknown method: ${method}`);\n\t} catch (error) {\n\t\tsendError(connection, id, error);\n\t}\n}\n\nfunction parseFrames(connection: ClientConnection): void {\n\twhile (connection.buffer.length >= 2) {\n\t\tconst first = connection.buffer[0];\n\t\tconst second = connection.buffer[1];\n\t\tconst opcode = first & 0x0f;\n\t\tconst masked = (second & 0x80) !== 0;\n\t\tlet payloadLength = second & 0x7f;\n\t\tlet offset = 2;\n\t\tif (payloadLength === 126) {\n\t\t\tif (connection.buffer.length < offset + 2) return;\n\t\t\tpayloadLength = connection.buffer.readUInt16BE(offset);\n\t\t\toffset += 2;\n\t\t} else if (payloadLength === 127) {\n\t\t\tif (connection.buffer.length < offset + 8) return;\n\t\t\tconst largeLength = connection.buffer.readBigUInt64BE(offset);\n\t\t\tif (largeLength > BigInt(Number.MAX_SAFE_INTEGER)) {\n\t\t\t\tconnection.socket.destroy(new Error(\"WebSocket frame too large\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpayloadLength = Number(largeLength);\n\t\t\toffset += 8;\n\t\t}\n\t\tif (!masked) {\n\t\t\tconnection.socket.destroy(new Error(\"Client WebSocket frames must be masked\"));\n\t\t\treturn;\n\t\t}\n\t\tif (connection.buffer.length < offset + 4 + payloadLength) return;\n\t\tconst mask = connection.buffer.subarray(offset, offset + 4);\n\t\toffset += 4;\n\t\tconst payload = Buffer.from(connection.buffer.subarray(offset, offset + payloadLength));\n\t\tconnection.buffer = connection.buffer.subarray(offset + payloadLength);\n\t\tfor (let index = 0; index < payload.length; index++) {\n\t\t\tpayload[index] ^= mask[index % 4];\n\t\t}\n\t\tif (opcode === 0x8) {\n\t\t\tconnection.socket.end();\n\t\t\treturn;\n\t\t}\n\t\tif (opcode !== 0x1) continue;\n\t\tlet message: unknown;\n\t\ttry {\n\t\t\tmessage = JSON.parse(payload.toString(\"utf-8\"));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (isRecord(message) && message.type === \"ping\") {\n\t\t\tsendFrame(connection.socket, { type: \"pong\", timestamp: message.timestamp });\n\t\t\tcontinue;\n\t\t}\n\t\tvoid handleRequest(connection, message as JsonRpcMessage);\n\t}\n}\n\nfunction isAuthorized(request: IncomingMessage): boolean {\n\tif (!token) return true;\n\tif (request.headers.authorization === `Bearer ${token}`) return true;\n\ttry {\n\t\tconst url = new URL(request.url ?? \"/\", `http://${request.headers.host ?? \"localhost\"}`);\n\t\treturn url.searchParams.get(\"token\") === token;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nconst server = createServer((_request, response) => {\n\tresponse.writeHead(404);\n\tresponse.end(\"pi-daemon only serves WebSocket remote commander connections\\n\");\n});\n\nserver.on(\"upgrade\", (request, socket) => {\n\tconst netSocket = socket as Socket;\n\tif (!isAuthorized(request)) {\n\t\tnetSocket.write(\"HTTP/1.1 401 Unauthorized\\r\\n\\r\\n\");\n\t\tnetSocket.destroy();\n\t\treturn;\n\t}\n\tconst key = request.headers[\"sec-websocket-key\"];\n\tif (typeof key !== \"string\") {\n\t\tnetSocket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n\t\tnetSocket.destroy();\n\t\treturn;\n\t}\n\tconst accept = createHash(\"sha1\").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest(\"base64\");\n\tnetSocket.write(\n\t\t[\n\t\t\t\"HTTP/1.1 101 Switching Protocols\",\n\t\t\t\"Upgrade: websocket\",\n\t\t\t\"Connection: Upgrade\",\n\t\t\t`Sec-WebSocket-Accept: ${accept}`,\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t].join(\"\\r\\n\"),\n\t);\n\tconst connection: ClientConnection = { socket: netSocket, buffer: Buffer.alloc(0), execs: new Map() };\n\tnetSocket.on(\"data\", (chunk: Buffer) => {\n\t\tconnection.buffer = Buffer.concat([connection.buffer, chunk]);\n\t\tparseFrames(connection);\n\t});\n\tnetSocket.on(\"close\", () => {\n\t\tfor (const child of connection.execs.values()) {\n\t\t\tchild.kill();\n\t\t}\n\t\tconnection.execs.clear();\n\t});\n});\n\nserver.listen(port, host, () => {\n\tconsole.log(`pi-daemon listening on ws://${host}:${port} cwd=${cwd}`);\n});\n"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@fleetagent/pi-daemon",
3
+ "version": "0.0.12",
4
+ "description": "Remote commander daemon for Pi",
5
+ "type": "module",
6
+ "bin": {
7
+ "pi-daemon": "dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "scripts": {
23
+ "clean": "shx rm -rf dist",
24
+ "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/index.js",
25
+ "test": "vitest --run --passWithNoTests",
26
+ "prepublishOnly": "npm run clean && npm run build"
27
+ },
28
+ "keywords": [
29
+ "pi",
30
+ "daemon",
31
+ "remote",
32
+ "coding-agent"
33
+ ],
34
+ "author": "Mario Zechner",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/fleetagent/pi",
39
+ "directory": "packages/pi-daemon"
40
+ },
41
+ "engines": {
42
+ "node": ">=22.19.0"
43
+ }
44
+ }