chore(remote-control): add Biome and fix all lint warnings
This commit is contained in:
parent
06fa1147f3
commit
82c463ec27
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,9 @@ export function generateSessionId(): string {
|
|||
return randomBytes(24).toString("base64url");
|
||||
}
|
||||
|
||||
export function parseCookies(header: string | undefined): Record<string, string> {
|
||||
export function parseCookies(
|
||||
header: string | undefined,
|
||||
): Record<string, string> {
|
||||
const cookies: Record<string, string> = {};
|
||||
if (!header) return cookies;
|
||||
for (const pair of header.split(";")) {
|
||||
|
|
@ -33,7 +35,11 @@ export function parseCookies(header: string | undefined): Record<string, string>
|
|||
const name = pair.slice(0, idx).trim();
|
||||
const raw = pair.slice(idx + 1).trim();
|
||||
let value = raw;
|
||||
try { value = decodeURIComponent(raw); } catch { /* keep raw */ }
|
||||
try {
|
||||
value = decodeURIComponent(raw);
|
||||
} catch {
|
||||
/* keep raw */
|
||||
}
|
||||
cookies[name] = value;
|
||||
}
|
||||
return cookies;
|
||||
|
|
|
|||
|
|
@ -54,10 +54,16 @@ export async function readRemoteControlConfig(): Promise<RemoteControlConfig> {
|
|||
}
|
||||
}
|
||||
|
||||
async function writeRemoteControlConfig(config: RemoteControlConfig): Promise<void> {
|
||||
async function writeRemoteControlConfig(
|
||||
config: RemoteControlConfig,
|
||||
): Promise<void> {
|
||||
const configPath = getRemoteControlConfigPath();
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(config, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizePublicBaseUrl(value: string): string {
|
||||
|
|
@ -70,7 +76,11 @@ export function normalizePublicBaseUrl(value: string): string {
|
|||
return parsed.toString().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function buildRemoteControlUrl(publicBaseUrl: string, port: number, token: string): string {
|
||||
export function buildRemoteControlUrl(
|
||||
publicBaseUrl: string,
|
||||
port: number,
|
||||
token: string,
|
||||
): string {
|
||||
const parsed = new URL(normalizePublicBaseUrl(publicBaseUrl));
|
||||
if (parsed.protocol === "http:") {
|
||||
parsed.port = String(port);
|
||||
|
|
@ -79,7 +89,9 @@ export function buildRemoteControlUrl(publicBaseUrl: string, port: number, token
|
|||
return parsed.toString();
|
||||
}
|
||||
|
||||
export async function configureRemoteControlUI(ctx: ExtensionContext): Promise<void> {
|
||||
export async function configureRemoteControlUI(
|
||||
ctx: ExtensionContext,
|
||||
): Promise<void> {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const current = (await readRemoteControlConfig()).publicBaseUrl ?? "";
|
||||
|
|
@ -93,14 +105,23 @@ export async function configureRemoteControlUI(ctx: ExtensionContext): Promise<v
|
|||
try {
|
||||
value = normalizePublicBaseUrl(raw);
|
||||
} catch {
|
||||
ctx.ui.notify("Public base URL must be a valid http:// or https:// URL", "warning");
|
||||
ctx.ui.notify(
|
||||
"Public base URL must be a valid http:// or https:// URL",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!["http:", "https:"].includes(new URL(value).protocol)) {
|
||||
ctx.ui.notify("Public base URL must start with http:// or https://", "warning");
|
||||
ctx.ui.notify(
|
||||
"Public base URL must start with http:// or https://",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await writeRemoteControlConfig({ publicBaseUrl: value });
|
||||
ctx.ui.notify(`Saved remote-control URL to ${getRemoteControlConfigPath()}`, "info");
|
||||
ctx.ui.notify(
|
||||
`Saved remote-control URL to ${getRemoteControlConfigPath()}`,
|
||||
"info",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
export function buildHTML(nonce: string): string {
|
||||
return /* html */ `<!DOCTYPE html>
|
||||
return /* html */ `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
|
|
|||
|
|
@ -11,21 +11,26 @@
|
|||
*/
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder, keyHint } from "@mariozechner/pi-coding-agent";
|
||||
import { Container, Text } from "@mariozechner/pi-tui";
|
||||
import {
|
||||
readRemoteControlConfig,
|
||||
buildRemoteControlUrl,
|
||||
configureRemoteControlUI,
|
||||
readRemoteControlConfig,
|
||||
} from "./config.js";
|
||||
import { serializeMessage } from "./messages.js";
|
||||
import { type RawMessage, serializeMessage } from "./messages.js";
|
||||
import { type RemoteServer, startServer } from "./server.js";
|
||||
|
||||
// ── Extension entry point ────────────────────────────────────────────────────
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
const QRCode = _require("qrcode") as { toString: (text: string, opts: any) => Promise<string> };
|
||||
const QRCode = _require("qrcode") as {
|
||||
toString: (text: string, opts: Record<string, unknown>) => Promise<string>;
|
||||
};
|
||||
|
||||
export default function remoteControl(pi: ExtensionAPI) {
|
||||
let server: RemoteServer | undefined;
|
||||
|
|
@ -43,7 +48,8 @@ export default function remoteControl(pi: ExtensionAPI) {
|
|||
// ── CLI flag ──────────────────────────────────────────────────────────────
|
||||
|
||||
pi.registerFlag("remote-control", {
|
||||
description: "Start the remote-control server automatically on session start",
|
||||
description:
|
||||
"Start the remote-control server automatically on session start",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
});
|
||||
|
|
@ -120,7 +126,10 @@ export default function remoteControl(pi: ExtensionAPI) {
|
|||
});
|
||||
|
||||
pi.on("message_update", async (event) => {
|
||||
const m = serializeMessage("pending", (event as any).message);
|
||||
const m = serializeMessage(
|
||||
"pending",
|
||||
(event as { message: RawMessage }).message,
|
||||
);
|
||||
if (m) server?.broadcast({ type: "message_update", message: m });
|
||||
});
|
||||
|
||||
|
|
@ -129,7 +138,7 @@ export default function remoteControl(pi: ExtensionAPI) {
|
|||
const branch = ctx.sessionManager.getBranch();
|
||||
const last = branch[branch.length - 1];
|
||||
const id = last?.id ?? `msg_${Date.now()}`;
|
||||
const m = serializeMessage(id, (event as any).message);
|
||||
const m = serializeMessage(id, (event as { message: RawMessage }).message);
|
||||
if (m) server?.broadcast({ type: "message_end", message: m });
|
||||
});
|
||||
|
||||
|
|
@ -143,11 +152,14 @@ export default function remoteControl(pi: ExtensionAPI) {
|
|||
});
|
||||
|
||||
pi.on("tool_execution_end", async (event) => {
|
||||
const result = event.result as any;
|
||||
const resultText = Array.isArray(result?.content)
|
||||
? result.content
|
||||
.filter((c: any) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
type TextContent = { type: string; text: string };
|
||||
type ToolResult = { content?: TextContent[] } | string;
|
||||
const result = event.result as ToolResult;
|
||||
const content = typeof result === "object" ? result.content : undefined;
|
||||
const resultText = Array.isArray(content)
|
||||
? content
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("")
|
||||
: typeof result === "string"
|
||||
? result
|
||||
|
|
@ -184,15 +196,20 @@ export default function remoteControl(pi: ExtensionAPI) {
|
|||
await ctx.ui.custom<void>((_tui, theme, kb, done) => {
|
||||
const container = new Container();
|
||||
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
||||
container.addChild(new Text(
|
||||
container.addChild(
|
||||
new Text(
|
||||
theme.fg("accent", theme.bold(" Remote-control")) +
|
||||
" " +
|
||||
keyHint("tui.select.confirm", "close") +
|
||||
theme.fg("muted", " · ") +
|
||||
keyHint("tui.select.cancel", "cancel"),
|
||||
1, 0,
|
||||
));
|
||||
container.addChild(new Text("\n" + qrLines.map((l) => ` ${l}`).join("\n") + "\n", 1, 0));
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
container.addChild(
|
||||
new Text(`\n${qrLines.map((l) => ` ${l}`).join("\n")}\n`, 1, 0),
|
||||
);
|
||||
container.addChild(new Text(theme.fg("accent", url), 1, 0));
|
||||
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
||||
|
||||
|
|
@ -200,22 +217,29 @@ export default function remoteControl(pi: ExtensionAPI) {
|
|||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
if (kb.matches(data, "tui.select.cancel") || kb.matches(data, "tui.select.confirm")) done();
|
||||
if (
|
||||
kb.matches(data, "tui.select.cancel") ||
|
||||
kb.matches(data, "tui.select.confirm")
|
||||
)
|
||||
done();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
pi.registerCommand("remote-control", {
|
||||
description: "Remote control — start/stop server, configure, show connection info",
|
||||
handler: async (args, ctx) => {
|
||||
description:
|
||||
"Remote control — start/stop server, configure, show connection info",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const isRunning = !!server;
|
||||
const config = await readRemoteControlConfig();
|
||||
const currentUrl = config.publicBaseUrl?.trim();
|
||||
|
||||
const configLabel = currentUrl ? `Configure URL (${currentUrl})` : "Configure URL (not set)";
|
||||
const configLabel = currentUrl
|
||||
? `Configure URL (${currentUrl})`
|
||||
: "Configure URL (not set)";
|
||||
const menuItems = [
|
||||
isRunning ? "Turn off" : "Turn on",
|
||||
configLabel,
|
||||
|
|
@ -228,7 +252,10 @@ export default function remoteControl(pi: ExtensionAPI) {
|
|||
if (choice === "Turn on") {
|
||||
const publicBaseUrl = currentUrl;
|
||||
if (!publicBaseUrl) {
|
||||
ctx.ui.notify("Set the public URL first — opening config…", "warning");
|
||||
ctx.ui.notify(
|
||||
"Set the public URL first — opening config…",
|
||||
"warning",
|
||||
);
|
||||
await configureRemoteControlUI(ctx);
|
||||
// Re-check after config
|
||||
const updated = await readRemoteControlConfig();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,23 @@
|
|||
|
||||
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
interface RawContent {
|
||||
type: string;
|
||||
text?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
arguments?: unknown;
|
||||
}
|
||||
|
||||
export interface RawMessage {
|
||||
role: string;
|
||||
content: string | RawContent[];
|
||||
model?: string;
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export interface RenderMsg {
|
||||
id: string; // SessionEntry id, or "pending" while streaming
|
||||
role: "user" | "assistant" | "tool_result";
|
||||
|
|
@ -18,12 +35,15 @@ export interface RenderMsg {
|
|||
model?: string;
|
||||
}
|
||||
|
||||
export function serializeMessage(id: string, msg: any): RenderMsg | null {
|
||||
export function serializeMessage(
|
||||
id: string,
|
||||
msg: RawMessage,
|
||||
): RenderMsg | null {
|
||||
if (msg.role === "user") {
|
||||
const text =
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: (msg.content as any[])
|
||||
: (msg.content as RawContent[])
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
|
|
@ -31,11 +51,11 @@ export function serializeMessage(id: string, msg: any): RenderMsg | null {
|
|||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
const text = (msg.content as any[])
|
||||
const text = (msg.content as RawContent[])
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const toolCalls = (msg.content as any[])
|
||||
const toolCalls = (msg.content as RawContent[])
|
||||
.filter((c) => c.type === "toolCall")
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
|
|
@ -52,7 +72,7 @@ export function serializeMessage(id: string, msg: any): RenderMsg | null {
|
|||
}
|
||||
|
||||
if (msg.role === "toolResult") {
|
||||
const text = (msg.content as any[])
|
||||
const text = (msg.content as RawContent[])
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
|
|
@ -74,7 +94,10 @@ export function getBranchMessages(ctx: ExtensionContext): RenderMsg[] {
|
|||
const out: RenderMsg[] = [];
|
||||
for (const entry of branch) {
|
||||
if (entry.type !== "message") continue;
|
||||
const m = serializeMessage(entry.id, (entry as any).message);
|
||||
const m = serializeMessage(
|
||||
entry.id,
|
||||
(entry as { id: string; type: string; message: RawMessage }).message,
|
||||
);
|
||||
if (m) out.push(m);
|
||||
}
|
||||
return out;
|
||||
|
|
@ -83,7 +106,7 @@ export function getBranchMessages(ctx: ExtensionContext): RenderMsg[] {
|
|||
function abbreviateHome(p: string): string {
|
||||
const home = process.env.HOME;
|
||||
if (home && p === home) return "~";
|
||||
if (home && p.startsWith(home + "/")) return "~" + p.slice(home.length);
|
||||
if (home && p.startsWith(`${home}/`)) return `~${p.slice(home.length)}`;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,24 +5,50 @@
|
|||
* for real-time message streaming between the pi session and browser clients.
|
||||
*/
|
||||
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { createServer } from "node:http";
|
||||
import { createRequire } from "node:module";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
import type { AddressInfo, Socket } from "node:net";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
generateToken,
|
||||
validateToken,
|
||||
SESSION_COOKIE,
|
||||
generateSessionId,
|
||||
generateToken,
|
||||
parseCookies,
|
||||
SESSION_COOKIE,
|
||||
validateToken,
|
||||
} from "./auth.js";
|
||||
import { buildSyncMessage } from "./messages.js";
|
||||
import { buildHTML } from "./html.js";
|
||||
import { buildSyncMessage } from "./messages.js";
|
||||
|
||||
interface WsClient {
|
||||
readyState: number;
|
||||
send(data: string): void;
|
||||
terminate(): void;
|
||||
on(event: "message", listener: (data: Buffer) => void): void;
|
||||
on(event: "close" | "error", listener: () => void): void;
|
||||
}
|
||||
|
||||
interface WsServer {
|
||||
on(event: "connection", listener: (ws: WsClient) => void): void;
|
||||
on(event: "error", listener: (err: Error) => void): void;
|
||||
handleUpgrade(
|
||||
request: IncomingMessage,
|
||||
socket: Socket,
|
||||
head: Buffer,
|
||||
cb: (ws: WsClient) => void,
|
||||
): void;
|
||||
emit(event: string, ...args: unknown[]): void;
|
||||
close(cb?: () => void): void;
|
||||
}
|
||||
|
||||
// Load ws (bundled with pi) without needing @types/ws installed locally
|
||||
const _require = createRequire(import.meta.url);
|
||||
const wsModule = _require("ws") as {
|
||||
WebSocketServer: new (opts: { noServer: boolean }) => any;
|
||||
WebSocketServer: new (opts: { noServer: boolean }) => WsServer;
|
||||
OPEN: number;
|
||||
};
|
||||
const { WebSocketServer, OPEN } = wsModule;
|
||||
|
|
@ -37,9 +63,12 @@ export interface RemoteServer {
|
|||
token: string;
|
||||
}
|
||||
|
||||
export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<RemoteServer> {
|
||||
export function startServer(
|
||||
pi: ExtensionAPI,
|
||||
ctx: ExtensionContext,
|
||||
): Promise<RemoteServer> {
|
||||
const clientChangeListeners: Array<() => void> = [];
|
||||
const clients = new Set<any>();
|
||||
const clients = new Set<WsClient>();
|
||||
const token = generateToken();
|
||||
// Map of valid session IDs → expiry timestamp (ms since epoch)
|
||||
const SESSION_TTL_MS = 86_400_000; // 24 h — matches cookie Max-Age
|
||||
|
|
@ -52,7 +81,7 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
};
|
||||
|
||||
/** Check if a request is authenticated (valid token query param OR valid session cookie) */
|
||||
function isAuthenticated(req: any): boolean {
|
||||
function isAuthenticated(req: IncomingMessage): boolean {
|
||||
// Check session cookie first
|
||||
const cookies = parseCookies(req.headers.cookie);
|
||||
const sessionId = cookies[SESSION_COOKIE];
|
||||
|
|
@ -92,13 +121,17 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
// Check authentication
|
||||
const cookies = parseCookies(req.headers.cookie);
|
||||
const sc = cookies[SESSION_COOKIE];
|
||||
const hasValidSession = sc !== undefined && (validSessions.get(sc) ?? 0) > Date.now();
|
||||
const hasValidSession =
|
||||
sc !== undefined && (validSessions.get(sc) ?? 0) > Date.now();
|
||||
const providedToken = url.searchParams.get("token");
|
||||
const hasValidToken = providedToken && validateToken(providedToken, token);
|
||||
const hasValidToken =
|
||||
providedToken && validateToken(providedToken, token);
|
||||
|
||||
if (!hasValidSession && !hasValidToken) {
|
||||
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end("Forbidden — valid token required. Use the URL shown in the pi terminal.");
|
||||
res.end(
|
||||
"Forbidden — valid token required. Use the URL shown in the pi terminal.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -122,8 +155,7 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"Content-Security-Policy":
|
||||
`default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'`,
|
||||
"Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'`,
|
||||
});
|
||||
res.end(buildHTML(nonce));
|
||||
} else {
|
||||
|
|
@ -142,7 +174,9 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
console.error("[remote-control] wss error:", err.message);
|
||||
});
|
||||
|
||||
httpServer.on("upgrade", (request: any, socket: any, head: any) => {
|
||||
httpServer.on(
|
||||
"upgrade",
|
||||
(request: IncomingMessage, socket: Socket, head: Buffer) => {
|
||||
const url = new URL(request.url, "http://localhost");
|
||||
if (url.pathname === "/ws") {
|
||||
// Validate auth: session cookie or token query param
|
||||
|
|
@ -151,15 +185,16 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
wss.handleUpgrade(request, socket, head, (ws: any) => {
|
||||
wss.handleUpgrade(request, socket, head, (ws: WsClient) => {
|
||||
wss.emit("connection", ws, request);
|
||||
});
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
wss.on("connection", (ws: any) => {
|
||||
wss.on("connection", (ws: WsClient) => {
|
||||
clients.add(ws);
|
||||
for (const cb of clientChangeListeners) cb();
|
||||
|
||||
|
|
@ -176,11 +211,13 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
const MAX_MSG_BYTES = 64 * 1024;
|
||||
const recentPrompts: number[] = [];
|
||||
|
||||
ws.on("message", (data: any) => {
|
||||
ws.on("message", (data: Buffer) => {
|
||||
if (data.length > MAX_MSG_BYTES) return;
|
||||
let msg: any;
|
||||
let msg: { type?: string; text?: string };
|
||||
try {
|
||||
msg = JSON.parse(data.toString());
|
||||
const parsed: unknown = JSON.parse(data.toString());
|
||||
if (typeof parsed !== "object" || parsed === null) return;
|
||||
msg = parsed as { type?: string; text?: string };
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
|
@ -190,12 +227,17 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === "prompt" && typeof msg.text === "string" && msg.text.trim()) {
|
||||
if (
|
||||
msg.type === "prompt" &&
|
||||
typeof msg.text === "string" &&
|
||||
msg.text.trim()
|
||||
) {
|
||||
const text = msg.text.trim();
|
||||
// Sliding-window rate limit
|
||||
const now = Date.now();
|
||||
const cutoff = now - RATE_WINDOW_MS;
|
||||
while (recentPrompts.length > 0 && recentPrompts[0] < cutoff) recentPrompts.shift();
|
||||
while (recentPrompts.length > 0 && recentPrompts[0] < cutoff)
|
||||
recentPrompts.shift();
|
||||
if (recentPrompts.length >= RATE_MAX) return;
|
||||
recentPrompts.push(now);
|
||||
if (ctx.isIdle()) {
|
||||
|
|
@ -251,9 +293,11 @@ export function startServer(pi: ExtensionAPI, ctx: ExtensionContext): Promise<Re
|
|||
);
|
||||
}),
|
||||
clientCount: () => clients.size,
|
||||
onClientChange: (cb: () => void) => { clientChangeListeners.push(cb); },
|
||||
onClientChange: (cb: () => void) => {
|
||||
clientChangeListeners.push(cb);
|
||||
},
|
||||
get port() {
|
||||
return (httpServer.address() as any)?.port ?? 0;
|
||||
return (httpServer.address() as AddressInfo | null)?.port ?? 0;
|
||||
},
|
||||
get token() {
|
||||
return token;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"ws": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.12",
|
||||
"@types/qrcode": "^1.5.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
@ -785,6 +786,169 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/biome": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.12.tgz",
|
||||
"integrity": "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"bin": {
|
||||
"biome": "bin/biome"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/biome"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@biomejs/cli-darwin-arm64": "2.4.12",
|
||||
"@biomejs/cli-darwin-x64": "2.4.12",
|
||||
"@biomejs/cli-linux-arm64": "2.4.12",
|
||||
"@biomejs/cli-linux-arm64-musl": "2.4.12",
|
||||
"@biomejs/cli-linux-x64": "2.4.12",
|
||||
"@biomejs/cli-linux-x64-musl": "2.4.12",
|
||||
"@biomejs/cli-win32-arm64": "2.4.12",
|
||||
"@biomejs/cli-win32-x64": "2.4.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.12.tgz",
|
||||
"integrity": "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-darwin-x64": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.12.tgz",
|
||||
"integrity": "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.12.tgz",
|
||||
"integrity": "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.12.tgz",
|
||||
"integrity": "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.12.tgz",
|
||||
"integrity": "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.12.tgz",
|
||||
"integrity": "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-arm64": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.12.tgz",
|
||||
"integrity": "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@biomejs/cli-win32-x64": {
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.12.tgz",
|
||||
"integrity": "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@borewit/text-codec": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@
|
|||
"@mariozechner/pi-coding-agent": "*",
|
||||
"@mariozechner/pi-tui": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "biome check --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.12",
|
||||
"@types/qrcode": "^1.5.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue