diff --git a/.oxlintrc.json b/.oxlintrc.json index 947c86c899..c9891efb05 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,13 +8,13 @@ }, "rules": { "curly": "error", - "eslint-plugin-unicorn/prefer-array-find": "off", + "eslint-plugin-unicorn/prefer-array-find": "error", "eslint/no-await-in-loop": "off", - "eslint/no-new": "off", + "eslint/no-new": "error", "eslint/no-shadow": "off", - "eslint/no-unmodified-loop-condition": "off", - "eslint-plugin-unicorn/prefer-set-size": "off", - "oxc/no-accumulating-spread": "off", + "eslint/no-unmodified-loop-condition": "error", + "eslint-plugin-unicorn/prefer-set-size": "error", + "oxc/no-accumulating-spread": "error", "oxc/no-async-endpoint-handlers": "off", "oxc/no-map-spread": "off", "typescript/consistent-return": "error", @@ -23,8 +23,8 @@ "typescript/no-unnecessary-type-conversion": "error", "typescript/no-unsafe-type-assertion": "off", "unicorn/consistent-function-scoping": "off", - "unicorn/prefer-set-size": "off", - "unicorn/require-post-message-target-origin": "off" + "unicorn/prefer-set-size": "error", + "unicorn/require-post-message-target-origin": "error" }, "ignorePatterns": [ "assets/", diff --git a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js index 530287ca21..b2d03165aa 100644 --- a/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js +++ b/apps/shared/OpenClawKit/Tools/CanvasA2UI/bootstrap.js @@ -466,8 +466,10 @@ class OpenClawA2UIHost extends LitElement { try { // WebKit message handlers support structured objects; Android's JS interface expects strings. if (handler === globalThis.openclawCanvasA2UIAction) { + // oxlint-disable-next-line unicorn/require-post-message-target-origin -- Native app message handler, not Window.postMessage. handler.postMessage(JSON.stringify({ userAction })); } else { + // oxlint-disable-next-line unicorn/require-post-message-target-origin -- WebKit message handler, not Window.postMessage. handler.postMessage({ userAction }); } } catch (e) { diff --git a/extensions/bluebubbles/src/monitor-debounce.ts b/extensions/bluebubbles/src/monitor-debounce.ts index 8005012906..24718cc4d1 100644 --- a/extensions/bluebubbles/src/monitor-debounce.ts +++ b/extensions/bluebubbles/src/monitor-debounce.ts @@ -89,9 +89,7 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized const latestTimestamp = timestamps.length > 0 ? Math.max(...timestamps) : first.timestamp; // Collect all message IDs for reference - const messageIds = entries - .map((e) => e.message.messageId) - .filter((id): id is string => Boolean(id)); + const messageId = entries.map((e) => e.message.messageId).find((id): id is string => Boolean(id)); // Prefer reply context from any entry that has it const entryWithReply = entries.find((e) => e.message.replyToId); @@ -102,7 +100,7 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized attachments: allAttachments.length > 0 ? allAttachments : first.attachments, timestamp: latestTimestamp, // Use first message's ID as primary (for reply reference), but we've coalesced others - messageId: messageIds[0] ?? first.messageId, + messageId: messageId ?? first.messageId, // Preserve reply context if present replyToId: entryWithReply?.message.replyToId ?? first.replyToId, replyToBody: entryWithReply?.message.replyToBody ?? first.replyToBody, diff --git a/extensions/bluebubbles/src/setup-surface.ts b/extensions/bluebubbles/src/setup-surface.ts index d066ed62ef..44e5155fe8 100644 --- a/extensions/bluebubbles/src/setup-surface.ts +++ b/extensions/bluebubbles/src/setup-surface.ts @@ -83,7 +83,9 @@ function validateBlueBubblesServerUrlInput(value: unknown): string | undefined { } try { const normalized = normalizeBlueBubblesServerUrl(trimmed); - new URL(normalized); + if (!URL.canParse(normalized)) { + return "Invalid URL format"; + } return undefined; } catch { return "Invalid URL format"; diff --git a/extensions/copilot-proxy/index.ts b/extensions/copilot-proxy/index.ts index ef0aa61030..b14847df6a 100644 --- a/extensions/copilot-proxy/index.ts +++ b/extensions/copilot-proxy/index.ts @@ -41,12 +41,7 @@ function normalizeBaseUrl(value: string): string { function validateBaseUrl(value: string): string | undefined { const normalized = normalizeBaseUrl(value); - try { - new URL(normalized); - } catch { - return "Enter a valid URL"; - } - return undefined; + return URL.canParse(normalized) ? undefined : "Enter a valid URL"; } function parseModelIds(input: string): string[] { diff --git a/extensions/matrix/src/matrix/monitor/inbound-dedupe.ts b/extensions/matrix/src/matrix/monitor/inbound-dedupe.ts index d01c70b917..961356ccb8 100644 --- a/extensions/matrix/src/matrix/monitor/inbound-dedupe.ts +++ b/extensions/matrix/src/matrix/monitor/inbound-dedupe.ts @@ -188,7 +188,10 @@ export async function createMatrixInboundEventDeduper(params: { clearTimeout(persistTimer); persistTimer = null; } - while (dirty || persistPromise) { + for (;;) { + if (!dirty && !persistPromise) { + break; + } if (dirty && !persistPromise) { persistPromise = persist().finally(() => { persistPromise = null; diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index 34bd2f2c12..1bbcc19792 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -248,9 +248,10 @@ describe("MatrixClient request hardening", () => { }); it("injects a guarded fetchFn into matrix-js-sdk", () => { - new MatrixClient("https://matrix.example.org", "token", { + const client = new MatrixClient("https://matrix.example.org", "token", { ssrfPolicy: { allowPrivateNetwork: true }, }); + expect(client).toBeInstanceOf(MatrixClient); expect(lastCreateClientOpts).toMatchObject({ baseUrl: "https://matrix.example.org", @@ -1323,10 +1324,11 @@ describe("MatrixClient crypto bootstrapping", () => { "utf8", ); - new MatrixClient("https://matrix.example.org", "token", { + const client = new MatrixClient("https://matrix.example.org", "token", { encryption: true, recoveryKeyPath, }); + expect(client).toBeInstanceOf(MatrixClient); const callbacks = (lastCreateClientOpts?.cryptoCallbacks ?? null) as { getSecretStorageKey?: ( @@ -1345,7 +1347,8 @@ describe("MatrixClient crypto bootstrapping", () => { }); it("provides a matrix-js-sdk logger to createClient", () => { - new MatrixClient("https://matrix.example.org", "token"); + const client = new MatrixClient("https://matrix.example.org", "token"); + expect(client).toBeInstanceOf(MatrixClient); const logger = (lastCreateClientOpts?.logger ?? null) as { debug?: (...args: unknown[]) => void; getChild?: (namespace: string) => unknown; diff --git a/extensions/microsoft-foundry/onboard.ts b/extensions/microsoft-foundry/onboard.ts index 9ab288388d..bd9402dae4 100644 --- a/extensions/microsoft-foundry/onboard.ts +++ b/extensions/microsoft-foundry/onboard.ts @@ -256,12 +256,7 @@ async function promptEndpointAndModelBase( if (!val) { return "Endpoint URL is required"; } - try { - new URL(val); - } catch { - return "Invalid URL"; - } - return undefined; + return URL.canParse(val) ? undefined : "Invalid URL"; }, }) ).trim(); diff --git a/extensions/qa-lab/src/manual-lane.runtime.ts b/extensions/qa-lab/src/manual-lane.runtime.ts index 27324b7ea4..cdea2e2998 100644 --- a/extensions/qa-lab/src/manual-lane.runtime.ts +++ b/extensions/qa-lab/src/manual-lane.runtime.ts @@ -108,11 +108,10 @@ export async function runQaManualLane(params: QaManualLaneParams) { const reply = lab.state .getSnapshot() - .messages.filter( + .messages.findLast( (candidate) => candidate.direction === "outbound" && candidate.conversation.id === "qa-operator", - ) - .at(-1)?.text ?? null; + )?.text ?? null; return { model: params.primaryModel, diff --git a/extensions/qa-lab/src/suite.test.ts b/extensions/qa-lab/src/suite.test.ts index d9a28647c0..f0a41aa164 100644 --- a/extensions/qa-lab/src/suite.test.ts +++ b/extensions/qa-lab/src/suite.test.ts @@ -188,13 +188,12 @@ describe("qa suite failure reply handling", () => { () => state .getSnapshot() - .messages.filter( + .messages.findLast( (message) => message.direction === "outbound" && message.conversation.id === "qa-operator" && message.text.includes("ALPHA-7"), - ) - .at(-1), + ), 5_000, 10, ); @@ -236,13 +235,12 @@ describe("qa suite failure reply handling", () => { state .getSnapshot() .messages.slice(3) - .filter( + .findLast( (message) => message.direction === "outbound" && message.conversation.id === "qa-operator" && message.text.includes("mission"), - ) - .at(-1), + ), 150, 10, ); diff --git a/extensions/signal/src/sse-reconnect.ts b/extensions/signal/src/sse-reconnect.ts index 7e06f3a98e..75bfe480a4 100644 --- a/extensions/signal/src/sse-reconnect.ts +++ b/extensions/signal/src/sse-reconnect.ts @@ -45,7 +45,10 @@ export async function runSignalSseLoop({ logVerbose(message); }; - while (!abortSignal?.aborted) { + for (;;) { + if (abortSignal?.aborted) { + break; + } try { await streamSignalEvents({ baseUrl, diff --git a/extensions/slack/src/send.ts b/extensions/slack/src/send.ts index 822a251677..ae95dd12a1 100644 --- a/extensions/slack/src/send.ts +++ b/extensions/slack/src/send.ts @@ -111,24 +111,25 @@ async function postSlackMessageBestEffort(params: { thread_ts: params.threadTs, ...(params.blocks?.length ? { blocks: params.blocks } : {}), }; + const postChatMessage = params.client.chat.postMessage.bind(params.client.chat); try { // Slack Web API types model icon_url and icon_emoji as mutually exclusive. // Build payloads in explicit branches so TS and runtime stay aligned. if (params.identity?.iconUrl) { - return await params.client.chat.postMessage({ + return await postChatMessage({ ...basePayload, ...(params.identity.username ? { username: params.identity.username } : {}), icon_url: params.identity.iconUrl, }); } if (params.identity?.iconEmoji) { - return await params.client.chat.postMessage({ + return await postChatMessage({ ...basePayload, ...(params.identity.username ? { username: params.identity.username } : {}), icon_emoji: params.identity.iconEmoji, }); } - return await params.client.chat.postMessage({ + return await postChatMessage({ ...basePayload, ...(params.identity?.username ? { username: params.identity.username } : {}), }); @@ -137,7 +138,7 @@ async function postSlackMessageBestEffort(params: { throw err; } logVerbose("slack send: missing chat:write.customize, retrying without custom identity"); - return params.client.chat.postMessage(basePayload); + return postChatMessage(basePayload); } } diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index 662450dcc0..1182672339 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -71,7 +71,10 @@ function installPollingStallWatchdogHarness( return { async waitForWatchdog() { - for (let attempt = 0; attempt < 20 && !watchdog; attempt += 1) { + for (let attempt = 0; attempt < 20; attempt += 1) { + if (watchdog) { + break; + } await Promise.resolve(); } expect(watchdog).toBeTypeOf("function"); diff --git a/extensions/whatsapp/src/session-errors.ts b/extensions/whatsapp/src/session-errors.ts index 1aca21a107..6ab13e129e 100644 --- a/extensions/whatsapp/src/session-errors.ts +++ b/extensions/whatsapp/src/session-errors.ts @@ -99,8 +99,10 @@ export function formatError(err: unknown): string { typeof (err as { error?: { message?: unknown } })?.error?.message === "string" ? ((err as { error?: { message?: unknown } }).error?.message as string) : undefined, - ].filter((value): value is string => Boolean(value && value.trim().length > 0)); - const message = messageCandidates[0]; + ]; + const message = messageCandidates.find((value): value is string => + Boolean(value && value.trim().length > 0), + ); const pieces: string[] = []; if (typeof status === "number") { diff --git a/src/acp/server.ts b/src/acp/server.ts index bda0ccceca..e57fd2d8c0 100644 --- a/src/acp/server.ts +++ b/src/acp/server.ts @@ -113,7 +113,7 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise; const stream = ndJsonStream(input, output); - new AgentSideConnection((conn: AgentSideConnection) => { + const _connection = new AgentSideConnection((conn: AgentSideConnection) => { agent = new AcpGatewayAgent(conn, gateway, opts); agent.start(); return agent; diff --git a/src/acp/translator.stop-reason.test.ts b/src/acp/translator.stop-reason.test.ts index fd09c7372a..39c3664504 100644 --- a/src/acp/translator.stop-reason.test.ts +++ b/src/acp/translator.stop-reason.test.ts @@ -330,7 +330,10 @@ describe("acp translator stop reason mapping", () => { await Promise.resolve(); agent.handleGatewayDisconnect("1006: first disconnect"); agent.handleGatewayReconnect(); - for (let attempt = 0; attempt < 5 && !resolveAgentWait; attempt += 1) { + for (let attempt = 0; attempt < 5; attempt += 1) { + if (resolveAgentWait) { + break; + } await Promise.resolve(); } expect(resolveAgentWait).toBeDefined(); diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index 642fe2877f..b43e763ac1 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -539,10 +539,10 @@ describe("spawnAcpDirect", () => { expect(accepted.childSessionKey).toMatch(/^agent:codex:acp:/); expect(accepted.runId).toBe("run-1"); expect(accepted.mode).toBe("session"); - const patchCalls = hoisted.callGatewayMock.mock.calls + const patchCall = hoisted.callGatewayMock.mock.calls .map((call: unknown[]) => call[0] as { method?: string; params?: Record }) - .filter((request) => request.method === "sessions.patch"); - expect(patchCalls[0]?.params).toMatchObject({ + .find((request) => request.method === "sessions.patch"); + expect(patchCall?.params).toMatchObject({ key: accepted.childSessionKey, spawnedBy: "agent:main:main", }); diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index bac25c7af5..06418ea5a6 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -758,12 +758,13 @@ function shouldFailClosedInterpreterPreflight(command: string): { const rawArgv = splitShellArgs(raw); const argv = rawArgv ? stripPreflightEnvPrefix(rawArgv) : null; let commandIdx = 0; - while ( - argv && - commandIdx < argv.length && - /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(argv[commandIdx]) - ) { - commandIdx += 1; + if (argv) { + while ( + commandIdx < argv.length && + /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(argv[commandIdx] ?? "") + ) { + commandIdx += 1; + } } const directExecutable = normalizeOptionalLowercaseString(argv?.[commandIdx]); const args = argv ? argv.slice(commandIdx + 1) : []; diff --git a/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts b/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts index 33d0887204..ab587fd38b 100644 --- a/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts +++ b/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts @@ -25,8 +25,7 @@ function buildLiveAnthropicModel(): { const modelId = (process.env.OPENCLAW_LIVE_ANTHROPIC_CACHE_MODEL || "claude-sonnet-4-6") .split(/[/:]/) - .filter(Boolean) - .pop() || "claude-sonnet-4-6"; + .findLast(Boolean) || "claude-sonnet-4-6"; return { apiKey, model: { diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 840ac3d18d..c875ca77cb 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -759,7 +759,7 @@ export async function runEmbeddedPiAgent( incompleteTurnText, }); if (resolveReplayInvalidForAttempt(null)) { - accumulatedReplayState = { ...accumulatedReplayState, replayInvalid: true }; + accumulatedReplayState.replayInvalid = true; } accumulatedReplayState = observeReplayMetadata( accumulatedReplayState, diff --git a/src/agents/provider-request-config.ts b/src/agents/provider-request-config.ts index d155ebdc30..b2f4bf467b 100644 --- a/src/agents/provider-request-config.ts +++ b/src/agents/provider-request-config.ts @@ -323,27 +323,27 @@ export function sanitizeConfiguredModelProviderRequest( export function mergeProviderRequestOverrides( ...overrides: Array ): ProviderRequestTransportOverrides | undefined { - let merged: ProviderRequestTransportOverrides | undefined; + const merged: ProviderRequestTransportOverrides = {}; + let hasMerged = false; for (const current of overrides) { if (!current) { continue; } - merged = { - ...merged, - ...(current.headers - ? { - headers: { - ...merged?.headers, - ...current.headers, - }, - } - : {}), - ...(current.auth ? { auth: current.auth } : {}), - ...(current.proxy ? { proxy: current.proxy } : {}), - ...(current.tls ? { tls: current.tls } : {}), - }; + hasMerged = true; + if (current.headers) { + merged.headers = Object.assign({}, merged.headers, current.headers); + } + if (current.auth) { + merged.auth = current.auth; + } + if (current.proxy) { + merged.proxy = current.proxy; + } + if (current.tls) { + merged.tls = current.tls; + } } - return merged; + return hasMerged ? merged : undefined; } export function mergeModelProviderRequestOverrides( @@ -354,10 +354,8 @@ export function mergeModelProviderRequestOverrides( ); for (const current of overrides) { if (current?.allowPrivateNetwork !== undefined) { - merged = { - ...merged, - allowPrivateNetwork: current.allowPrivateNetwork, - }; + merged ??= {}; + merged.allowPrivateNetwork = current.allowPrivateNetwork; } } return merged; diff --git a/src/agents/tool-images.ts b/src/agents/tool-images.ts index e2019570a3..9937cb0360 100644 --- a/src/agents/tool-images.ts +++ b/src/agents/tool-images.ts @@ -95,14 +95,14 @@ function fileNameFromPathLike(pathLike: string): string | undefined { try { const url = new URL(value); - const candidate = url.pathname.split("/").filter(Boolean).at(-1); + const candidate = url.pathname.split("/").findLast(Boolean); return candidate && candidate.length > 0 ? candidate : undefined; } catch { // Not a URL; continue with path-like parsing. } const normalized = value.replaceAll("\\", "/"); - const candidate = normalized.split("/").filter(Boolean).at(-1); + const candidate = normalized.split("/").findLast(Boolean); return candidate && candidate.length > 0 ? candidate : undefined; } diff --git a/src/auto-reply/reply/commands-context-report.ts b/src/auto-reply/reply/commands-context-report.ts index a20208ea1f..d8f7e6a931 100644 --- a/src/auto-reply/reply/commands-context-report.ts +++ b/src/auto-reply/reply/commands-context-report.ts @@ -77,7 +77,7 @@ async function resolveContextReport( export async function buildContextReply(params: HandleCommandsParams): Promise { const args = parseContextArgs(params.command.commandBodyNormalized); - const sub = normalizeLowercaseStringOrEmpty(args.split(/\s+/).filter(Boolean)[0]); + const sub = normalizeLowercaseStringOrEmpty(args.split(/\s+/).find(Boolean)); if (!sub || sub === "help") { return { diff --git a/src/auto-reply/reply/exec/directive.ts b/src/auto-reply/reply/exec/directive.ts index 14d741432e..de7d3bffae 100644 --- a/src/auto-reply/reply/exec/directive.ts +++ b/src/auto-reply/reply/exec/directive.ts @@ -85,7 +85,10 @@ function parseExecDirectiveArgs(raw: string): Omit< return { key, value }; }; - while (i < len) { + for (;;) { + if (i >= len) { + break; + } const token = takeToken(); if (!token) { break; diff --git a/src/auto-reply/reply/queue/directive.ts b/src/auto-reply/reply/queue/directive.ts index 31a6a99aef..0579dd1a00 100644 --- a/src/auto-reply/reply/queue/directive.ts +++ b/src/auto-reply/reply/queue/directive.ts @@ -65,7 +65,10 @@ function parseQueueDirectiveArgs(raw: string): { i = res.nextIndex; return res.token; }; - while (i < len) { + for (;;) { + if (i >= len) { + break; + } const token = takeToken(); if (!token) { break; diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index 970925b253..9c78c3ba3d 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -291,9 +291,12 @@ describe("initSessionState thread forking", () => { if (!newSessionFile) { throw new Error("Missing session file for forked thread"); } - const [headerLine] = (await fs.readFile(newSessionFile, "utf-8")) + const headerLine = (await fs.readFile(newSessionFile, "utf-8")) .split(/\r?\n/) - .filter((line) => line.trim().length > 0); + .find((line) => line.trim().length > 0); + if (!headerLine) { + throw new Error("Missing session header"); + } const parsedHeader = JSON.parse(headerLine) as { parentSession?: string; }; @@ -522,7 +525,10 @@ describe("initSessionState thread forking", () => { expect(result.sessionEntry.forkedFromParent).toBe(true); expect(result.sessionEntry.sessionFile).toBeTruthy(); const forkedContent = await fs.readFile(result.sessionEntry.sessionFile ?? "", "utf-8"); - const [headerLine] = forkedContent.split(/\r?\n/).filter((line) => line.trim().length > 0); + const headerLine = forkedContent.split(/\r?\n/).find((line) => line.trim().length > 0); + if (!headerLine) { + throw new Error("Missing session header"); + } const parsedHeader = JSON.parse(headerLine) as { parentSession?: string }; const expectedParentSession = await fs.realpath(parentSessionFile); const actualParentSession = parsedHeader.parentSession diff --git a/src/cli/command-path-policy.ts b/src/cli/command-path-policy.ts index cc73091bc2..71ff218515 100644 --- a/src/cli/command-path-policy.ts +++ b/src/cli/command-path-policy.ts @@ -19,16 +19,10 @@ export function resolveCliCommandPathPolicy(commandPath: string[]): CliCommandPa if (!matchesCommandPath(commandPath, entry.commandPath, { exact: entry.exact })) { continue; } - resolvedPolicy = { - ...resolvedPolicy, - ...entry.policy, - }; + Object.assign(resolvedPolicy, entry.policy); } if (isGatewayConfigBypassCommandPath(commandPath)) { - resolvedPolicy = { - ...resolvedPolicy, - bypassConfigGuard: true, - }; + resolvedPolicy.bypassConfigGuard = true; } return resolvedPolicy; } diff --git a/src/commands/configure.channels.ts b/src/commands/configure.channels.ts index 664b7cd30e..8dd95b1036 100644 --- a/src/commands/configure.channels.ts +++ b/src/commands/configure.channels.ts @@ -67,12 +67,11 @@ export async function removeChannelConfigWizard( const nextChannels: Record = { ...next.channels }; delete nextChannels[channel]; - next = { - ...next, - channels: Object.keys(nextChannels).length - ? (nextChannels as OpenClawConfig["channels"]) - : undefined, - }; + if (Object.keys(nextChannels).length) { + next.channels = nextChannels as OpenClawConfig["channels"]; + } else { + delete next.channels; + } note( [`${label} removed from config.`, "Note: credentials/sessions on disk are unchanged."].join( diff --git a/src/commands/docs.ts b/src/commands/docs.ts index ba16670c3c..88bad3185f 100644 --- a/src/commands/docs.ts +++ b/src/commands/docs.ts @@ -75,11 +75,12 @@ function normalizeSnippet(raw: string | undefined, fallback: string): string { } function firstParagraph(text: string): string { - const parts = text - .split(/\n\s*\n/) - .map((chunk) => chunk.trim()) - .filter(Boolean); - return parts[0] ?? ""; + return ( + text + .split(/\n\s*\n/) + .map((chunk) => chunk.trim()) + .find(Boolean) ?? "" + ); } function parseSearchOutput(raw: string): DocResult[] { diff --git a/src/commands/onboard-custom.ts b/src/commands/onboard-custom.ts index a64be0fc4b..c0bb1bb4fa 100644 --- a/src/commands/onboard-custom.ts +++ b/src/commands/onboard-custom.ts @@ -446,12 +446,7 @@ async function promptBaseUrlAndKey(params: { initialValue: params.initialBaseUrl ?? OLLAMA_DEFAULT_BASE_URL, placeholder: "https://api.example.com/v1", validate: (val) => { - try { - new URL(val); - return undefined; - } catch { - return "Please enter a valid URL (e.g. http://...)"; - } + return URL.canParse(val) ? undefined : "Please enter a valid URL (e.g. http://...)"; }, }); const baseUrl = baseUrlInput.trim(); @@ -608,9 +603,7 @@ export function parseNonInteractiveCustomApiFlags( export function applyCustomApiConfig(params: ApplyCustomApiConfigParams): CustomApiResult { const baseUrl = normalizeOptionalString(params.baseUrl) ?? ""; - try { - new URL(baseUrl); - } catch { + if (!URL.canParse(baseUrl)) { throw new CustomApiError("invalid_base_url", "Custom provider base URL must be a valid URL."); } diff --git a/src/config/io.observe-recovery.test.ts b/src/config/io.observe-recovery.test.ts index cdd39318b6..623390ed22 100644 --- a/src/config/io.observe-recovery.test.ts +++ b/src/config/io.observe-recovery.test.ts @@ -87,8 +87,7 @@ describe("config observe recovery", () => { const lines = (await fsp.readFile(auditPath, "utf-8")).trim().split("\n").filter(Boolean); const observe = lines .map((line) => JSON.parse(line) as Record) - .filter((line) => line.event === "config.observe") - .at(-1); + .findLast((line) => line.event === "config.observe"); expect(observe?.restoredFromBackup).toBe(true); expect(observe?.suspicious).toEqual( expect.arrayContaining(["gateway-mode-missing-vs-last-good", "update-channel-only-root"]), @@ -154,8 +153,7 @@ describe("config observe recovery", () => { const lines = (await fsp.readFile(auditPath, "utf-8")).trim().split("\n").filter(Boolean); const observe = lines .map((line) => JSON.parse(line) as Record) - .filter((line) => line.event === "config.observe") - .at(-1); + .findLast((line) => line.event === "config.observe"); expect(observe?.backupHash).toBeTypeOf("string"); expect(observe?.lastKnownGoodIno ?? null).toBeNull(); }); diff --git a/src/gateway/mcp-http.schema.ts b/src/gateway/mcp-http.schema.ts index 4997619158..232a3bedc7 100644 --- a/src/gateway/mcp-http.schema.ts +++ b/src/gateway/mcp-http.schema.ts @@ -53,11 +53,7 @@ function flattenUnionSchema(raw: Record): Record 0 - ? [ - ...requiredSets.reduce( - (left, right) => new Set([...left].filter((key) => right.has(key))), - ), - ] + ? [...(requiredSets[0] ?? [])].filter((key) => requiredSets.every((set) => set.has(key))) : []; const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = raw; return { ...rest, type: "object", properties: mergedProps, required }; diff --git a/src/gateway/server.node-invoke-approval-bypass.test.ts b/src/gateway/server.node-invoke-approval-bypass.test.ts index a308adaac1..2a98815814 100644 --- a/src/gateway/server.node-invoke-approval-bypass.test.ts +++ b/src/gateway/server.node-invoke-approval-bypass.test.ts @@ -389,7 +389,10 @@ describe("node.invoke approval bypass", () => { idempotencyKey: crypto.randomUUID(), }); expect(invoke.ok).toBe(true); - for (let i = 0; i < 100 && !lastInvokeParams; i += 1) { + for (let i = 0; i < 100; i += 1) { + if (lastInvokeParams) { + break; + } await sleep(50); } expect(lastInvokeParams).toBeTruthy(); diff --git a/src/infra/bonjour-discovery.test.ts b/src/infra/bonjour-discovery.test.ts index c58a4eeed3..1df5d7fd42 100644 --- a/src/infra/bonjour-discovery.test.ts +++ b/src/infra/bonjour-discovery.test.ts @@ -306,6 +306,6 @@ describe("bonjour-discovery", () => { }); expect(calls.filter((c) => c[1] === "-B")).toHaveLength(1); - expect(calls.filter((c) => c[1] === "-B")[0]?.[3]).toBe("local."); + expect(calls.find((c) => c[1] === "-B")?.[3]).toBe("local."); }); }); diff --git a/src/infra/fs-pinned-write-helper.ts b/src/infra/fs-pinned-write-helper.ts index 98a13aa925..c7a91510b9 100644 --- a/src/infra/fs-pinned-write-helper.ts +++ b/src/infra/fs-pinned-write-helper.ts @@ -138,8 +138,7 @@ function parsePinnedIdentity(stdout: string): FileIdentityStat { .trim() .split(/\r?\n/) .map((value) => value.trim()) - .filter(Boolean) - .at(-1); + .findLast(Boolean); if (!line) { throw new Error("Pinned write helper returned no identity"); } diff --git a/src/infra/outbound/delivery-queue.reconnect-drain.test.ts b/src/infra/outbound/delivery-queue.reconnect-drain.test.ts index ebef6f8f12..2113583bb2 100644 --- a/src/infra/outbound/delivery-queue.reconnect-drain.test.ts +++ b/src/infra/outbound/delivery-queue.reconnect-drain.test.ts @@ -225,8 +225,11 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => { // Fail it so it matches the "no listener" filter const pending = fs .readdirSync(path.join(tmpDir, "delivery-queue")) - .filter((f) => f.endsWith(".json")); - const entryPath = path.join(tmpDir, "delivery-queue", pending[0]); + .find((f) => f.endsWith(".json")); + if (!pending) { + throw new Error("Missing pending delivery entry"); + } + const entryPath = path.join(tmpDir, "delivery-queue", pending); const entry = JSON.parse(fs.readFileSync(entryPath, "utf-8")); entry.lastError = "No active WhatsApp Web listener"; entry.retryCount = 1; diff --git a/src/logging/redact.ts b/src/logging/redact.ts index 6de5d92e77..6e21853e15 100644 --- a/src/logging/redact.ts +++ b/src/logging/redact.ts @@ -98,8 +98,7 @@ function redactMatch(match: string, groups: string[]): string { if (match.includes("PRIVATE KEY-----")) { return redactPemBlock(match); } - const token = - groups.filter((value) => typeof value === "string" && value.length > 0).at(-1) ?? match; + const token = groups.findLast((value) => typeof value === "string" && value.length > 0) ?? match; const masked = maskToken(token); if (token === match) { return masked; diff --git a/src/logging/timestamps.ts b/src/logging/timestamps.ts index 7a3abec2aa..c0937cb79f 100644 --- a/src/logging/timestamps.ts +++ b/src/logging/timestamps.ts @@ -1,6 +1,6 @@ export function isValidTimeZone(tz: string): boolean { try { - new Intl.DateTimeFormat("en", { timeZone: tz }); + new Intl.DateTimeFormat("en", { timeZone: tz }).format(); return true; } catch { return false; diff --git a/src/plugins/schema-validator.ts b/src/plugins/schema-validator.ts index 4f84ad6a57..e7ed9c6d26 100644 --- a/src/plugins/schema-validator.ts +++ b/src/plugins/schema-validator.ts @@ -37,14 +37,9 @@ function getAjv(mode: "default" | "defaults"): AjvLike { instance.addFormat("uri", { type: "string", validate: (value: string) => { - try { - // Accept absolute URIs so generated config schemas can keep JSON Schema - // `format: "uri"` without noisy AJV warnings during validation/build. - new URL(value); - return true; - } catch { - return false; - } + // Accept absolute URIs so generated config schemas can keep JSON Schema + // `format: "uri"` without noisy AJV warnings during validation/build. + return URL.canParse(value); }, }); ajvSingletons.set(mode, instance); diff --git a/src/plugins/slots.ts b/src/plugins/slots.ts index 5aea34eedd..24661fe5c7 100644 --- a/src/plugins/slots.ts +++ b/src/plugins/slots.ts @@ -85,14 +85,14 @@ export function applyExclusiveSlotSelection(params: { } const warnings: string[] = []; - let pluginsConfig = params.config.plugins ?? {}; + const pluginsConfig = params.config.plugins ?? {}; let anyChanged = false; - let entries = { ...pluginsConfig.entries }; - let slots = { ...pluginsConfig.slots }; + const entries = { ...pluginsConfig.entries }; + const slots = { ...pluginsConfig.slots }; for (const slotKey of slotKeys) { const prevSlot = slots[slotKey]; - slots = { ...slots, [slotKey]: params.selectedId }; + slots[slotKey] = params.selectedId; const inferredPrevSlot = prevSlot ?? defaultSlotIdForKey(slotKey); if (inferredPrevSlot && inferredPrevSlot !== params.selectedId) { @@ -123,10 +123,7 @@ export function applyExclusiveSlotSelection(params: { } const entry = entries[plugin.id]; if (!entry || entry.enabled !== false) { - entries = { - ...entries, - [plugin.id]: { ...entry, enabled: false }, - }; + entries[plugin.id] = { ...entry, enabled: false }; disabledIds.push(plugin.id); } } diff --git a/ui/src/ui/views/config-form.shared.ts b/ui/src/ui/views/config-form.shared.ts index e3e6547791..04fc651e5e 100644 --- a/ui/src/ui/views/config-form.shared.ts +++ b/ui/src/ui/views/config-form.shared.ts @@ -24,8 +24,7 @@ export function schemaType(schema: JsonSchema): string | undefined { return undefined; } if (Array.isArray(schema.type)) { - const filtered = schema.type.filter((t) => t !== "null"); - return filtered[0] ?? schema.type[0]; + return schema.type.find((t) => t !== "null") ?? schema.type[0]; } return schema.type; }