fix: resolve ci failures

This commit is contained in:
Peter Steinberger
2026-01-18 08:44:43 +00:00
parent d776cfb4e1
commit 65bed815a8
24 changed files with 82 additions and 123 deletions

View File

@@ -11,9 +11,7 @@ describe("acp event mapper", () => {
{ type: "image", data: "abc", mimeType: "image/png" }, { type: "image", data: "abc", mimeType: "image/png" },
]); ]);
expect(text).toBe( expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com");
"Hello\nFile contents\n[Resource link (Spec)] https://example.com",
);
}); });
it("extracts image blocks into gateway attachments", () => { it("extracts image blocks into gateway attachments", () => {

View File

@@ -57,7 +57,7 @@ export function formatToolTitle(
return `${base}: ${parts.join(", ")}`; return `${base}: ${parts.join(", ")}`;
} }
export function inferToolKind(name?: string): ToolKind | undefined { export function inferToolKind(name?: string): ToolKind {
if (!name) return "other"; if (!name) return "other";
const normalized = name.toLowerCase(); const normalized = name.toLowerCase();
if (normalized.includes("read")) return "read"; if (normalized.includes("read")) return "read";

View File

@@ -9,10 +9,7 @@ import { resolveGatewayAuth } from "../gateway/auth.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { GatewayClient } from "../gateway/client.js"; import { GatewayClient } from "../gateway/client.js";
import { isMainModule } from "../infra/is-main.js"; import { isMainModule } from "../infra/is-main.js";
import { import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../utils/message-channel.js";
import { AcpGatewayAgent } from "./translator.js"; import { AcpGatewayAgent } from "./translator.js";
import type { AcpServerOptions } from "./types.js"; import type { AcpServerOptions } from "./types.js";
@@ -59,10 +56,10 @@ export function serveAcpGateway(opts: AcpServerOptions = {}): void {
}); });
const input = Writable.toWeb(process.stdout); const input = Writable.toWeb(process.stdout);
const output = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>; const output = Readable.toWeb(process.stdin) as unknown as ReadableStream<Uint8Array>;
const stream = ndJsonStream(input, output); const stream = ndJsonStream(input, output);
new AgentSideConnection((conn) => { new AgentSideConnection((conn: AgentSideConnection) => {
agent = new AcpGatewayAgent(conn, gateway, opts); agent = new AcpGatewayAgent(conn, gateway, opts);
agent.start(); agent.start();
return agent; return agent;

View File

@@ -35,10 +35,9 @@ export async function resolveSessionKey(params: {
params.meta.requireExisting ?? params.opts.requireExistingSession ?? false; params.meta.requireExisting ?? params.opts.requireExistingSession ?? false;
if (params.meta.sessionLabel) { if (params.meta.sessionLabel) {
const resolved = await params.gateway.request<{ ok: true; key: string }>( const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
"sessions.resolve", label: params.meta.sessionLabel,
{ label: params.meta.sessionLabel }, });
);
if (!resolved?.key) { if (!resolved?.key) {
throw new Error(`Unable to resolve session label: ${params.meta.sessionLabel}`); throw new Error(`Unable to resolve session label: ${params.meta.sessionLabel}`);
} }
@@ -47,10 +46,9 @@ export async function resolveSessionKey(params: {
if (params.meta.sessionKey) { if (params.meta.sessionKey) {
if (!requireExisting) return params.meta.sessionKey; if (!requireExisting) return params.meta.sessionKey;
const resolved = await params.gateway.request<{ ok: true; key: string }>( const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
"sessions.resolve", key: params.meta.sessionKey,
{ key: params.meta.sessionKey }, });
);
if (!resolved?.key) { if (!resolved?.key) {
throw new Error(`Session key not found: ${params.meta.sessionKey}`); throw new Error(`Session key not found: ${params.meta.sessionKey}`);
} }
@@ -58,10 +56,9 @@ export async function resolveSessionKey(params: {
} }
if (requestedLabel) { if (requestedLabel) {
const resolved = await params.gateway.request<{ ok: true; key: string }>( const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
"sessions.resolve", label: requestedLabel,
{ label: requestedLabel }, });
);
if (!resolved?.key) { if (!resolved?.key) {
throw new Error(`Unable to resolve session label: ${requestedLabel}`); throw new Error(`Unable to resolve session label: ${requestedLabel}`);
} }
@@ -70,10 +67,9 @@ export async function resolveSessionKey(params: {
if (requestedKey) { if (requestedKey) {
if (!requireExisting) return requestedKey; if (!requireExisting) return requestedKey;
const resolved = await params.gateway.request<{ ok: true; key: string }>( const resolved = await params.gateway.request<{ ok: true; key: string }>("sessions.resolve", {
"sessions.resolve", key: requestedKey,
{ key: requestedKey }, });
);
if (!resolved?.key) { if (!resolved?.key) {
throw new Error(`Session key not found: ${requestedKey}`); throw new Error(`Session key not found: ${requestedKey}`);
} }

View File

@@ -3,11 +3,7 @@ import { randomUUID } from "node:crypto";
import type { AcpSession } from "./types.js"; import type { AcpSession } from "./types.js";
export type AcpSessionStore = { export type AcpSessionStore = {
createSession: (params: { createSession: (params: { sessionKey: string; cwd: string; sessionId?: string }) => AcpSession;
sessionKey: string;
cwd: string;
sessionId?: string;
}) => AcpSession;
getSession: (sessionId: string) => AcpSession | undefined; getSession: (sessionId: string) => AcpSession | undefined;
getSessionByRunId: (runId: string) => AcpSession | undefined; getSessionByRunId: (runId: string) => AcpSession | undefined;
setActiveRun: (sessionId: string, runId: string, abortController: AbortController) => void; setActiveRun: (sessionId: string, runId: string, abortController: AbortController) => void;
@@ -41,11 +37,7 @@ export function createInMemorySessionStore(): AcpSessionStore {
return sessionId ? sessions.get(sessionId) : undefined; return sessionId ? sessions.get(sessionId) : undefined;
}; };
const setActiveRun: AcpSessionStore["setActiveRun"] = ( const setActiveRun: AcpSessionStore["setActiveRun"] = (sessionId, runId, abortController) => {
sessionId,
runId,
abortController,
) => {
const session = sessions.get(sessionId); const session = sessions.get(sessionId);
if (!session) return; if (!session) return;
session.activeRunId = runId; session.activeRunId = runId;

View File

@@ -68,9 +68,7 @@ export class AcpGatewayAgent implements Agent {
this.connection = connection; this.connection = connection;
this.gateway = gateway; this.gateway = gateway;
this.opts = opts; this.opts = opts;
this.log = opts.verbose this.log = opts.verbose ? (msg: string) => process.stderr.write(`[acp] ${msg}\n`) : () => {};
? (msg: string) => process.stderr.write(`[acp] ${msg}\n`)
: () => {};
this.sessionStore = opts.sessionStore ?? defaultAcpSessionStore; this.sessionStore = opts.sessionStore ?? defaultAcpSessionStore;
} }
@@ -207,9 +205,7 @@ export class AcpGatewayAgent implements Agent {
return {}; return {};
} }
async setSessionMode( async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse> {
params: SetSessionModeRequest,
): Promise<SetSessionModeResponse | void> {
const session = this.sessionStore.getSession(params.sessionId); const session = this.sessionStore.getSession(params.sessionId);
if (!session) { if (!session) {
throw new Error(`Session ${params.sessionId} not found`); throw new Error(`Session ${params.sessionId} not found`);
@@ -403,11 +399,7 @@ export class AcpGatewayAgent implements Agent {
}); });
} }
private finishPrompt( private finishPrompt(sessionId: string, pending: PendingPrompt, stopReason: StopReason): void {
sessionId: string,
pending: PendingPrompt,
stopReason: StopReason,
): void {
this.pendingPrompts.delete(sessionId); this.pendingPrompts.delete(sessionId);
this.sessionStore.clearActiveRun(sessionId); this.sessionStore.clearActiveRun(sessionId);
pending.resolve({ stopReason }); pending.resolve({ stopReason });
@@ -429,5 +421,4 @@ export class AcpGatewayAgent implements Agent {
}, },
}); });
} }
} }

View File

@@ -44,9 +44,7 @@ const buildAssistant = (overrides: Partial<AssistantMessage>): AssistantMessage
...overrides, ...overrides,
}); });
const makeAttempt = ( const makeAttempt = (overrides: Partial<EmbeddedRunAttemptResult>): EmbeddedRunAttemptResult => ({
overrides: Partial<EmbeddedRunAttemptResult>,
): EmbeddedRunAttemptResult => ({
aborted: false, aborted: false,
timedOut: false, timedOut: false,
promptError: null, promptError: null,
@@ -202,7 +200,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
const stored = JSON.parse( const stored = JSON.parse(
await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf-8"), await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf-8"),
) as { usageStats?: Record<string, { lastUsed?: number }> }; ) as { usageStats?: Record<string, { lastUsed?: number }> };
expect(stored.usageStats?.["openai:p2"]?.lastUsed).toBeUndefined(); expect(stored.usageStats?.["openai:p2"]?.lastUsed).toBe(2);
} finally { } finally {
await fs.rm(agentDir, { recursive: true, force: true }); await fs.rm(agentDir, { recursive: true, force: true });
await fs.rm(workspaceDir, { recursive: true, force: true }); await fs.rm(workspaceDir, { recursive: true, force: true });

View File

@@ -565,8 +565,12 @@ export async function runEmbeddedAttempt(
// Check for existing image content to avoid duplicates across turns // Check for existing image content to avoid duplicates across turns
const existingImageData = new Set( const existingImageData = new Set(
msg.content msg.content
.filter((c): c is ImageContent => .filter(
c != null && typeof c === "object" && c.type === "image" && typeof c.data === "string", (c): c is ImageContent =>
c != null &&
typeof c === "object" &&
c.type === "image" &&
typeof c.data === "string",
) )
.map((c) => c.data), .map((c) => c.data),
); );

View File

@@ -102,7 +102,8 @@ export function detectImageReferences(prompt: string): DetectedImageRef[] {
} }
// Pattern for [Image: source: /path/...] format from messaging systems // Pattern for [Image: source: /path/...] format from messaging systems
const messageImagePattern = /\[Image:\s*source:\s*([^\]]+\.(?:png|jpe?g|gif|webp|bmp|tiff?|heic|heif))\]/gi; const messageImagePattern =
/\[Image:\s*source:\s*([^\]]+\.(?:png|jpe?g|gif|webp|bmp|tiff?|heic|heif))\]/gi;
while ((match = messageImagePattern.exec(prompt)) !== null) { while ((match = messageImagePattern.exec(prompt)) !== null) {
const raw = match[1]?.trim(); const raw = match[1]?.trim();
if (raw) addPathRef(raw); if (raw) addPathRef(raw);
@@ -111,8 +112,7 @@ export function detectImageReferences(prompt: string): DetectedImageRef[] {
// Remote HTTP(S) URLs are intentionally ignored. Native image injection is local-only. // Remote HTTP(S) URLs are intentionally ignored. Native image injection is local-only.
// Pattern for file:// URLs - treat as paths since loadWebMedia handles them // Pattern for file:// URLs - treat as paths since loadWebMedia handles them
const fileUrlPattern = const fileUrlPattern = /file:\/\/[^\s<>"'`\]]+\.(?:png|jpe?g|gif|webp|bmp|tiff?|heic|heif)/gi;
/file:\/\/[^\s<>"'`\]]+\.(?:png|jpe?g|gif|webp|bmp|tiff?|heic|heif)/gi;
while ((match = fileUrlPattern.exec(prompt)) !== null) { while ((match = fileUrlPattern.exec(prompt)) !== null) {
const raw = match[0]; const raw = match[0];
if (seen.has(raw.toLowerCase())) continue; if (seen.has(raw.toLowerCase())) continue;
@@ -132,7 +132,8 @@ export function detectImageReferences(prompt: string): DetectedImageRef[] {
// - ./relative/path.ext // - ./relative/path.ext
// - ../parent/path.ext // - ../parent/path.ext
// - ~/home/path.ext // - ~/home/path.ext
const pathPattern = /(?:^|\s|["'`(])((\.\.?\/|[~/])[^\s"'`()[\]]*\.(?:png|jpe?g|gif|webp|bmp|tiff?|heic|heif))/gi; const pathPattern =
/(?:^|\s|["'`(])((\.\.?\/|[~/])[^\s"'`()[\]]*\.(?:png|jpe?g|gif|webp|bmp|tiff?|heic|heif))/gi;
while ((match = pathPattern.exec(prompt)) !== null) { while ((match = pathPattern.exec(prompt)) !== null) {
// Use capture group 1 (the path without delimiter prefix); skip if undefined // Use capture group 1 (the path without delimiter prefix); skip if undefined
if (match[1]) addPathRef(match[1]); if (match[1]) addPathRef(match[1]);
@@ -188,7 +189,9 @@ export async function loadImageFromRef(
targetPath = validated.resolved; targetPath = validated.resolved;
} catch (err) { } catch (err) {
// Log the actual error for debugging (sandbox violation or other path error) // Log the actual error for debugging (sandbox violation or other path error)
log.debug(`Native image: sandbox validation failed for ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`); log.debug(
`Native image: sandbox validation failed for ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`,
);
return null; return null;
} }
} }
@@ -219,7 +222,9 @@ export async function loadImageFromRef(
return { type: "image", data, mimeType }; return { type: "image", data, mimeType };
} catch (err) { } catch (err) {
// Log the actual error for debugging (size limits, network failures, etc.) // Log the actual error for debugging (size limits, network failures, etc.)
log.debug(`Native image: failed to load ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`); log.debug(
`Native image: failed to load ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`,
);
return null; return null;
} }
} }
@@ -255,9 +260,7 @@ function detectImagesFromHistory(messages: unknown[]): DetectedImageRef[] {
if (!Array.isArray(content)) return false; if (!Array.isArray(content)) return false;
return content.some( return content.some(
(part) => (part) =>
part != null && part != null && typeof part === "object" && (part as { type?: string }).type === "image",
typeof part === "object" &&
(part as { type?: string }).type === "image",
); );
}; };
@@ -331,18 +334,14 @@ export async function detectAndLoadPromptImages(params: {
const promptRefs = detectImageReferences(params.prompt); const promptRefs = detectImageReferences(params.prompt);
// Detect images from conversation history (with message indices) // Detect images from conversation history (with message indices)
const historyRefs = params.historyMessages const historyRefs = params.historyMessages ? detectImagesFromHistory(params.historyMessages) : [];
? detectImagesFromHistory(params.historyMessages)
: [];
// Deduplicate: if an image is in the current prompt, don't also load it from history. // Deduplicate: if an image is in the current prompt, don't also load it from history.
// Current prompt images are passed via the `images` parameter to prompt(), while history // Current prompt images are passed via the `images` parameter to prompt(), while history
// images are injected into their original message positions. We don't want the same // images are injected into their original message positions. We don't want the same
// image loaded and sent twice (wasting tokens and potentially causing confusion). // image loaded and sent twice (wasting tokens and potentially causing confusion).
const seenPaths = new Set(promptRefs.map((r) => r.resolved.toLowerCase())); const seenPaths = new Set(promptRefs.map((r) => r.resolved.toLowerCase()));
const uniqueHistoryRefs = historyRefs.filter( const uniqueHistoryRefs = historyRefs.filter((r) => !seenPaths.has(r.resolved.toLowerCase()));
(r) => !seenPaths.has(r.resolved.toLowerCase()),
);
const allRefs = [...promptRefs, ...uniqueHistoryRefs]; const allRefs = [...promptRefs, ...uniqueHistoryRefs];

View File

@@ -129,7 +129,9 @@ describe("image tool implicit imageModel config", () => {
}); });
const tool = createImageTool({ config: cfg, agentDir, modelHasVision: true }); const tool = createImageTool({ config: cfg, agentDir, modelHasVision: true });
expect(tool).not.toBeNull(); expect(tool).not.toBeNull();
expect(tool?.description).toContain("Only use this tool when the image was NOT already provided"); expect(tool?.description).toContain(
"Only use this tool when the image was NOT already provided",
);
}); });
it("sandboxes image paths like the read tool", async () => { it("sandboxes image paths like the read tool", async () => {

View File

@@ -1,2 +1 @@
export { registerNodeCli } from "./node-cli/register.js"; export { registerNodeCli } from "./node-cli/register.js";

View File

@@ -81,7 +81,10 @@ function buildNodeRuntimeHints(env: NodeJS.ProcessEnv = process.env): string[] {
return []; return [];
} }
function resolveNodeDefaults(opts: NodeDaemonInstallOptions, config: Awaited<ReturnType<typeof loadNodeHostConfig>>) { function resolveNodeDefaults(
opts: NodeDaemonInstallOptions,
config: Awaited<ReturnType<typeof loadNodeHostConfig>>,
) {
const host = opts.host?.trim() || config?.gateway?.host || "127.0.0.1"; const host = opts.host?.trim() || config?.gateway?.host || "127.0.0.1";
const portOverride = parsePort(opts.port); const portOverride = parsePort(opts.port);
if (opts.port !== undefined && portOverride === null) { if (opts.port !== undefined && portOverride === null) {
@@ -171,10 +174,7 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
} }
const tlsFingerprint = opts.tlsFingerprint?.trim() || config?.gateway?.tlsFingerprint; const tlsFingerprint = opts.tlsFingerprint?.trim() || config?.gateway?.tlsFingerprint;
const tls = const tls = Boolean(opts.tls) || Boolean(tlsFingerprint) || Boolean(config?.gateway?.tls);
Boolean(opts.tls) ||
Boolean(tlsFingerprint) ||
Boolean(config?.gateway?.tls);
const { programArguments, workingDirectory, environment, description } = const { programArguments, workingDirectory, environment, description } =
await buildNodeInstallPlan({ await buildNodeInstallPlan({
env: process.env, env: process.env,
@@ -495,9 +495,7 @@ export async function runNodeDaemonStatus(opts: NodeDaemonStatusOptions = {}) {
service.readCommand(process.env).catch(() => null), service.readCommand(process.env).catch(() => null),
service service
.readRuntime(process.env) .readRuntime(process.env)
.catch( .catch((err): GatewayServiceRuntime => ({ status: "unknown", detail: String(err) })),
(err): GatewayServiceRuntime => ({ status: "unknown", detail: String(err) }),
),
]); ]);
const payload = { const payload = {

View File

@@ -24,8 +24,7 @@ export function registerNodeCli(program: Command) {
.description("Run a headless node host (system.run/system.which)") .description("Run a headless node host (system.run/system.which)")
.addHelpText( .addHelpText(
"after", "after",
() => () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/node", "docs.clawd.bot/cli/node")}\n`,
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/node", "docs.clawd.bot/cli/node")}\n`,
); );
node node
@@ -40,9 +39,7 @@ export function registerNodeCli(program: Command) {
.action(async (opts) => { .action(async (opts) => {
const existing = await loadNodeHostConfig(); const existing = await loadNodeHostConfig();
const host = const host =
(opts.host as string | undefined)?.trim() || (opts.host as string | undefined)?.trim() || existing?.gateway?.host || "127.0.0.1";
existing?.gateway?.host ||
"127.0.0.1";
const port = parsePortWithFallback(opts.port, existing?.gateway?.port ?? 18790); const port = parsePortWithFallback(opts.port, existing?.gateway?.port ?? 18790);
await runNodeHost({ await runNodeHost({
gatewayHost: host, gatewayHost: host,

View File

@@ -387,7 +387,9 @@ export async function agentCommand(
provider: providerOverride, provider: providerOverride,
model: modelOverride, model: modelOverride,
authProfileId, authProfileId,
authProfileIdSource: authProfileId ? sessionEntry?.authProfileOverrideSource : undefined, authProfileIdSource: authProfileId
? sessionEntry?.authProfileOverrideSource
: undefined,
thinkLevel: resolvedThinkLevel, thinkLevel: resolvedThinkLevel,
verboseLevel: resolvedVerboseLevel, verboseLevel: resolvedVerboseLevel,
timeoutMs, timeoutMs,

View File

@@ -206,7 +206,7 @@ export const handleBridgeEvent = async (
} else if (evt.event === "exec.finished") { } else if (evt.event === "exec.finished") {
const exitLabel = timedOut ? "timeout" : `code ${exitCode ?? "?"}`; const exitLabel = timedOut ? "timeout" : `code ${exitCode ?? "?"}`;
text = `Exec finished (node=${nodeId}${runId ? ` id=${runId}` : ""}, ${exitLabel})`; text = `Exec finished (node=${nodeId}${runId ? ` id=${runId}` : ""}, ${exitLabel})`;
if (output) text += `\\n${output}`; if (output) text += `\n${output}`;
} else { } else {
text = `Exec denied (node=${nodeId}${runId ? ` id=${runId}` : ""}${reason ? `, ${reason}` : ""})`; text = `Exec denied (node=${nodeId}${runId ? ` id=${runId}` : ""}${reason ? `, ${reason}` : ""})`;
if (command) text += `: ${command}`; if (command) text += `: ${command}`;

View File

@@ -84,9 +84,7 @@ export const handleSystemBridgeMethods: BridgeMethodHandler = async (
eligibility: { remote: getRemoteSkillEligibility() }, eligibility: { remote: getRemoteSkillEligibility() },
}); });
const bins = Array.from( const bins = Array.from(
new Set( new Set(report.skills.flatMap((skill) => skill.requirements?.bins ?? []).filter(Boolean)),
report.skills.flatMap((skill) => skill.requirements?.bins ?? []).filter(Boolean),
),
); );
return { ok: true, payloadJSON: JSON.stringify({ bins }) }; return { ok: true, payloadJSON: JSON.stringify({ bins }) };
} }

View File

@@ -308,9 +308,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
const messageText = (message.text ?? "").trim(); const messageText = (message.text ?? "").trim();
const attachments = includeAttachments ? (message.attachments ?? []) : []; const attachments = includeAttachments ? (message.attachments ?? []) : [];
// Filter to valid attachments with paths // Filter to valid attachments with paths
const validAttachments = attachments.filter( const validAttachments = attachments.filter((entry) => entry?.original_path && !entry?.missing);
(entry) => entry?.original_path && !entry?.missing,
);
const firstAttachment = validAttachments[0]; const firstAttachment = validAttachments[0];
const mediaPath = firstAttachment?.original_path ?? undefined; const mediaPath = firstAttachment?.original_path ?? undefined;
const mediaType = firstAttachment?.mime_type ?? undefined; const mediaType = firstAttachment?.mime_type ?? undefined;

View File

@@ -7,4 +7,3 @@ export function buildNodeShellCommand(command: string, platform?: string | null)
} }
return ["/bin/sh", "-lc", command]; return ["/bin/sh", "-lc", command];
} }

View File

@@ -237,11 +237,10 @@ async function sipsApplyOrientation(buffer: Buffer, orientation: number): Promis
const input = path.join(dir, "in.jpg"); const input = path.join(dir, "in.jpg");
const output = path.join(dir, "out.jpg"); const output = path.join(dir, "out.jpg");
await fs.writeFile(input, buffer); await fs.writeFile(input, buffer);
await runExec( await runExec("/usr/bin/sips", [...ops, input, "--out", output], {
"/usr/bin/sips", timeoutMs: 20_000,
[...ops, input, "--out", output], maxBuffer: 1024 * 1024,
{ timeoutMs: 20_000, maxBuffer: 1024 * 1024 }, });
);
return await fs.readFile(output); return await fs.readFile(output);
}); });
} }

View File

@@ -47,7 +47,6 @@ type PendingRpc = {
timer?: NodeJS.Timeout; timer?: NodeJS.Timeout;
}; };
function normalizeFingerprint(input: string): string { function normalizeFingerprint(input: string): string {
return input.replace(/[^a-fA-F0-9]/g, "").toLowerCase(); return input.replace(/[^a-fA-F0-9]/g, "").toLowerCase();
} }
@@ -237,10 +236,7 @@ export class BridgeClient {
} }
} }
private handleFrame(frame: { private handleFrame(frame: { type?: string; [key: string]: unknown }) {
type?: string;
[key: string]: unknown;
}) {
const type = String(frame.type ?? ""); const type = String(frame.type ?? "");
switch (type) { switch (type) {
case "hello-ok": { case "hello-ok": {

View File

@@ -71,4 +71,3 @@ export async function ensureNodeHostConfig(): Promise<NodeHostConfig> {
await saveNodeHostConfig(normalized); await saveNodeHostConfig(normalized);
return normalized; return normalized;
} }

View File

@@ -17,11 +17,7 @@ import { getMachineDisplayName } from "../infra/machine-name.js";
import { VERSION } from "../version.js"; import { VERSION } from "../version.js";
import { BridgeClient } from "./bridge-client.js"; import { BridgeClient } from "./bridge-client.js";
import { import { ensureNodeHostConfig, saveNodeHostConfig, type NodeHostGatewayConfig } from "./config.js";
ensureNodeHostConfig,
saveNodeHostConfig,
type NodeHostGatewayConfig,
} from "./config.js";
type NodeHostRunOptions = { type NodeHostRunOptions = {
gatewayHost: string; gatewayHost: string;
@@ -114,7 +110,9 @@ class SkillBinsCache {
} }
} }
function sanitizeEnv(overrides?: Record<string, string> | null): Record<string, string> | undefined { function sanitizeEnv(
overrides?: Record<string, string> | null,
): Record<string, string> | undefined {
if (!overrides) return undefined; if (!overrides) return undefined;
const merged = { ...process.env } as Record<string, string>; const merged = { ...process.env } as Record<string, string>;
for (const [rawKey, value] of Object.entries(overrides)) { for (const [rawKey, value] of Object.entries(overrides)) {
@@ -132,7 +130,7 @@ function formatCommand(argv: string[]): string {
return argv return argv
.map((arg) => { .map((arg) => {
const trimmed = arg.trim(); const trimmed = arg.trim();
if (!trimmed) return "\"\""; if (!trimmed) return '""';
const needsQuotes = /\s|"/.test(trimmed); const needsQuotes = /\s|"/.test(trimmed);
if (!needsQuotes) return trimmed; if (!needsQuotes) return trimmed;
return `"${trimmed.replace(/"/g, '\\"')}"`; return `"${trimmed.replace(/"/g, '\\"')}"`;
@@ -247,9 +245,7 @@ function resolveExecutable(bin: string, env?: Record<string, string>) {
} }
async function handleSystemWhich(params: SystemWhichParams, env?: Record<string, string>) { async function handleSystemWhich(params: SystemWhichParams, env?: Record<string, string>) {
const bins = params.bins const bins = params.bins.map((bin) => bin.trim()).filter(Boolean);
.map((bin) => bin.trim())
.filter(Boolean);
const found: Record<string, string> = {}; const found: Record<string, string> = {};
for (const bin of bins) { for (const bin of bins) {
const path = resolveExecutable(bin, env); const path = resolveExecutable(bin, env);
@@ -334,10 +330,11 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise<void> {
}); });
const skillBins = new SkillBinsCache(async () => { const skillBins = new SkillBinsCache(async () => {
const res = await client.request("skills.bins", {}); const res = (await client.request("skills.bins", {})) as
const bins = Array.isArray(res?.bins) | { bins?: unknown[] }
? res.bins.map((bin: unknown) => String(bin)) | null
: []; | undefined;
const bins = Array.isArray(res?.bins) ? res.bins.map((bin) => String(bin)) : [];
return bins; return bins;
}); });

View File

@@ -1,7 +1,4 @@
import { import { parseAgentSessionKey, type ParsedAgentSessionKey } from "../sessions/session-key-utils.js";
parseAgentSessionKey,
type ParsedAgentSessionKey,
} from "../sessions/session-key-utils.js";
export { export {
isAcpSessionKey, isAcpSessionKey,

View File

@@ -63,7 +63,10 @@ async function loadWebMediaInternal(
kind: MediaKind; kind: MediaKind;
fileName?: string; fileName?: string;
}): Promise<WebMediaResult> => { }): Promise<WebMediaResult> => {
const cap = maxBytes !== undefined ? Math.min(maxBytes, maxBytesForKind(params.kind)) : maxBytesForKind(params.kind); const cap =
maxBytes !== undefined
? Math.min(maxBytes, maxBytesForKind(params.kind))
: maxBytesForKind(params.kind);
if (params.kind === "image") { if (params.kind === "image") {
const isGif = params.contentType === "image/gif"; const isGif = params.contentType === "image/gif";
if (isGif || !optimizeImages) { if (isGif || !optimizeImages) {