diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index c7f60f7f625e..a71df310b907 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -de87f2acba61514406fe343f0ee2896a101435203c4ef35af8db707b32d109f6 plugin-sdk-api-baseline.json -eb72aa8eb79d0729f12ea7715b0ff098677e06e677050009ba1add89cc357f27 plugin-sdk-api-baseline.jsonl +e9eede54e4d95e441f0d8dace534f3c94cbb4cc68523a5cae72914bca1683c42 plugin-sdk-api-baseline.json +4eff68c554e5c4740c528ea69be16e1f7079865bb363a4c5bc7a436d3c4d94c8 plugin-sdk-api-baseline.jsonl diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index c9cd0744ca70..b9708fc9d5d4 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -324,6 +324,7 @@ SDK. | Transport readiness waits | `openclaw/plugin-sdk/transport-ready-runtime` | | Secure token helpers | `openclaw/plugin-sdk/secure-random-runtime` | | Bounded async task concurrency | `openclaw/plugin-sdk/concurrency-runtime` | + | Required-value assertions for provable invariants | `openclaw/plugin-sdk/expect-runtime` | | Numeric coercion | `openclaw/plugin-sdk/number-runtime` | | Process-local async lock | `openclaw/plugin-sdk/async-lock-runtime` | | File locks | `openclaw/plugin-sdk/file-lock` | diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index da4ddbdecdad..1381d3460971 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -319,6 +319,7 @@ usage endpoint failed or returned no usable usage data. | `plugin-sdk/delivery-queue-runtime` | Outbound pending-delivery drain helper | | `plugin-sdk/file-access-runtime` | Safe local-file and media-source path helpers | | `plugin-sdk/heartbeat-runtime` | Heartbeat wake, event, and visibility helpers | + | `plugin-sdk/expect-runtime` | Required-value assertion helper for provable runtime invariants | | `plugin-sdk/number-runtime` | Numeric coercion helper | | `plugin-sdk/secure-random-runtime` | Secure token/UUID helpers | | `plugin-sdk/system-event-runtime` | System event queue helpers | diff --git a/extensions/acpx/src/codex-trust-config.ts b/extensions/acpx/src/codex-trust-config.ts index c6913a7f2778..33099d55e9e3 100644 --- a/extensions/acpx/src/codex-trust-config.ts +++ b/extensions/acpx/src/codex-trust-config.ts @@ -142,12 +142,14 @@ export function extractTrustedCodexProjectPaths(configToml: string): string[] { const assignment = /^(?"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*(?.+)$/.exec(line); - if (!assignment?.groups) { + const rawKey = assignment?.groups?.key; + const rawValue = assignment?.groups?.value; + if (!rawKey || rawValue === undefined) { continue; } - const key = parseTomlString(assignment.groups.key) ?? assignment.groups.key; - const value = assignment.groups.value.trim(); + const key = parseTomlString(rawKey) ?? rawKey; + const value = rawValue.trim(); if (inProjectsTable && /^\{.*\}$/.test(value)) { if (/\btrust_level\s*=\s*["']trusted["']/.test(value) && key) { trusted.add(key); diff --git a/extensions/acpx/src/process-reaper.ts b/extensions/acpx/src/process-reaper.ts index 9a9a499e5637..1f834193364d 100644 --- a/extensions/acpx/src/process-reaper.ts +++ b/extensions/acpx/src/process-reaper.ts @@ -198,13 +198,16 @@ function parseProcessList(stdout: string): AcpxProcessInfo[] { const processes: AcpxProcessInfo[] = []; for (const line of stdout.split(/\r?\n/)) { const match = /^\s*(?\d+)\s+(?\d+)\s+(?.+?)\s*$/.exec(line); - if (!match?.groups) { + const pid = match?.groups?.pid; + const ppid = match?.groups?.ppid; + const command = match?.groups?.command; + if (!pid || !ppid || !command) { continue; } processes.push({ - pid: Number.parseInt(match.groups.pid, 10), - ppid: Number.parseInt(match.groups.ppid, 10), - command: match.groups.command, + pid: Number.parseInt(pid, 10), + ppid: Number.parseInt(ppid, 10), + command, }); } return processes; diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index dfec279a4c92..7b4c8b776ced 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -399,11 +399,16 @@ function isEnvAssignment(value: string): boolean { } function unwrapEnvCommand(parts: string[]): string[] { - if (!parts.length || basename(parts[0]) !== "env") { + const command = parts.at(0); + if (!command || basename(command) !== "env") { return parts; } let index = 1; - while (index < parts.length && isEnvAssignment(parts[index])) { + while (true) { + const part = parts.at(index); + if (!part || !isEnvAssignment(part)) { + break; + } index += 1; } return parts.slice(index); diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index 176c42d816ca..bec0e2ace59a 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -337,8 +337,7 @@ function injectBedrockCachePoints( // Bedrock Converse uses lowercase roles ("user" / "assistant"). const messages = payload.messages as BedrockMessage[] | undefined; if (Array.isArray(messages) && messages.length > 0) { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; + for (const msg of messages.toReversed()) { if (msg.role === "user" && Array.isArray(msg.content)) { if (!hasCachePoint(msg.content)) { msg.content.push(point); diff --git a/extensions/amazon-bedrock/stream.runtime.ts b/extensions/amazon-bedrock/stream.runtime.ts index 65ebb47ca4f6..26a3b84e149b 100644 --- a/extensions/amazon-bedrock/stream.runtime.ts +++ b/extensions/amazon-bedrock/stream.runtime.ts @@ -26,6 +26,7 @@ import { } from "@aws-sdk/client-bedrock-runtime"; import { NodeHttpHandler } from "@smithy/node-http-handler"; import type { DocumentType } from "@smithy/types"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { adjustMaxTokensForThinking, AssistantMessageEventStream, @@ -50,7 +51,6 @@ import { type ThinkingLevel, type Tool, type ToolCall, - type ToolResultMessage, } from "openclaw/plugin-sdk/llm"; import { resolveClaudeFable5ModelIdentity, @@ -504,7 +504,7 @@ function handleContentBlockDelta( const newBlock: Block = { type: "text", text: "", index: contentBlockIndex }; output.content.push(newBlock); index = blocks.length - 1; - block = blocks[index]; + block = newBlock; stream.push({ type: "text_start", contentIndex: index, partial: output }); } if (block.type === "text") { @@ -794,7 +794,7 @@ function convertMessages( const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId); for (let i = 0; i < transformedMessages.length; i++) { - const m = transformedMessages[i]; + const m = expectDefined(transformedMessages[i], "message conversion index is in bounds"); switch (m.role) { case "user": { @@ -917,8 +917,11 @@ function convertMessages( // Look ahead for consecutive toolResult messages let j = i + 1; - while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") { - const nextMsg = transformedMessages[j] as ToolResultMessage; + while (true) { + const nextMsg = transformedMessages.at(j); + if (nextMsg?.role !== "toolResult") { + break; + } toolResults.push({ toolResult: { toolUseId: nextMsg.toolCallId, @@ -949,7 +952,7 @@ function convertMessages( // Add cache point to the last user message for supported Claude models when caching is enabled if (cacheRetention !== "none" && supportsPromptCaching(model) && result.length > 0) { - const lastMessage = result[result.length - 1]; + const lastMessage = expectDefined(result.at(-1), "non-empty converted message list"); if (lastMessage.role === ConversationRole.USER && lastMessage.content) { lastMessage.content.push({ cachePoint: { diff --git a/extensions/anthropic/cli-shared.ts b/extensions/anthropic/cli-shared.ts index 20e1946b7d64..23fa4f98e069 100644 --- a/extensions/anthropic/cli-shared.ts +++ b/extensions/anthropic/cli-shared.ts @@ -143,13 +143,17 @@ export function normalizeClaudePermissionArgs( } const normalized: string[] = []; let hasPermissionMode = false; - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; + let skipNext = false; + for (const [index, arg] of args.entries()) { + if (skipNext) { + skipNext = false; + continue; + } if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) { continue; } if (arg === CLAUDE_PERMISSION_MODE_ARG) { - const maybeValue = args[i + 1]; + const maybeValue = args.at(index + 1); if ( typeof maybeValue === "string" && maybeValue.trim().length > 0 && @@ -160,7 +164,7 @@ export function normalizeClaudePermissionArgs( normalized.push(arg); normalized.push(maybeValue); } - i += 1; + skipNext = true; } continue; } @@ -189,10 +193,14 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u } const normalized: string[] = []; let hasSettingSources = false; - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; + let skipNext = false; + for (const [index, arg] of args.entries()) { + if (skipNext) { + skipNext = false; + continue; + } if (arg === CLAUDE_SETTING_SOURCES_ARG) { - const maybeValue = args[i + 1]; + const maybeValue = args.at(index + 1); if ( typeof maybeValue === "string" && maybeValue.trim().length > 0 && @@ -200,7 +208,7 @@ export function normalizeClaudeSettingSourcesArgs(args?: string[]): string[] | u ) { hasSettingSources = true; normalized.push(arg, CLAUDE_SAFE_SETTING_SOURCES); - i += 1; + skipNext = true; } continue; } diff --git a/extensions/bonjour/src/advertiser.ts b/extensions/bonjour/src/advertiser.ts index 7aca1df0c8f2..136b495da182 100644 --- a/extensions/bonjour/src/advertiser.ts +++ b/extensions/bonjour/src/advertiser.ts @@ -431,13 +431,11 @@ export async function startGatewayBonjourAdvertiser( const hostnameRaw = process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || resolveSystemMdnsHostname() || "openclaw"; - const hostname = truncateToDnsLabel( - hostnameRaw - .replace(/\.local$/i, "") - .split(".")[0] - .trim() || "openclaw", - "openclaw", - ); + const hostnameWithoutLocal = hostnameRaw.replace(/\.local$/i, ""); + const dotIndex = hostnameWithoutLocal.indexOf("."); + const labelEnd = dotIndex === -1 ? hostnameWithoutLocal.length : dotIndex; + const hostnameLabel = hostnameWithoutLocal.slice(0, labelEnd).trim() || "openclaw"; + const hostname = truncateToDnsLabel(hostnameLabel, "openclaw"); const instanceName = typeof opts.instanceName === "string" && opts.instanceName.trim() ? opts.instanceName.trim() diff --git a/extensions/brave/src/brave-web-search-provider.shared.ts b/extensions/brave/src/brave-web-search-provider.shared.ts index d73a70417f90..37dfdbb1e03a 100644 --- a/extensions/brave/src/brave-web-search-provider.shared.ts +++ b/extensions/brave/src/brave-web-search-provider.shared.ts @@ -167,6 +167,9 @@ function normalizeBraveUiLang(value: string | undefined): string | undefined { return undefined; } const [, language, region] = match; + if (!language || !region) { + return undefined; + } return `${normalizeLowercaseStringOrEmpty(language)}-${region.toUpperCase()}`; } diff --git a/extensions/browser/src/browser/cdp.ts b/extensions/browser/src/browser/cdp.ts index 5c86c2060ad2..27e6dd31daca 100644 --- a/extensions/browser/src/browser/cdp.ts +++ b/extensions/browser/src/browser/cdp.ts @@ -4,6 +4,7 @@ * Provides screenshots, target creation, JavaScript evaluation, ARIA/role * snapshots, DOM text, and selector lookup on top of the CDP socket helpers. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { SsrFPolicy } from "../infra/net/ssrf.js"; @@ -517,7 +518,7 @@ function buildRoleTree(nodes: RawAXNode[]): { tree: RoleTreeNode[]; roots: numbe continue; } tree[index]?.children.push(childIndex); - tree[childIndex].parent = index; + expectDefined(tree[childIndex], "CDP child node index").parent = index; childIndexes.add(childIndex); } } @@ -529,7 +530,7 @@ function buildRoleTree(nodes: RawAXNode[]): { tree: RoleTreeNode[]; roots: numbe if (!current) { break; } - tree[current.index].depth = current.depth; + expectDefined(tree[current.index], "CDP traversal node index").depth = current.depth; for (const child of (tree[current.index]?.children ?? []).toReversed()) { stack.push({ index: child, depth: current.depth + 1 }); } @@ -857,7 +858,7 @@ async function buildCdpRoleSnapshot(params: { if (node.backendDOMNodeId && iframeFrameIds.has(node.backendDOMNodeId)) { node.frameId = iframeFrameIds.get(node.backendDOMNodeId); if (node.ref && refs[node.ref]) { - refs[node.ref].frameId = node.frameId; + expectDefined(refs[node.ref], "owned CDP role reference").frameId = node.frameId; } } } diff --git a/extensions/browser/src/browser/chrome.profile-decoration.ts b/extensions/browser/src/browser/chrome.profile-decoration.ts index 546ab2cc513b..c1da5846feb1 100644 --- a/extensions/browser/src/browser/chrome.profile-decoration.ts +++ b/extensions/browser/src/browser/chrome.profile-decoration.ts @@ -55,7 +55,10 @@ function setDeep(obj: Record, keys: string[], value: unknown) { } node = node[key] as Record; } - node[keys[keys.length - 1]] = value; + const lastKey = keys.at(-1); + if (lastKey !== undefined) { + node[lastKey] = value; + } } function parseHexRgbToSignedArgbInt(hex: string): number | null { diff --git a/extensions/browser/src/browser/profiles.ts b/extensions/browser/src/browser/profiles.ts index 9807049da094..1710162a9ce9 100644 --- a/extensions/browser/src/browser/profiles.ts +++ b/extensions/browser/src/browser/profiles.ts @@ -1,10 +1,11 @@ +import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config"; /** * Browser profile allocation helpers. * * Validates profile names and allocates CDP ports/colors for newly persisted * browser profiles. */ -import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; /** * CDP port allocation for browser profiles. @@ -111,7 +112,7 @@ export function allocateColor(usedColors: Set): string { } // All colors used, cycle based on count const index = usedColors.size % PROFILE_COLORS.length; - return PROFILE_COLORS[index] ?? PROFILE_COLORS[0]; + return expectDefined(PROFILE_COLORS[index], "cycled browser color palette index"); } /** Extract currently used profile colors from profile config. */ diff --git a/extensions/browser/src/browser/pw-role-snapshot.ts b/extensions/browser/src/browser/pw-role-snapshot.ts index ddb870873c27..cb3fc704842b 100644 --- a/extensions/browser/src/browser/pw-role-snapshot.ts +++ b/extensions/browser/src/browser/pw-role-snapshot.ts @@ -4,6 +4,7 @@ * Converts ARIA or AI snapshots into compact role/name text with stable refs * and duplicate disambiguation for agent actions. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { CONTENT_ROLES, INTERACTIVE_ROLES, STRUCTURAL_ROLES } from "./snapshot-roles.js"; @@ -107,7 +108,8 @@ export function finalizeRoleSnapshot(params: { function getIndentLevel(line: string): number { const match = line.match(/^(\s*)/); - return match ? Math.floor(match[1].length / 2) : 0; + const indent = match?.[1]; + return indent === undefined ? 0 : Math.floor(indent.length / 2); } function matchInteractiveSnapshotLine( @@ -125,6 +127,9 @@ function matchInteractiveSnapshotLine( const roleRaw = match[2]; const name = match[3]; const suffix = match[4]; + if (roleRaw === undefined || suffix === undefined) { + return null; + } if (roleRaw.startsWith("/")) { return null; } @@ -201,13 +206,20 @@ function compactTree(tree: string) { } current.entry.keep ||= current.entry.hasRef; if (current.entry.hasRef && stack.length > 0) { - stack[stack.length - 1].entry.hasRef = true; + const parent = stack.at(-1); + if (parent !== undefined) { + parent.entry.hasRef = true; + } } }; for (const line of lines) { const indent = getIndentLevel(line); - while (stack.length > 0 && stack[stack.length - 1].indent >= indent) { + while (stack.length > 0) { + const lastEntry = expectDefined(stack.at(-1), "non-empty role snapshot stack"); + if (lastEntry.indent < indent) { + break; + } finishEntry(); } const entry = { @@ -246,7 +258,13 @@ function processLine( return options.interactive ? null : line; } - const [, prefix, roleRaw, name, suffix] = match; + const prefix = match[1]; + const roleRaw = match[2]; + const name = match[3]; + const suffix = match[4]; + if (prefix === undefined || roleRaw === undefined || suffix === undefined) { + return options.interactive ? null : line; + } if (roleRaw.startsWith("/")) { return options.interactive ? null : line; } @@ -414,10 +432,10 @@ export function buildRoleSnapshotFromAriaSnapshot( function parseAiSnapshotRef(suffix: string): string | null { const eMatch = suffix.match(/\[ref=(e\d+)\]/i); if (eMatch) { - return eMatch[1]; + return eMatch[1] ?? null; } const numMatch = suffix.match(/\[ref=(\d{1,9})\]/); - return numMatch ? numMatch[1] : null; + return numMatch?.[1] ?? null; } /** @@ -466,6 +484,10 @@ export function buildRoleSnapshotFromAiSnapshot( const roleRaw = match[2]; const name = match[3]; const suffix = match[4]; + if (roleRaw === undefined || suffix === undefined) { + out.push(line); + continue; + } if (roleRaw.startsWith("/")) { out.push(line); continue; diff --git a/extensions/browser/src/browser/pw-session.ts b/extensions/browser/src/browser/pw-session.ts index 80409c69576f..8d25a0f512f4 100644 --- a/extensions/browser/src/browser/pw-session.ts +++ b/extensions/browser/src/browser/pw-session.ts @@ -4,6 +4,7 @@ * Manages CDP-backed Playwright connections, page lookup, observed dialogs, * console/network/page state, role refs, and safe navigation handling. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { isFutureDateTimestampMs, parseFiniteNumber, @@ -1152,7 +1153,7 @@ function resolvePendingDialogForResponse(params: { throw new Error(`Dialog "${dialogId}" is not pending.`); } if (params.state.pendingDialogs.length === 1) { - return params.state.pendingDialogs[0]; + return expectDefined(params.state.pendingDialogs.at(0), "single pending browser dialog"); } if (params.state.pendingDialogs.length > 1) { throw new Error("Multiple dialogs are pending; pass dialogId."); @@ -1488,7 +1489,7 @@ async function getPageForTargetIdOnce(opts: { } throw new Error("No pages available in the connected browser."); } - const first = accessible[0]; + const first = expectDefined(accessible.at(0), "non-empty accessible browser pages"); if (!opts.targetId) { bindRoleRefsTarget(first.page, opts.cdpUrl, first.targetId); return first.page; diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.ts b/extensions/browser/src/browser/pw-tools-core.interactions.ts index 7b6b17bf4e1d..c249f85cb813 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.ts @@ -1483,6 +1483,10 @@ export async function screenshotWithLabelsViaPlaywright(opts: { const inputs: RawAnnotationInput[] = []; let bboxFailures = 0; for (const ref of refKeys) { + const refInfo = opts.refs[ref]; + if (refInfo === undefined) { + continue; + } const box = await refLocator(page, ref) .boundingBox() .catch(() => null); @@ -1492,8 +1496,8 @@ export async function screenshotWithLabelsViaPlaywright(opts: { } inputs.push({ ref, - role: opts.refs[ref].role, - name: opts.refs[ref].name, + role: refInfo.role, + name: refInfo.name, doc: { x: box.x + scroll.x, y: box.y + scroll.y, diff --git a/extensions/browser/src/browser/routes/agent.act.ts b/extensions/browser/src/browser/routes/agent.act.ts index 8b5d64f09c85..cdd2b2d18add 100644 --- a/extensions/browser/src/browser/routes/agent.act.ts +++ b/extensions/browser/src/browser/routes/agent.act.ts @@ -1,10 +1,11 @@ +import { setTimeout as sleep } from "node:timers/promises"; /** * Browser agent action route registration and existing-session execution. * * Dispatches normalized actions to either Playwright-backed OpenClaw browser * control or Chrome MCP existing-session operations with navigation guards. */ -import { setTimeout as sleep } from "node:timers/promises"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { formatErrorMessage } from "../../infra/errors.js"; import { clickChromeMcpElement, @@ -216,7 +217,9 @@ function buildExistingSessionWaitPredicate(params: { if (checks.length === 0) { return null; } - return checks.length === 1 ? checks[0] : checks.map((check) => `(${check})`).join(" && "); + return checks.length === 1 + ? expectDefined(checks.at(0), "single existing-session condition") + : checks.map((check) => `(${check})`).join(" && "); } async function waitForExistingSessionCondition( diff --git a/extensions/browser/src/browser/server-context.remote-tab-ops.harness.ts b/extensions/browser/src/browser/server-context.remote-tab-ops.harness.ts index 8cc5fee9e01f..08d1c6d01768 100644 --- a/extensions/browser/src/browser/server-context.remote-tab-ops.harness.ts +++ b/extensions/browser/src/browser/server-context.remote-tab-ops.harness.ts @@ -73,15 +73,15 @@ function resolveProfileForTest( state: BrowserServerState, profileName: string, ): ResolvedBrowserProfile { - const rawProfile = state.resolved.profiles[profileName] ?? {}; + const rawProfile = state.resolved.profiles[profileName]; const cdpPort = - typeof rawProfile.cdpPort === "number" + typeof rawProfile?.cdpPort === "number" ? rawProfile.cdpPort : profileName === "remote" ? 9222 : state.resolved.cdpPortRangeStart; const cdpUrl = - typeof rawProfile.cdpUrl === "string" + typeof rawProfile?.cdpUrl === "string" ? rawProfile.cdpUrl : `${state.resolved.cdpProtocol}://${state.resolved.cdpHost}:${cdpPort}`; const parsed = new URL(cdpUrl.replace(/^ws/i, "http")); @@ -93,13 +93,13 @@ function resolveProfileForTest( cdpUrl, cdpHost, cdpIsLoopback, - color: rawProfile.color ?? state.resolved.color, - driver: rawProfile.driver === "existing-session" ? "existing-session" : "openclaw", - headless: rawProfile.headless ?? state.resolved.headless, + color: rawProfile?.color ?? state.resolved.color, + driver: rawProfile?.driver === "existing-session" ? "existing-session" : "openclaw", + headless: rawProfile?.headless ?? state.resolved.headless, headlessSource: - typeof rawProfile.headless === "boolean" ? "profile" : state.resolved.headlessSource, - attachOnly: rawProfile.attachOnly ?? state.resolved.attachOnly, - userDataDir: rawProfile.userDataDir, + typeof rawProfile?.headless === "boolean" ? "profile" : state.resolved.headlessSource, + attachOnly: rawProfile?.attachOnly ?? state.resolved.attachOnly, + userDataDir: rawProfile?.userDataDir, }; } diff --git a/extensions/browser/src/browser/system-chrome-cookies.ts b/extensions/browser/src/browser/system-chrome-cookies.ts index f883be215c6a..0742da41a348 100644 --- a/extensions/browser/src/browser/system-chrome-cookies.ts +++ b/extensions/browser/src/browser/system-chrome-cookies.ts @@ -97,10 +97,10 @@ export async function readKeychainSecret( const raw = Buffer.from(stdout); let start = 0; let end = raw.length; - while (start < end && isAsciiWhitespace(raw[start])) { + while (start < end && isAsciiWhitespace(raw.readUInt8(start))) { start += 1; } - while (end > start && isAsciiWhitespace(raw[end - 1])) { + while (end > start && isAsciiWhitespace(raw.readUInt8(end - 1))) { end -= 1; } const secret = Buffer.from(raw.subarray(start, end)); diff --git a/extensions/browser/src/browser/vision.ts b/extensions/browser/src/browser/vision.ts index 4ee86d7c7121..395b8d780ec8 100644 --- a/extensions/browser/src/browser/vision.ts +++ b/extensions/browser/src/browser/vision.ts @@ -118,8 +118,7 @@ export function neutralizeMediaDirectives(text: string): string { } const lines = text.split("\n"); let changed = false; - for (let i = 0; i < lines.length; i += 1) { - const line = lines[i]; + for (const [i, line] of lines.entries()) { const leading = line.length - line.trimStart().length; const rest = line.slice(leading); if (/^MEDIA:/i.test(rest)) { diff --git a/extensions/canvas/src/documents.ts b/extensions/canvas/src/documents.ts index 7fbb48280f9b..2e2438a85ae3 100644 --- a/extensions/canvas/src/documents.ts +++ b/extensions/canvas/src/documents.ts @@ -225,6 +225,9 @@ export function resolveCanvasHttpPathToLocalPath( return null; } const [rawDocumentId, ...entrySegments] = segments; + if (!rawDocumentId) { + return null; + } try { const documentId = normalizeCanvasDocumentId(rawDocumentId); const normalizedEntrypoint = normalizeLogicalPath(entrySegments.join("/")); diff --git a/extensions/canvas/src/host/server.ts b/extensions/canvas/src/host/server.ts index 9e0f73b45b10..a57c876f4362 100644 --- a/extensions/canvas/src/host/server.ts +++ b/extensions/canvas/src/host/server.ts @@ -1,7 +1,6 @@ /** * Canvas host server and static-file/live-reload handler implementation. */ -import * as fsSync from "node:fs"; import fs from "node:fs/promises"; import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { createRequire } from "node:module"; @@ -246,15 +245,7 @@ async function resolveDocumentCspSandbox( } function resolveDefaultCanvasRoot(): string { - const candidates = [path.join(resolveStateDir(), "canvas")]; - const existing = candidates.find((dir) => { - try { - return fsSync.statSync(dir).isDirectory(); - } catch { - return false; - } - }); - return existing ?? candidates[0]; + return path.join(resolveStateDir(), "canvas"); } function resolveDefaultWatchFactory(): ChokidarWatch { diff --git a/extensions/cerebras/models.ts b/extensions/cerebras/models.ts index 58de3addf274..2784304058b8 100644 --- a/extensions/cerebras/models.ts +++ b/extensions/cerebras/models.ts @@ -1,6 +1,7 @@ /** * Cerebras model catalog helpers derived from the plugin manifest. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; import manifest from "./openclaw.plugin.json" with { type: "json" }; @@ -28,5 +29,5 @@ export function buildCerebrasModelDefinition( providerId: "cerebras", catalog: { ...CEREBRAS_MANIFEST_CATALOG, models: [model] }, }); - return providerConfig.models[0]; + return expectDefined(providerConfig.models.at(0), "normalized Cerebras manifest model"); } diff --git a/extensions/codex/provider.ts b/extensions/codex/provider.ts index f3251f870cfa..bcb645ed140c 100644 --- a/extensions/codex/provider.ts +++ b/extensions/codex/provider.ts @@ -2,6 +2,7 @@ * Codex provider plugin and live app-server model catalog discovery. */ import { createSubsystemLogger } from "openclaw/plugin-sdk/core"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime"; import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry"; import { @@ -33,7 +34,9 @@ const DEFAULT_DISCOVERY_TIMEOUT_MS = 2500; const LIVE_DISCOVERY_ENV = "OPENCLAW_CODEX_DISCOVERY_LIVE"; const MODEL_DISCOVERY_PAGE_LIMIT = 100; const CODEX_APP_SERVER_SETUP_METHOD_ID = "app-server"; -const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${FALLBACK_CODEX_MODELS[0].id}`; +const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${ + expectDefined(FALLBACK_CODEX_MODELS[0], "Codex fallback model catalog must not be empty").id +}`; const codexCatalogLog = createSubsystemLogger("codex/catalog"); const CODEX_REASONING_EFFORTS = [ "minimal", diff --git a/extensions/codex/src/app-server/dynamic-tools.ts b/extensions/codex/src/app-server/dynamic-tools.ts index da06a0472db1..784769766869 100644 --- a/extensions/codex/src/app-server/dynamic-tools.ts +++ b/extensions/codex/src/app-server/dynamic-tools.ts @@ -42,6 +42,7 @@ import { wrapToolWithBeforeToolCallHook, } from "openclaw/plugin-sdk/agent-harness-runtime"; import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { ImageContent, TextContent } from "openclaw/plugin-sdk/llm"; import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; import { @@ -387,7 +388,7 @@ function computerFrameImageIdentity( if (images.length !== 1) { return undefined; } - const image = images[0]; + const image = expectDefined(images[0], "single Codex computer frame image"); return createHash("sha256") .update(JSON.stringify([image.mimeType, image.data])) .digest("hex"); @@ -1139,7 +1140,7 @@ function composeAbortSignals(...signals: Array): AbortS return new AbortController().signal; } if (activeSignals.length === 1) { - return activeSignals[0]; + return expectDefined(activeSignals[0], "single active Codex abort signal"); } return AbortSignal.any(activeSignals); } diff --git a/extensions/codex/src/app-server/managed-binary.ts b/extensions/codex/src/app-server/managed-binary.ts index cdb247b4fa75..c441183fb9d9 100644 --- a/extensions/codex/src/app-server/managed-binary.ts +++ b/extensions/codex/src/app-server/managed-binary.ts @@ -7,6 +7,7 @@ import { access } from "node:fs/promises"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { CodexAppServerStartOptions, CodexManagedCommandOrder } from "./config.js"; import { MANAGED_CODEX_APP_SERVER_PACKAGE } from "./version.js"; @@ -57,7 +58,7 @@ export async function resolveManagedCodexAppServerStartOptions( pathExists, platform, }); - const commandPath = commandPaths[0]; + const commandPath = expectDefined(commandPaths[0], "resolved managed Codex command path"); const managedFallbackCommandPaths = commandPaths.slice(1); return { diff --git a/extensions/codex/src/app-server/native-hook-relay.ts b/extensions/codex/src/app-server/native-hook-relay.ts index d4dd63a7cb4b..cf6b55fc823d 100644 --- a/extensions/codex/src/app-server/native-hook-relay.ts +++ b/extensions/codex/src/app-server/native-hook-relay.ts @@ -409,8 +409,10 @@ function sortJsonValue(value: JsonValue): JsonValue { return value.map(sortJsonValue); } const sorted: JsonObject = {}; - for (const key of Object.keys(value).toSorted()) { - sorted[key] = sortJsonValue(value[key]); + for (const [key, entry] of Object.entries(value).toSorted(([left], [right]) => + left.localeCompare(right), + )) { + sorted[key] = sortJsonValue(entry); } return sorted; } diff --git a/extensions/codex/src/app-server/native-subagent-monitor.ts b/extensions/codex/src/app-server/native-subagent-monitor.ts index d92440197a8f..9ebc8d934f5e 100644 --- a/extensions/codex/src/app-server/native-subagent-monitor.ts +++ b/extensions/codex/src/app-server/native-subagent-monitor.ts @@ -1482,8 +1482,7 @@ function lastChildAssistantMessage(childState: ChildState, turnId: string): stri if (!messages) { return undefined; } - for (let index = messages.order.length - 1; index >= 0; index -= 1) { - const itemId = messages.order[index]; + for (const itemId of messages.order.toReversed()) { if (messages.finalMessageIds.has(itemId) && !messages.commentaryIds.has(itemId)) { const text = normalizeOptionalString(messages.texts.get(itemId)); if (text) { diff --git a/extensions/codex/src/app-server/rate-limits.ts b/extensions/codex/src/app-server/rate-limits.ts index 3e63a1fe9d0a..71ec781533c7 100644 --- a/extensions/codex/src/app-server/rate-limits.ts +++ b/extensions/codex/src/app-server/rate-limits.ts @@ -2,6 +2,7 @@ * Parses Codex account rate-limit payloads into user-facing usage summaries, * reset hints, and enriched usage-limit error messages. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { MAX_DATE_TIMESTAMP_MS, resolveExpiresAtMsFromEpochSeconds, @@ -160,7 +161,9 @@ export function summarizeCodexAccountUsage( if (snapshots.length === 0) { return undefined; } - const usageSnapshot = snapshots.find(isCodexLimitSnapshot) ?? snapshots[0]; + const usageSnapshot = + snapshots.find(isCodexLimitSnapshot) ?? + expectDefined(snapshots[0], "displayable Codex rate-limit snapshot"); const blockedSnapshots = snapshots.filter(snapshotHasLimitBlock); const blockingSnapshot = blockedSnapshots.find(isCodexLimitSnapshot) ?? blockedSnapshots[0] ?? undefined; diff --git a/extensions/codex/src/app-server/shared-client.ts b/extensions/codex/src/app-server/shared-client.ts index 39e19d197caa..347ab5aba23b 100644 --- a/extensions/codex/src/app-server/shared-client.ts +++ b/extensions/codex/src/app-server/shared-client.ts @@ -767,8 +767,7 @@ async function startInitializedCodexAppServerClient(params: { const acquireStartedAt = Date.now(); const timeoutMs = params.timeoutMs ?? 0; const startOptionsCandidates = resolveManagedFallbackStartOptions(params.startOptions); - for (let index = 0; index < startOptionsCandidates.length; index += 1) { - const startOptions = startOptionsCandidates[index]; + for (const [index, startOptions] of startOptionsCandidates.entries()) { const runtimeArtifactModule = params.runtimeArtifactMode ? await import("./runtime-artifact.js") : undefined; @@ -905,8 +904,7 @@ function resolveManagedFallbackStartOptions( ): CodexAppServerStartOptions[] { const commands = [startOptions.command, ...(startOptions.managedFallbackCommandPaths ?? [])]; const candidates: CodexAppServerStartOptions[] = []; - for (let index = 0; index < commands.length; index += 1) { - const command = commands[index]; + for (const [index, command] of commands.entries()) { const managedFallbackCommandPaths = commands.slice(index + 1); const candidate = { ...startOptions, diff --git a/extensions/codex/src/command-account.ts b/extensions/codex/src/command-account.ts index d74ee48ee11b..ffae4d3788e3 100644 --- a/extensions/codex/src/command-account.ts +++ b/extensions/codex/src/command-account.ts @@ -290,7 +290,7 @@ function resolveLiveAccountProfileId(params: { return ( params.order.find((profileId) => { const credential = params.store.profiles[profileId]; - if (!isChatGptSubscriptionProfile(credential)) { + if (!credential || !isChatGptSubscriptionProfile(credential)) { return false; } const profileEmail = diff --git a/extensions/codex/src/command-handlers.ts b/extensions/codex/src/command-handlers.ts index 82f1d3bec82b..bc872b647c06 100644 --- a/extensions/codex/src/command-handlers.ts +++ b/extensions/codex/src/command-handlers.ts @@ -1,6 +1,7 @@ // Codex plugin module implements command handlers behavior. import crypto from "node:crypto"; import { resolveAgentDir, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { isModelSelectionLocked, MODEL_SELECTION_LOCKED_MESSAGE, @@ -2337,7 +2338,7 @@ function splitArgs(value: string | undefined): string[] { function parseBindArgs(args: string[]): ParsedBindArgs { const parsed: ParsedBindArgs = {}; for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; + const arg = expectDefined(args[index], "current Codex bind argument"); if (arg === "--help" || arg === "-h") { parsed.help = true; continue; @@ -2389,7 +2390,7 @@ function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs { const parsed: ParsedCodexCliSessionsArgs = { filter: "" }; const filter: string[] = []; for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; + const arg = expectDefined(args[index], "current Codex sessions argument"); if (arg === "--help" || arg === "-h") { parsed.help = true; continue; @@ -2429,7 +2430,7 @@ function parseCodexCliSessionsArgs(args: string[]): ParsedCodexCliSessionsArgs { function parseResumeArgs(args: string[]): ParsedResumeArgs { const parsed: ParsedResumeArgs = {}; for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; + const arg = expectDefined(args[index], "current Codex resume argument"); if (arg === "--help" || arg === "-h") { parsed.help = true; continue; diff --git a/extensions/codex/src/node-cli-sessions.ts b/extensions/codex/src/node-cli-sessions.ts index f8944587571d..e17b60e1a7b6 100644 --- a/extensions/codex/src/node-cli-sessions.ts +++ b/extensions/codex/src/node-cli-sessions.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import process from "node:process"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime"; import type { OpenClawPluginNodeHostCommand, @@ -594,7 +595,7 @@ async function resolveCodexCliNode(params: { if (usable.length > 1) { throw new Error("Multiple Codex CLI-capable nodes connected. Pass --host ."); } - return usable[0]; + return expectDefined(usable[0], "single usable Codex CLI node"); } function parseCodexCliSessionsListResult(raw: unknown): CodexCliSessionsListResult { diff --git a/extensions/codex/src/supervision-tools.ts b/extensions/codex/src/supervision-tools.ts index 17917da52181..55fe299a9522 100644 --- a/extensions/codex/src/supervision-tools.ts +++ b/extensions/codex/src/supervision-tools.ts @@ -1,3 +1,6 @@ +import { resolveDefaultAgentDir } from "openclaw/plugin-sdk/agent-runtime"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core"; /** * Compatibility tools for the retired Codex Supervisor plugin. * @@ -6,9 +9,7 @@ * continuation belongs to the Codex harness, which installs approval and tool * handlers before it starts or resumes the harness-owned Codex thread. */ -import { resolveDefaultAgentDir } from "openclaw/plugin-sdk/agent-runtime"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { Type } from "typebox"; import { @@ -753,7 +754,7 @@ async function resolveEndpointForThread(params: { } } if (matches.length === 1) { - return matches[0]; + return expectDefined(matches[0], "single matching Codex supervision endpoint"); } if (matches.length > 1) { throw new Error(`Codex thread id is ambiguous across endpoints: ${params.threadId}`); @@ -763,8 +764,7 @@ async function resolveEndpointForThread(params: { function findInProgressTurnId(thread: Record): string | undefined { const turns = asRecordArray(thread.turns); - for (let index = turns.length - 1; index >= 0; index -= 1) { - const turn = turns[index]; + for (const turn of turns.toReversed()) { if (turn.status === "inProgress" && typeof turn.id === "string") { return turn.id; } diff --git a/extensions/cohere/models.ts b/extensions/cohere/models.ts index ef7efe847196..ac7e777329ab 100644 --- a/extensions/cohere/models.ts +++ b/extensions/cohere/models.ts @@ -1,6 +1,7 @@ /** * Cohere model catalog helpers derived from the plugin manifest. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; import manifest from "./openclaw.plugin.json" with { type: "json" }; @@ -35,8 +36,9 @@ export function buildCohereCatalogModels(): ModelDefinitionConfig[] { export function buildCohereModelDefinition( model: (typeof COHERE_MODEL_CATALOG)[number], ): ModelDefinitionConfig { - return buildManifestModelProviderConfig({ + const providerConfig = buildManifestModelProviderConfig({ providerId: "cohere", catalog: { ...COHERE_MANIFEST_CATALOG, models: [model] }, - }).models[0]; + }); + return expectDefined(providerConfig.models.at(0), "normalized Cohere manifest model"); } diff --git a/extensions/device-pair/pair-command-approve.ts b/extensions/device-pair/pair-command-approve.ts index cd5018de9491..8518bcb13612 100644 --- a/extensions/device-pair/pair-command-approve.ts +++ b/extensions/device-pair/pair-command-approve.ts @@ -26,20 +26,20 @@ export function selectPendingApprovalRequest(params: { pending: PendingPairingEntry[]; requested?: string; }): { pending?: PendingPairingEntry; reply?: { text: string } } { - if (params.pending.length === 0) { + const [firstPending, ...remainingPending] = params.pending; + if (!firstPending) { return { reply: { text: "No pending device pairing requests." } }; } if (!params.requested) { - return params.pending.length === 1 - ? { pending: params.pending[0] } + return remainingPending.length === 0 + ? { pending: firstPending } : { reply: buildMultiplePendingApprovalReply(params.pending) }; } if (normalizeLowercaseStringOrEmpty(params.requested) === "latest") { - let latest = params.pending[0]; - for (let index = 1; index < params.pending.length; index += 1) { - const pending = params.pending[index]; + let latest = firstPending; + for (const pending of remainingPending) { if ((pending.ts ?? 0) > (latest.ts ?? 0)) { latest = pending; } diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index d03b1d3abde5..f071c1c7bb0e 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -1327,13 +1327,10 @@ function assignOtelLogEventAttributes( if (!eventAttributes) { return; } - for (const rawKey in eventAttributes) { + for (const [rawKey, value] of Object.entries(eventAttributes)) { if (Object.keys(attributes).length >= MAX_OTEL_LOG_ATTRIBUTE_COUNT) { break; } - if (!Object.hasOwn(eventAttributes, rawKey)) { - continue; - } const key = rawKey.trim(); if (BLOCKED_OTEL_LOG_ATTRIBUTE_KEYS.has(key)) { continue; @@ -1344,7 +1341,7 @@ function assignOtelLogEventAttributes( if (!OTEL_LOG_RAW_ATTRIBUTE_KEY_RE.test(key)) { continue; } - assignOtelLogAttribute(attributes, `openclaw.${key}`, eventAttributes[rawKey]); + assignOtelLogAttribute(attributes, `openclaw.${key}`, value); } } @@ -1355,13 +1352,10 @@ function assignOtelSecurityEventAttributes( if (!eventAttributes) { return; } - for (const rawKey in eventAttributes) { + for (const [rawKey, value] of Object.entries(eventAttributes)) { if (Object.keys(attributes).length >= MAX_OTEL_LOG_ATTRIBUTE_COUNT) { break; } - if (!Object.hasOwn(eventAttributes, rawKey)) { - continue; - } const key = rawKey.trim(); if (BLOCKED_OTEL_LOG_ATTRIBUTE_KEYS.has(key)) { continue; @@ -1372,7 +1366,6 @@ function assignOtelSecurityEventAttributes( if (!OTEL_LOG_RAW_ATTRIBUTE_KEY_RE.test(key)) { continue; } - const value = eventAttributes[rawKey]; assignOtelLogAttribute( attributes, `openclaw.security.attribute.${key}`, diff --git a/extensions/discord/src/approval-native.ts b/extensions/discord/src/approval-native.ts index 01a28650153d..5d32263a9a17 100644 --- a/extensions/discord/src/approval-native.ts +++ b/extensions/discord/src/approval-native.ts @@ -30,7 +30,7 @@ export function extractDiscordChannelId(sessionKey?: string | null): string | nu return null; } const match = sessionKey.match(/discord:(?:channel|group):(\d+)/); - return match ? match[1] : null; + return match?.[1] ?? null; } function extractDiscordSessionKind(sessionKey?: string | null): "channel" | "group" | "dm" | null { @@ -48,7 +48,7 @@ function extractDiscordSessionKind(sessionKey?: string | null): "channel" | "gro if (raw === "direct") { return "dm"; } - return raw as "channel" | "group" | "dm"; + return raw === "channel" || raw === "group" || raw === "dm" ? raw : null; } function normalizeDiscordOriginChannelId(value?: string | null): string | null { @@ -61,7 +61,7 @@ function normalizeDiscordOriginChannelId(value?: string | null): string | null { } const prefixed = trimmed.match(/^(?:channel|group):(\d+)$/i); if (prefixed) { - return prefixed[1]; + return prefixed[1] ?? null; } return /^\d+$/.test(trimmed) ? trimmed : null; } diff --git a/extensions/discord/src/chunk.ts b/extensions/discord/src/chunk.ts index eb8cacb50b24..c9f20f56ba08 100644 --- a/extensions/discord/src/chunk.ts +++ b/extensions/discord/src/chunk.ts @@ -1,4 +1,5 @@ // Discord plugin module implements chunk behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime"; import { chunkMarkdownTextWithMode, type ChunkMode } from "openclaw/plugin-sdk/reply-chunking"; @@ -93,7 +94,7 @@ function clampToCodePointBoundary(text: string, index: number) { function findWhitespaceBreak(window: string) { for (let i = window.length - 1; i >= 0; i--) { - if (/\s/.test(window[i])) { + if (/\s/.test(window.charAt(i))) { // Return the separator index so whitespace stays with the next segment. return i; } @@ -244,7 +245,7 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}): currentLines += 1; } } else { - current = segment; + current = expectDefined(segment, "current Discord chunk segment"); currentLines = 1; } } @@ -305,7 +306,7 @@ function rebalanceReasoningItalics(source: string, chunks: string[]): string[] { const adjusted = [...chunks]; for (let i = 0; i < adjusted.length; i++) { const isLast = i === adjusted.length - 1; - const current = adjusted[i]; + const current = expectDefined(adjusted[i], "Discord chunk adjustment index"); // Ensure current chunk closes italics so Discord renders it italicized. const needsClosing = !current.trimEnd().endsWith("_"); @@ -318,7 +319,7 @@ function rebalanceReasoningItalics(source: string, chunks: string[]): string[] { } // Re-open italics on the next chunk if needed. - const next = adjusted[i + 1]; + const next = expectDefined(adjusted[i + 1], "non-final Discord chunk successor"); const leadingWhitespaceLen = next.length - next.trimStart().length; const leadingWhitespace = next.slice(0, leadingWhitespaceLen); const nextBody = next.slice(leadingWhitespaceLen); diff --git a/extensions/discord/src/doctor.ts b/extensions/discord/src/doctor.ts index f4ed4c6e7e0f..c49d02ab8394 100644 --- a/extensions/discord/src/doctor.ts +++ b/extensions/discord/src/doctor.ts @@ -1,6 +1,7 @@ -// Discord plugin module implements doctor behavior. import type { ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +// Discord plugin module implements doctor behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime-doctor"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { inspectDiscordAccount } from "./account-inspect.js"; @@ -161,14 +162,14 @@ export function collectDiscordNumericIdWarnings(params: { const lines: string[] = []; if (repairableHits.length > 0) { - const sample = repairableHits[0]; + const sample = expectDefined(repairableHits.at(0), "non-empty repairable Discord ID hits"); lines.push( `- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${sanitizeForLog(sample.path)}=${sanitizeForLog(String(sample.entry))}).`, `- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`, ); } if (blockedHits.length > 0) { - const sample = blockedHits[0]; + const sample = expectDefined(blockedHits.at(0), "non-empty blocked Discord ID hits"); lines.push( `- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${sanitizeForLog(sample.path)}).`, `- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`, diff --git a/extensions/discord/src/internal/components.base.ts b/extensions/discord/src/internal/components.base.ts index 85d2be6e7cd8..6ec80eb9deca 100644 --- a/extensions/discord/src/internal/components.base.ts +++ b/extensions/discord/src/internal/components.base.ts @@ -1,4 +1,5 @@ // Discord plugin module implements components.base behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { BaseComponentInteraction } from "./interactions.js"; export type ComponentParserResult = { @@ -13,9 +14,13 @@ export type ComponentData< export type ConditionalComponentOption = (interaction: BaseComponentInteraction) => boolean; export function parseCustomId(id: string): ComponentParserResult { - const [rawKey, ...parts] = id.split(";"); + const [rawKeyValue, ...parts] = id.split(";"); + const rawKey = expectDefined(rawKeyValue, "custom id split first segment"); const [keyPart, firstValue] = rawKey.split("="); - const key = keyPart.includes(":") ? keyPart.split(":")[0] : keyPart; + const definedKeyPart = expectDefined(keyPart, "custom id key segment"); + const key = definedKeyPart.includes(":") + ? expectDefined(definedKeyPart.split(":").at(0), "namespaced custom id key") + : definedKeyPart; const data: ComponentParserResult["data"] = {}; const entries = firstValue === undefined ? parts : [rawKey.slice(key.length + 1), ...parts]; for (const entry of entries) { diff --git a/extensions/discord/src/mentions.ts b/extensions/discord/src/mentions.ts index 512698b1877a..d5d545a1fc18 100644 --- a/extensions/discord/src/mentions.ts +++ b/extensions/discord/src/mentions.ts @@ -1,4 +1,5 @@ // Discord plugin module implements mentions behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -38,7 +39,7 @@ export function formatMention(params: { if (values.length !== 1) { throw new Error("formatMention requires exactly one of userId, roleId, or channelId"); } - const target = values[0]; + const target = expectDefined(values.at(0), "single Discord mention target"); if (target.kind === "user") { return `<@${target.id}>`; } diff --git a/extensions/discord/src/monitor/message-handler.draft-preview.ts b/extensions/discord/src/monitor/message-handler.draft-preview.ts index 893c12dad311..9dffaa594a70 100644 --- a/extensions/discord/src/monitor/message-handler.draft-preview.ts +++ b/extensions/discord/src/monitor/message-handler.draft-preview.ts @@ -1,4 +1,3 @@ -// Discord plugin module implements message handlerraft preview behavior. import { EmbeddedBlockChunker } from "openclaw/plugin-sdk/agent-runtime"; import { type ChannelProgressDraftLine, @@ -10,6 +9,8 @@ import { resolveChannelStreamingSuppressDefaultToolProgressMessages, } from "openclaw/plugin-sdk/channel-outbound"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +// Discord plugin module implements message handlerraft preview behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { convertMarkdownTables, stripInlineDirectiveTagsForDelivery, @@ -213,7 +214,7 @@ export function createDiscordDraftPreviewController(params: { if (chunks.length !== 1) { return undefined; } - const trimmed = chunks[0].trim(); + const trimmed = expectDefined(chunks.at(0), "single Discord preview chunk").trim(); if (!trimmed) { return undefined; } diff --git a/extensions/discord/src/monitor/model-picker.state.ts b/extensions/discord/src/monitor/model-picker.state.ts index 1c42686cdf25..1c5f27c0adba 100644 --- a/extensions/discord/src/monitor/model-picker.state.ts +++ b/extensions/discord/src/monitor/model-picker.state.ts @@ -1,6 +1,7 @@ // Discord plugin module implements model picker.state behavior. import { createHash } from "node:crypto"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import type { ModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime"; import { parseStrictInteger, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; @@ -398,9 +399,8 @@ export function computeAlphaBuckets(sortedItems: string[]): DiscordModelPickerBu } const firstLetter = (value: string): string => value.charAt(0).toLowerCase(); - const allSamePrefix = sortedItems.every( - (item) => firstLetter(item) === firstLetter(sortedItems[0]), - ); + const firstItem = expectDefined(sortedItems.at(0), "non-empty sorted model picker items"); + const allSamePrefix = sortedItems.every((item) => firstLetter(item) === firstLetter(firstItem)); if (allSamePrefix) { return chunkBucketsByCount(sortedItems); } @@ -418,13 +418,16 @@ export function computeAlphaBuckets(sortedItems: string[]): DiscordModelPickerBu let end = Math.min(sortedItems.length, start + target); // Extend `end` so we don't split a letter group across two buckets. if (end < sortedItems.length) { - const last = firstLetter(sortedItems[end - 1]); - while (end < sortedItems.length && firstLetter(sortedItems[end]) === last) { + const last = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor")); + while ( + end < sortedItems.length && + firstLetter(expectDefined(sortedItems[end], "bucket extension index")) === last + ) { end += 1; } } - const startLetter = firstLetter(sortedItems[start]); - const endLetter = firstLetter(sortedItems[end - 1]); + const startLetter = firstLetter(expectDefined(sortedItems[start], "bucket start index")); + const endLetter = firstLetter(expectDefined(sortedItems[end - 1], "bucket end predecessor")); const id = startLetter === endLetter ? startLetter : `${startLetter}-${endLetter}`; const label = startLetter === endLetter @@ -477,9 +480,12 @@ function resolveBucket( return null; } if (!id) { - return buckets[0]; + return expectDefined(buckets.at(0), "non-empty model picker buckets"); } - return buckets.find((bucket) => bucket.id === id) ?? buckets[0]; + return ( + buckets.find((bucket) => bucket.id === id) ?? + expectDefined(buckets.at(0), "non-empty model picker buckets") + ); } /** diff --git a/extensions/discord/src/monitor/model-picker.view.ts b/extensions/discord/src/monitor/model-picker.view.ts index 0ef3bae2ee0a..eed2a1acec9f 100644 --- a/extensions/discord/src/monitor/model-picker.view.ts +++ b/extensions/discord/src/monitor/model-picker.view.ts @@ -103,10 +103,14 @@ function parseCurrentModelRef(raw?: string): DiscordModelPickerCurrentModelRef | if (!match) { return null; } - const provider = normalizeProviderId(match[1]); + const providerText = match[1]; + const model = match[2]; + if (providerText === undefined || model === undefined) { + return null; + } + const provider = normalizeProviderId(providerText); // Preserve the model suffix exactly as entered after "/" so select defaults // continue to mirror the stored ref for Discord interactions. - const model = match[2]; if (!provider || !model) { return null; } @@ -938,8 +942,7 @@ export function renderDiscordModelPickerRecentsView( ); // Recent model buttons — slot 2+. - for (let i = 0; i < dedupedQuickModels.length; i++) { - const modelRef = dedupedQuickModels[i]; + for (const [i, modelRef] of dedupedQuickModels.entries()) { rows.push( new Row([ createModelPickerButton({ diff --git a/extensions/discord/src/monitor/thread-bindings.lifecycle.ts b/extensions/discord/src/monitor/thread-bindings.lifecycle.ts index 60b037e98a57..46bfba609855 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.ts @@ -1,6 +1,7 @@ -// Discord plugin module implements thread bindings.lifecycle behavior. import { readAcpSessionEntry, type AcpSessionStoreEntry } from "openclaw/plugin-sdk/acp-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +// Discord plugin module implements thread bindings.lifecycle behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -71,13 +72,16 @@ async function mapWithConcurrency(params: { if (index >= params.items.length) { return; } - resultsByIndex.set(index, await params.worker(params.items[index], index)); + const item = expectDefined(params.items[index], "bounded worker item index"); + resultsByIndex.set(index, await params.worker(item, index)); } }; const workers = Array.from({ length: Math.min(limit, params.items.length) }, () => runWorker()); await Promise.all(workers); - return params.items.map((_item, index) => resultsByIndex.get(index)!); + return params.items.map((_item, index) => + expectDefined(resultsByIndex.get(index), "completed bounded worker result"), + ); } export function listThreadBindingsForAccount(accountId?: string): ThreadBindingRecord[] { diff --git a/extensions/discord/src/outbound-payload.ts b/extensions/discord/src/outbound-payload.ts index 458a8d5655dc..cffc3b897588 100644 --- a/extensions/discord/src/outbound-payload.ts +++ b/extensions/discord/src/outbound-payload.ts @@ -1,8 +1,9 @@ -// Discord plugin module implements outbound payload behavior. import { attachChannelToResult, type ChannelOutboundAdapter, } from "openclaw/plugin-sdk/channel-send-result"; +// Discord plugin module implements outbound payload behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { getReplyPayloadTtsSupplement, resolvePayloadMediaUrls, @@ -101,7 +102,8 @@ export async function sendDiscordOutboundPayload(params: { let deliveredVoice = false; let lastResult: Awaited>; try { - lastResult = await sendContext.sendVoice(sendContext.target, mediaUrls[0], { + const voiceUrl = expectDefined(mediaUrls.at(0), "non-empty Discord voice media URLs"); + lastResult = await sendContext.sendVoice(sendContext.target, voiceUrl, { ...resolveDiscordDeliveryOptions(ctx, sendContext, voiceReply), }); deliveredVoice = true; diff --git a/extensions/discord/src/send.shared.ts b/extensions/discord/src/send.shared.ts index a473e2a30d91..5c14c645191c 100644 --- a/extensions/discord/src/send.shared.ts +++ b/extensions/discord/src/send.shared.ts @@ -1,8 +1,9 @@ -// Discord plugin module implements send.shared behavior. import { PollLayoutType } from "discord-api-types/payloads/v10"; import type { RESTAPIPoll } from "discord-api-types/rest/v10"; import type { APIChannel } from "discord-api-types/v10"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +// Discord plugin module implements send.shared behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { buildOutboundMediaLoadOptions } from "openclaw/plugin-sdk/media-runtime"; import { extensionForMime } from "openclaw/plugin-sdk/media-runtime"; import { @@ -378,7 +379,8 @@ async function sendDiscordText(params: DiscordTextSendParams) { return { result, replyToId: chunkReplyTo }; }; if (chunks.length === 1) { - const { result, replyToId } = await sendChunk(chunks[0], true); + const chunk = expectDefined(chunks.at(0), "single Discord text chunk"); + const { result, replyToId } = await sendChunk(chunk, true); await onResult?.(result, "text", replyToId); return { ...result, platformMessageIds: result.id ? [result.id] : [] }; } diff --git a/extensions/discord/src/voice-message.ts b/extensions/discord/src/voice-message.ts index ed534ecea9e5..362161468602 100644 --- a/extensions/discord/src/voice-message.ts +++ b/extensions/discord/src/voice-message.ts @@ -14,6 +14,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { parseFfprobeCodecAndSampleRate, runFfmpeg, @@ -168,7 +169,7 @@ async function generateWaveformFromPcm(filePath: string): Promise { let sum = 0; let count = 0; for (let j = 0; j < step && i * step + j < samples.length; j++) { - sum += Math.abs(samples[i * step + j]); + sum += Math.abs(expectDefined(samples.at(i * step + j), "bounded PCM waveform sample")); count++; } const avg = count > 0 ? sum / count : 0; diff --git a/extensions/discord/src/voice/manager.ts b/extensions/discord/src/voice/manager.ts index dc830fd6a79d..31ceebea86e7 100644 --- a/extensions/discord/src/voice/manager.ts +++ b/extensions/discord/src/voice/manager.ts @@ -1,6 +1,7 @@ -// Discord plugin module implements manager behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-contracts"; +// Discord plugin module implements manager behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; @@ -1225,7 +1226,10 @@ export class DiscordVoiceManager { if (this.botUserId && remainingLookups === 1) { break; } - const guildId = guildIds[(start + offset) % guildIds.length]; + const guildId = expectDefined( + guildIds[(start + offset) % guildIds.length], + "voice reconciliation guild index", + ); const userLimit = this.resolveFollowUserReconcileUserLookupLimit( followedUserIds.length, remainingLookups, @@ -1269,7 +1273,10 @@ export class DiscordVoiceManager { let scanned = 0; let assigned = 0; for (; scanned < guildIds.length && assigned < remainingLookups; scanned += 1) { - const guildId = guildIds[(start + scanned) % guildIds.length]; + const guildId = expectDefined( + guildIds[(start + scanned) % guildIds.length], + "bot voice reconciliation guild index", + ); const plan = plansByGuild.get(guildId); if (!plan?.checkedAllUsers) { continue; @@ -1301,10 +1308,15 @@ export class DiscordVoiceManager { return { userIds: followedUserIds, completedCycle: true }; } const start = this.followUsersReconcileUserCursors.get(guildId) ?? 0; - const selected = Array.from( - { length: limit }, - (_, offset) => followedUserIds[(start + offset) % followedUserIds.length], - ); + const selected: string[] = []; + for (let offset = 0; offset < limit; offset += 1) { + selected.push( + expectDefined( + followedUserIds[(start + offset) % followedUserIds.length], + "followed user selection index", + ), + ); + } const completedCycle = start + selected.length >= followedUserIds.length; this.followUsersReconcileUserCursors.set( guildId, @@ -1464,7 +1476,9 @@ export class DiscordVoiceManager { return null; } const guildAllowed = this.allowedChannels.filter((entry) => entry.guildId === guildId); - return guildAllowed.length === 1 ? guildAllowed[0] : null; + return guildAllowed.length === 1 + ? expectDefined(guildAllowed.at(0), "single allowed guild voice channel") + : null; } private enqueueProcessing(entry: VoiceSessionEntry, task: () => Promise) { diff --git a/extensions/document-extract/document-extractor.ts b/extensions/document-extract/document-extractor.ts index e99ba5bc7a06..809c9d70b284 100644 --- a/extensions/document-extract/document-extractor.ts +++ b/extensions/document-extract/document-extractor.ts @@ -93,13 +93,12 @@ async function extractPdfContent( try { const images: DocumentExtractedImage[] = []; let remainingPixels = request.maxPixels; - for (let index = 0; index < imagePages.length; index += 1) { + for (const [index, pageNumber] of imagePages.entries()) { if (remainingPixels <= 0) { break; } const pagesRemaining = imagePages.length - index; const maxPixelsPerPage = Math.max(1, Math.ceil(remainingPixels / pagesRemaining)); - const pageNumber = imagePages[index]; const imageResult = await pdf.extract({ mode: "images", pages: [pageNumber], diff --git a/extensions/feishu/src/bitable.ts b/extensions/feishu/src/bitable.ts index 813708f84959..b6a51c775183 100644 --- a/extensions/feishu/src/bitable.ts +++ b/extensions/feishu/src/bitable.ts @@ -82,13 +82,19 @@ function parseBitableUrl(url: string): { token: string; tableId?: string; isWiki // Wiki format: /wiki/XXXXX?table=YYY const wikiMatch = u.pathname.match(/\/wiki\/([A-Za-z0-9]+)/); if (wikiMatch) { - return { token: wikiMatch[1], tableId, isWiki: true }; + const wikiPathSegment = wikiMatch[1]; + return wikiPathSegment === undefined + ? null + : { token: wikiPathSegment, tableId, isWiki: true }; } // Base format: /base/XXXXX?table=YYY const baseMatch = u.pathname.match(/\/base\/([A-Za-z0-9]+)/); if (baseMatch) { - return { token: baseMatch[1], tableId, isWiki: false }; + const basePathSegment = baseMatch[1]; + return basePathSegment === undefined + ? null + : { token: basePathSegment, tableId, isWiki: false }; } return null; @@ -407,7 +413,7 @@ async function createApp( path: { app_token: appToken }, }); if (tablesRes.code === 0 && tablesRes.data?.items && tablesRes.data.items.length > 0) { - tableId = tablesRes.data.items[0].table_id ?? undefined; + tableId = tablesRes.data.items.at(0)?.table_id; if (tableId) { const cleanup = await cleanupNewBitable(client, appToken, tableId, name, log); cleanedRows = cleanup.cleanedRows; diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 8aa31f9b97d4..e1de4277a613 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -1736,10 +1736,14 @@ export async function handleFeishuMessage(params: { } } else { const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent)); - for (let i = 0; i < results.length; i++) { - if (results[i].status === "rejected") { + for (const [i, result] of results.entries()) { + if (result.status === "rejected") { + const agentId = broadcastAgents.at(i); + if (agentId === undefined) { + continue; + } log( - `feishu[${account.accountId}]: broadcast dispatch failed for agent=${broadcastAgents[i]}: ${String((results[i] as PromiseRejectedResult).reason)}`, + `feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`, ); } } diff --git a/extensions/feishu/src/conversation-id.ts b/extensions/feishu/src/conversation-id.ts index 3ac050acc4b1..4842652ea0d9 100644 --- a/extensions/feishu/src/conversation-id.ts +++ b/extensions/feishu/src/conversation-id.ts @@ -124,6 +124,9 @@ export function parseFeishuConversationId(params: { const topicSenderMatch = conversationId.match(/^(.+):topic:([^:]+):sender:([^:]+)$/i); if (topicSenderMatch) { const [, chatId, topicId, senderOpenId] = topicSenderMatch; + if (chatId === undefined || topicId === undefined || senderOpenId === undefined) { + return null; + } return { canonicalConversationId: buildFeishuConversationId({ chatId, @@ -141,6 +144,9 @@ export function parseFeishuConversationId(params: { const topicMatch = conversationId.match(/^(.+):topic:([^:]+)$/i); if (topicMatch) { const [, chatId, topicId] = topicMatch; + if (chatId === undefined || topicId === undefined) { + return null; + } return { canonicalConversationId: buildFeishuConversationId({ chatId, @@ -156,6 +162,9 @@ export function parseFeishuConversationId(params: { const senderMatch = conversationId.match(/^(.+):sender:([^:]+)$/i); if (senderMatch) { const [, chatId, senderOpenId] = senderMatch; + if (chatId === undefined || senderOpenId === undefined) { + return null; + } return { canonicalConversationId: buildFeishuConversationId({ chatId, diff --git a/extensions/feishu/src/docx-batch-insert.ts b/extensions/feishu/src/docx-batch-insert.ts index 1c0c0244fcfa..efad30e56c98 100644 --- a/extensions/feishu/src/docx-batch-insert.ts +++ b/extensions/feishu/src/docx-batch-insert.ts @@ -197,8 +197,7 @@ export async function insertBlocksInBatches( // When startIndex == -1 (append to end), each batch appends after the previous. // When startIndex >= 0, each batch starts at startIndex + count of first-level IDs already inserted. let currentIndex = startIndex; - for (let i = 0; i < batches.length; i++) { - const batch = batches[i]; + for (const [i, batch] of batches.entries()) { logger?.info?.( `feishu_doc: Inserting batch ${i + 1}/${batches.length} (${batch.blocks.length} blocks)...`, ); diff --git a/extensions/feishu/src/docx-color-text.ts b/extensions/feishu/src/docx-color-text.ts index 601fc4b99218..6da59c781ed2 100644 --- a/extensions/feishu/src/docx-color-text.ts +++ b/extensions/feishu/src/docx-color-text.ts @@ -89,6 +89,9 @@ function parseColorMarkup(content: string): Segment[] { // Tagged segment const tagStr = normalizeLowercaseStringOrEmpty(match[1]); const text = match[2]; + if (text === undefined) { + continue; + } const tags = tagStr.split(/\s+/); const segment: Segment = { text }; diff --git a/extensions/feishu/src/docx-table-ops.ts b/extensions/feishu/src/docx-table-ops.ts index 79e047c6d729..dcc9ab4fb3c7 100644 --- a/extensions/feishu/src/docx-table-ops.ts +++ b/extensions/feishu/src/docx-table-ops.ts @@ -126,7 +126,7 @@ function calculateAdaptiveColumnWidths(blocks: FeishuDocxBlock[], tableBlockId: if (cellId) { const content = getCellText(cellId); const length = getWeightedLength(content); - maxLengths[col] = Math.max(maxLengths[col], length); + maxLengths[col] = Math.max(maxLengths[col] ?? 0, length); } } } @@ -168,8 +168,12 @@ function calculateAdaptiveColumnWidths(blocks: FeishuDocxBlock[], tableBlockId: } for (const i of growable) { - const add = Math.min(perColumn, MAX_COLUMN_WIDTH - widths[i]); - widths[i] += add; + const width = widths[i]; + if (width === undefined) { + continue; + } + const add = Math.min(perColumn, MAX_COLUMN_WIDTH - width); + widths[i] = width + add; remaining -= add; } } diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index 248c05119ad3..8302566752e8 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -54,7 +54,11 @@ function extractImageUrls(markdown: string): string[] { const urls: string[] = []; let match; while ((match = regex.exec(markdown)) !== null) { - const url = match[1].trim(); + const capturedUrl = match[1]; + if (capturedUrl === undefined) { + continue; + } + const url = capturedUrl.trim(); if (url.startsWith("http://") || url.startsWith("https://")) { urls.push(url); } @@ -669,7 +673,7 @@ async function processImages( for (let i = 0; i < Math.min(imageUrls.length, imageBlocks.length); i++) { const url = imageUrls[i]; const blockId = imageBlocks[i]?.block_id; - if (!blockId) { + if (!url || !blockId) { continue; } diff --git a/extensions/feishu/src/monitor.bot-identity.ts b/extensions/feishu/src/monitor.bot-identity.ts index db5118aa7e1a..fee5f92d8103 100644 --- a/extensions/feishu/src/monitor.bot-identity.ts +++ b/extensions/feishu/src/monitor.bot-identity.ts @@ -31,12 +31,13 @@ async function retryBotIdentityProbe( const log = runtime?.log ?? console.log; const error = runtime?.error ?? console.error; - for (let i = 0; i < BOT_IDENTITY_RETRY_DELAYS_MS.length; i += 1) { + const nextDelays = BOT_IDENTITY_RETRY_DELAYS_MS.slice(1)[Symbol.iterator](); + for (const [i, delayMs] of BOT_IDENTITY_RETRY_DELAYS_MS.entries()) { if (abortSignal?.aborted) { return; } - const delayElapsed = await waitForAbortableDelay(BOT_IDENTITY_RETRY_DELAYS_MS[i], abortSignal); + const delayElapsed = await waitForAbortableDelay(delayMs, abortSignal); if (!delayElapsed) { return; } @@ -50,7 +51,8 @@ async function retryBotIdentityProbe( return; } - const nextDelay = BOT_IDENTITY_RETRY_DELAYS_MS[i + 1]; + const nextDelayResult = nextDelays.next(); + const nextDelay = nextDelayResult.done ? undefined : nextDelayResult.value; error( `feishu[${accountId}]: bot identity background retry ${i + 1}/${BOT_IDENTITY_RETRY_DELAYS_MS.length} failed` + (nextDelay ? `; next attempt in ${nextDelay / 1000}s` : ""), diff --git a/extensions/feishu/src/monitor.message-handler.ts b/extensions/feishu/src/monitor.message-handler.ts index 3ce385aaa7f4..8b1bcb4ad2e9 100644 --- a/extensions/feishu/src/monitor.message-handler.ts +++ b/extensions/feishu/src/monitor.message-handler.ts @@ -138,8 +138,7 @@ function resolveFeishuDebounceMentions(params: { if (entries.length === 0) { return undefined; } - for (let index = entries.length - 1; index >= 0; index -= 1) { - const entry = entries[index]; + for (const entry of entries.toReversed()) { if (isMentionForwardRequest(entry, botOpenId)) { return mergeFeishuDebounceMentions([entry]); } diff --git a/extensions/feishu/src/subagent-hooks.ts b/extensions/feishu/src/subagent-hooks.ts index a5d9fc638730..4286671669ab 100644 --- a/extensions/feishu/src/subagent-hooks.ts +++ b/extensions/feishu/src/subagent-hooks.ts @@ -56,7 +56,10 @@ function resolveFeishuRequesterConversation(params: { if (requesterSessionKey) { const existingBindings = manager.listBySessionKey(requesterSessionKey); if (existingBindings.length === 1) { - const existing = existingBindings[0]; + const existing = existingBindings.at(0); + if (existing === undefined) { + return null; + } return { accountId: existing.accountId, conversationId: existing.conversationId, @@ -72,7 +75,10 @@ function resolveFeishuRequesterConversation(params: { !entry.parentConversationId, ); if (directMatches.length === 1) { - const existing = directMatches[0]; + const existing = directMatches.at(0); + if (existing === undefined) { + return null; + } return { accountId: existing.accountId, conversationId: existing.conversationId, @@ -93,7 +99,10 @@ function resolveFeishuRequesterConversation(params: { ); }); if (matchingTopicBindings.length === 1) { - const existing = matchingTopicBindings[0]; + const existing = matchingTopicBindings.at(0); + if (existing === undefined) { + return null; + } return { accountId: existing.accountId, conversationId: existing.conversationId, @@ -111,7 +120,10 @@ function resolveFeishuRequesterConversation(params: { senderScopedTopicBindings.length === 1 && matchingTopicBindings.length === senderScopedTopicBindings.length ) { - const existing = senderScopedTopicBindings[0]; + const existing = senderScopedTopicBindings.at(0); + if (existing === undefined) { + return null; + } return { accountId: existing.accountId, conversationId: existing.conversationId, diff --git a/extensions/file-transfer/src/node-host/dir-fetch.ts b/extensions/file-transfer/src/node-host/dir-fetch.ts index d6af355261fc..7ad94f1c80d3 100644 --- a/extensions/file-transfer/src/node-host/dir-fetch.ts +++ b/extensions/file-transfer/src/node-host/dir-fetch.ts @@ -113,7 +113,7 @@ async function preflightDu(dirPath: string, maxBytes: number): Promise finish(true); return; } - const sizeKb = Number.parseInt(match[1], 10); + const sizeKb = Number.parseInt(match[0], 10); finish(sizeKb <= heuristicKb); }); du.on("error", () => { diff --git a/extensions/file-transfer/src/shared/policy.ts b/extensions/file-transfer/src/shared/policy.ts index a2e76d1cabfd..c39fae3f7409 100644 --- a/extensions/file-transfer/src/shared/policy.ts +++ b/extensions/file-transfer/src/shared/policy.ts @@ -367,12 +367,18 @@ export async function persistAllowAlways(input: { ); // Use hasOwnProperty so a node with displayName "constructor" doesn't // accidentally hit Object.prototype.constructor and pretend to match. - let key = candidates.find((c) => Object.hasOwn(fileTransfer, c)); - if (!key) { - key = assertSafeConfigKey(input.nodeDisplayName ?? input.nodeId); - fileTransfer[key] = {}; + let entry: NodeFilePolicyConfig | undefined; + for (const candidate of candidates) { + entry = Object.entries(fileTransfer).find(([key]) => key === candidate)?.[1]; + if (entry) { + break; + } + } + if (!entry) { + const key = assertSafeConfigKey(input.nodeDisplayName ?? input.nodeId); + entry = {}; + fileTransfer[key] = entry; } - const entry = fileTransfer[key]; const list = Array.isArray(entry[field]) ? entry[field] : []; if (!list.includes(input.path)) { list.push(input.path); diff --git a/extensions/file-transfer/src/tools/dir-fetch-tool.ts b/extensions/file-transfer/src/tools/dir-fetch-tool.ts index 20fe3eb7b427..1c7b4e492c5c 100644 --- a/extensions/file-transfer/src/tools/dir-fetch-tool.ts +++ b/extensions/file-transfer/src/tools/dir-fetch-tool.ts @@ -257,9 +257,14 @@ async function preValidateTarball( }; } - for (let i = 0; i < paths.length; i++) { - const entryPath = paths[i]; - const t = typeChars[i]; + for (const [index, entryPath] of paths.entries()) { + const t = typeChars.at(index); + if (t === undefined) { + return { + ok: false, + reason: `tar -tzf and tar -tzvf disagree on entry count (${paths.length} vs ${typeChars.length}); refusing`, + }; + } if (t === "l" || t === "h") { return { ok: false, reason: `archive contains link entry: ${entryPath}` }; } diff --git a/extensions/github-copilot/embeddings.ts b/extensions/github-copilot/embeddings.ts index 2da4ae1ab163..da7c47ef2484 100644 --- a/extensions/github-copilot/embeddings.ts +++ b/extensions/github-copilot/embeddings.ts @@ -154,8 +154,9 @@ function pickBestModel(available: string[], userModel?: string): string { return preferred; } } - if (available.length > 0) { - return available[0]; + const [firstAvailable] = available; + if (firstAvailable) { + return firstAvailable; } throw new Error("No embedding models available from GitHub Copilot"); } diff --git a/extensions/github-copilot/index.ts b/extensions/github-copilot/index.ts index 3a487fe3cb76..cd85e3b6f17d 100644 --- a/extensions/github-copilot/index.ts +++ b/extensions/github-copilot/index.ts @@ -37,7 +37,11 @@ import { } from "./replay-policy.js"; import { wrapCopilotProviderStream } from "./stream.js"; -const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]; +const COPILOT_ENV_VARS: [string, string, string] = [ + "COPILOT_GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", +]; const DEFAULT_COPILOT_MODEL = "github-copilot/claude-opus-4.7"; const DEFAULT_COPILOT_PROFILE_ID = "github-copilot:github"; @@ -165,25 +169,30 @@ function applyGithubCopilotDomainToConfig( const models = config.models ?? {}; const providers = models.providers ?? {}; - const provider = providers[PROVIDER_ID] ?? {}; - const params = { ...provider.params } as Record; + const provider = providers[PROVIDER_ID]; + const params: Record = {}; + if (provider?.params) { + Object.assign(params, provider.params); + } if (isEnterprise) { params.githubDomain = domain; } else { delete params.githubDomain; } + const nextProviders = { ...providers }; + if (provider) { + nextProviders[PROVIDER_ID] = { ...provider, params }; + } else { + // Source config accepts partial provider inputs; catalog materialization + // supplies baseUrl/models before runtime consumption. + Object.assign(nextProviders, { [PROVIDER_ID]: { params } }); + } return { ...config, models: { ...models, - providers: { - ...providers, - [PROVIDER_ID]: { - ...provider, - params, - }, - }, + providers: nextProviders, }, }; } diff --git a/extensions/github-copilot/login.ts b/extensions/github-copilot/login.ts index 7e8ddb9c8848..ab140b9ed21d 100644 --- a/extensions/github-copilot/login.ts +++ b/extensions/github-copilot/login.ts @@ -368,8 +368,8 @@ export function withGithubCopilotDomainConfig(cfg: OpenClawConfig, domain: strin // `T | undefined`, which exactOptionalPropertyTypes rejects. const models: NonNullable = cfg.models ?? {}; const providers: NonNullable = models.providers ?? {}; - const provider: NonNullable<(typeof providers)[string]> = providers["github-copilot"] ?? {}; - const params = provider.params; + const provider = providers["github-copilot"]; + const params = provider?.params; const isDefault = domain === PUBLIC_GITHUB_COPILOT_DOMAIN; if (isDefault && !(params && "githubDomain" in params)) { return cfg; @@ -380,14 +380,19 @@ export function withGithubCopilotDomainConfig(cfg: OpenClawConfig, domain: strin } else { nextParams.githubDomain = domain; } + const nextProviders = { ...providers }; + if (provider) { + nextProviders["github-copilot"] = { ...provider, params: nextParams }; + } else { + // Source config accepts partial provider inputs; catalog materialization + // supplies baseUrl/models before runtime consumption. + Object.assign(nextProviders, { "github-copilot": { params: nextParams } }); + } return { ...cfg, models: { ...models, - providers: { - ...providers, - "github-copilot": { ...provider, params: nextParams }, - }, + providers: nextProviders, }, }; } diff --git a/extensions/google-meet/src/cli.ts b/extensions/google-meet/src/cli.ts index dae184133707..c7a2fda0c2b4 100644 --- a/extensions/google-meet/src/cli.ts +++ b/extensions/google-meet/src/cli.ts @@ -5,6 +5,7 @@ import { createInterface } from "node:readline/promises"; import { format } from "node:util"; import type { Command } from "commander"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime"; import { clampTimerTimeoutMs, @@ -1317,7 +1318,11 @@ const CRC32_TABLE = new Uint32Array( function crc32(buffer: Buffer): number { let value = 0xffffffff; for (const byte of buffer) { - value = CRC32_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8); + const tableValue = expectDefined( + CRC32_TABLE.at((value ^ byte) & 0xff), + "CRC32 lookup table entry", + ); + value = tableValue ^ (value >>> 8); } return (value ^ 0xffffffff) >>> 0; } diff --git a/extensions/google-meet/src/transports/chrome-browser-proxy.ts b/extensions/google-meet/src/transports/chrome-browser-proxy.ts index 9373fedd9e29..ab399c55249e 100644 --- a/extensions/google-meet/src/transports/chrome-browser-proxy.ts +++ b/extensions/google-meet/src/transports/chrome-browser-proxy.ts @@ -131,34 +131,35 @@ export async function resolveChromeNodeInfo(params: { if (requested) { const list = await listGoogleMeetNodes(params.runtime); const matches = list.nodes.filter((node) => matchesRequestedNode(node, requested)); - if (matches.length === 1) { - const [node] = matches; - if (isGoogleMeetNode(node)) { - return node; - } - throw new Error( - `Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`, - ); - } if (matches.length > 1) { throw new Error( `Configured Google Meet node ${requested} is ambiguous (${matches.length} matches). Pin chromeNode.node to a unique node id, display name, or remote IP.`, ); } + const [node] = matches; + if (!node) { + throw new Error( + `Configured Google Meet node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`, + ); + } + if (isGoogleMeetNode(node)) { + return node; + } throw new Error( - `Configured Google Meet node ${requested} was not found. Run \`openclaw nodes status\` and start or approve the Chrome node.`, + `Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`openclaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`, ); } const list = await listGoogleMeetNodes(params.runtime, { connected: true }); const nodes = list.nodes.filter(isGoogleMeetNode); - if (nodes.length === 0) { + const [node] = nodes; + if (!node) { throw new Error( "No connected Google Meet-capable node with browser proxy. Run `openclaw node run` on the Chrome host with browser proxy enabled, approve pairing, and allow googlemeet.chrome plus browser.proxy.", ); } if (nodes.length === 1) { - return nodes[0]; + return node; } throw new Error( "Multiple Google Meet-capable nodes connected. Set plugins.entries.google-meet.config.chromeNode.node.", diff --git a/extensions/google/generation-provider-metadata.ts b/extensions/google/generation-provider-metadata.ts index 11c1ba816f00..abbf72ac462d 100644 --- a/extensions/google/generation-provider-metadata.ts +++ b/extensions/google/generation-provider-metadata.ts @@ -13,8 +13,7 @@ export const GOOGLE_MAX_INPUT_IMAGES = 10; export const DEFAULT_GOOGLE_VIDEO_MODEL = "veo-3.1-fast-generate-preview"; export const GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS = [4, 6, 8] as const; export const GOOGLE_VIDEO_MIN_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[0]; -export const GOOGLE_VIDEO_MAX_DURATION_SECONDS = - GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1]; +export const GOOGLE_VIDEO_MAX_DURATION_SECONDS = GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[2]; function isGoogleProviderConfigured( ctx: { agentDir?: string } | VideoGenerationProviderConfiguredContext, diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index e3acf1be1014..e3e67d76899c 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -266,8 +266,8 @@ async function processMessageWithPipeline(params: { let mediaPath: string | undefined; let mediaType: string | undefined; - if (attachments.length > 0) { - const first = attachments[0]; + const first = attachments.at(0); + if (first) { const attachmentData = await downloadAttachment(first, account, mediaMaxMb, core); if (attachmentData) { mediaPath = attachmentData.path; diff --git a/extensions/imessage/src/accounts.ts b/extensions/imessage/src/accounts.ts index ed34ed851667..823d6dd420ed 100644 --- a/extensions/imessage/src/accounts.ts +++ b/extensions/imessage/src/accounts.ts @@ -1,4 +1,3 @@ -// Imessage plugin module implements accounts behavior. import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; import { createAccountListHelpers, @@ -6,6 +5,8 @@ import { resolveMergedAccountConfig, type OpenClawConfig, } from "openclaw/plugin-sdk/account-resolution"; +// Imessage plugin module implements accounts behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { resolveAccountEntry } from "openclaw/plugin-sdk/routing"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { IMessageAccountConfig } from "./account-types.js"; @@ -219,11 +220,12 @@ export function collectIMessageDuplicateAccountSourceWarnings(params: { if (collisions.length < 2) { continue; } + const firstCollision = expectDefined(collisions[0], "duplicate iMessage account source"); const ownerId = resolveIMessageAccountSourceOwner({ cfg: params.cfg, - signature: resolveIMessageAccountSourceSignature(collisions[0]), + signature: resolveIMessageAccountSourceSignature(firstCollision), }); - const owner = collisions.find((a) => a.accountId === ownerId) ?? collisions[0]; + const owner = collisions.find((a) => a.accountId === ownerId) ?? firstCollision; const duplicates = collisions.filter((a) => a.accountId !== owner.accountId); const dupIds = duplicates.map((a) => `"${a.accountId}"`).join(", "); const cliPath = normalizeIMessageCliPath(owner.config.cliPath); diff --git a/extensions/imessage/src/approval-reactions.ts b/extensions/imessage/src/approval-reactions.ts index d7a8b0648315..6896eba2bba5 100644 --- a/extensions/imessage/src/approval-reactions.ts +++ b/extensions/imessage/src/approval-reactions.ts @@ -414,10 +414,16 @@ function visibleApprovalBindingMatches( const visibleDecisions: ExecApprovalReplyDecision[] = []; for (const line of lines) { const match = line.match(APPROVE_COMMAND_LINE_RE); - if (!match || (match[1] !== binding.approvalId && match[1] !== binding.approvalSlug)) { + const approvalId = match?.[1]; + const decisionsText = match?.[2]; + if ( + !approvalId || + !decisionsText || + (approvalId !== binding.approvalId && approvalId !== binding.approvalSlug) + ) { continue; } - for (const token of match[2].split(/[\s|,]+/)) { + for (const token of decisionsText.split(/[\s|,]+/)) { const decision = normalizeApprovalDecision(token); if (decision && !visibleDecisions.includes(decision)) { visibleDecisions.push(decision); @@ -539,13 +545,17 @@ export function extractIMessageApprovalPromptBinding(text: string): { return null; } const approvalId = idHeaderMatch[1]; + if (!approvalId) { + return null; + } const allowedDecisions: ExecApprovalReplyDecision[] = []; for (const line of lines) { const match = line.match(APPROVE_COMMAND_LINE_RE); - if (!match || match[1] !== approvalId) { + const decisionsText = match?.[2]; + if (!match || match[1] !== approvalId || !decisionsText) { continue; } - const decisions = match[2].split(/[\s|,]+/); + const decisions = decisionsText.split(/[\s|,]+/); for (const decisionText of decisions) { const decision = normalizeApprovalDecision(decisionText); if (decision && !allowedDecisions.includes(decision)) { diff --git a/extensions/imessage/src/monitor/catchup.ts b/extensions/imessage/src/monitor/catchup.ts index f09dea07352b..13db2627479d 100644 --- a/extensions/imessage/src/monitor/catchup.ts +++ b/extensions/imessage/src/monitor/catchup.ts @@ -233,8 +233,7 @@ export function capFailureRetriesMap( // debugging). entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); const capped: Record = {}; - for (let i = 0; i < entries.length && i < maxSize; i++) { - const [guid, count] = entries[i]; + for (const [guid, count] of entries.slice(0, maxSize)) { capped[guid] = count; if (textEncoder.encode(JSON.stringify(capped)).byteLength > maxBytes) { delete capped[guid]; diff --git a/extensions/imessage/src/monitor/coalesce.ts b/extensions/imessage/src/monitor/coalesce.ts index c280040b82df..59b3fadb199e 100644 --- a/extensions/imessage/src/monitor/coalesce.ts +++ b/extensions/imessage/src/monitor/coalesce.ts @@ -1,4 +1,5 @@ // Imessage plugin module implements the same-sender inbound debounce merge. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { IMessagePayload } from "./types.js"; @@ -104,12 +105,12 @@ export function combineIMessagePayloads(payloads: IMessagePayload[]): CoalescedI if (payloads.length === 0) { throw new Error("combineIMessagePayloads: cannot combine empty payloads"); } + const first = expectDefined(payloads[0], "first iMessage payload to coalesce"); if (payloads.length === 1) { - return payloads[0]; + return first; } - const first = payloads[0]; - const last = payloads[payloads.length - 1]; + const last = expectDefined(payloads.at(-1), "last iMessage payload to coalesce"); // Cap entries: keep first (preserves command/context) + most recent // (preserves latest payload) when a flood exceeds the cap. diff --git a/extensions/imessage/src/monitor/monitor-provider.ts b/extensions/imessage/src/monitor/monitor-provider.ts index e4184cc6b939..93840dfa045f 100644 --- a/extensions/imessage/src/monitor/monitor-provider.ts +++ b/extensions/imessage/src/monitor/monitor-provider.ts @@ -24,6 +24,7 @@ import { upsertChannelPairingRequest, } from "openclaw/plugin-sdk/conversation-runtime"; import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeScpRemoteHost } from "openclaw/plugin-sdk/host-runtime"; import { isInboundPathAllowed, kindFromMime } from "openclaw/plugin-sdk/media-runtime"; import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-history"; @@ -772,7 +773,10 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P }; if (entries.length === 1) { - await dispatchUnit(entries, entries[0].message); + await dispatchUnit( + entries, + expectDefined(entries[0], "single iMessage dispatch entry").message, + ); return; } diff --git a/extensions/imessage/src/monitor/self-chat-cache.ts b/extensions/imessage/src/monitor/self-chat-cache.ts index 5aaf284b58dd..12f5f2bd3935 100644 --- a/extensions/imessage/src/monitor/self-chat-cache.ts +++ b/extensions/imessage/src/monitor/self-chat-cache.ts @@ -1,5 +1,6 @@ // Imessage plugin module implements self chat cache behavior. import { createHash } from "node:crypto"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { formatIMessageChatTarget } from "../targets.js"; type SelfChatCacheKeyParts = { @@ -134,7 +135,10 @@ class DefaultSelfChatCache implements SelfChatCache { this.entryCount > MAX_SELF_CHAT_CACHE_ENTRIES && this.insertionOrderOffset < this.insertionOrder.length ) { - const oldest = this.insertionOrder[this.insertionOrderOffset]; + const oldest = expectDefined( + this.insertionOrder[this.insertionOrderOffset], + "oldest iMessage self-chat cache entry", + ); this.insertionOrderOffset += 1; const entries = this.cache.get(oldest.key); if (!entries) { diff --git a/extensions/imessage/src/monitor/strip-imsg-length-prefixed-text.ts b/extensions/imessage/src/monitor/strip-imsg-length-prefixed-text.ts index 179327b62cd8..4e74c29e5b95 100644 --- a/extensions/imessage/src/monitor/strip-imsg-length-prefixed-text.ts +++ b/extensions/imessage/src/monitor/strip-imsg-length-prefixed-text.ts @@ -13,6 +13,9 @@ function readVarint(buf: Uint8Array, start: number): Varint | null { while (offset < buf.length && shift <= 28) { const byte = buf[offset]; + if (byte === undefined) { + return null; + } offset += 1; value |= (byte & 0x7f) << shift; if ((byte & 0x80) === 0) { diff --git a/extensions/line/src/card-command.ts b/extensions/line/src/card-command.ts index a124e6fce6ea..b7482c98eeea 100644 --- a/extensions/line/src/card-command.ts +++ b/extensions/line/src/card-command.ts @@ -1,5 +1,6 @@ -// Line plugin module implements card command behavior. import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +// Line plugin module implements card command behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; @@ -144,13 +145,14 @@ function parseCardArgs(argsStrInput: string): { const quotedRegex = /"([^"]*?)"/g; let match; while ((match = quotedRegex.exec(argsStr)) !== null) { - result.args.push(match[1]); + result.args.push(expectDefined(match[1], "quoted card argument capture")); } // Extract flags (--key value or --key "value") const flagRegex = /--(\w+)\s+(?:"([^"]*?)"|(\S+))/g; while ((match = flagRegex.exec(argsStr)) !== null) { - result.flags[match[1]] = match[2] ?? match[3]; + const key = expectDefined(match[1], "card flag name capture"); + result.flags[key] = expectDefined(match[2] ?? match[3], "card flag value capture"); } return result; diff --git a/extensions/line/src/flex-templates/media-control-cards.ts b/extensions/line/src/flex-templates/media-control-cards.ts index 03ea0b68c184..30c0bf17017a 100644 --- a/extensions/line/src/flex-templates/media-control-cards.ts +++ b/extensions/line/src/flex-templates/media-control-cards.ts @@ -511,8 +511,7 @@ export function createDeviceControlCard(params: { for (let i = 0; i < limitedControls.length; i += 2) { const rowButtons: FlexComponent[] = []; - for (let j = i; j < Math.min(i + 2, limitedControls.length); j++) { - const ctrl = limitedControls[j]; + for (const [offset, ctrl] of limitedControls.slice(i, i + 2).entries()) { const buttonLabel = ctrl.icon ? `${ctrl.icon} ${ctrl.label}` : ctrl.label; rowButtons.push({ @@ -525,7 +524,7 @@ export function createDeviceControlCard(params: { style: ctrl.style ?? "secondary", flex: 1, height: "sm", - margin: j > i ? "md" : undefined, + margin: offset > 0 ? "md" : undefined, } as FlexButton); } diff --git a/extensions/line/src/markdown-to-line.ts b/extensions/line/src/markdown-to-line.ts index 3da24d362bd1..795f84eea268 100644 --- a/extensions/line/src/markdown-to-line.ts +++ b/extensions/line/src/markdown-to-line.ts @@ -1,5 +1,6 @@ // Line plugin module implements markdown to line behavior. import type { messagingApi } from "@line/bot-sdk"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { stripMarkdown } from "openclaw/plugin-sdk/text-chunking"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { uriAction } from "./actions.js"; @@ -42,9 +43,9 @@ export function extractMarkdownTables(text: string): { const matches: { fullMatch: string; table: MarkdownTable }[] = []; while ((match = MARKDOWN_TABLE_REGEX.exec(text)) !== null) { - const fullMatch = match[0]; - const headerLine = match[1]; - const bodyLines = match[2]; + const fullMatch = expectDefined(match[0], "Markdown table match"); + const headerLine = expectDefined(match[1], "Markdown table header capture"); + const bodyLines = expectDefined(match[2], "Markdown table body capture"); const headers = parseTableRow(headerLine); const rows = bodyLines @@ -62,8 +63,7 @@ export function extractMarkdownTables(text: string): { } // Remove tables from text in reverse order to preserve indices - for (let i = matches.length - 1; i >= 0; i--) { - const { fullMatch, table } = matches[i]; + for (const { fullMatch, table } of matches.toReversed()) { tables.unshift(table); textWithoutTables = textWithoutTables.replace(fullMatch, ""); } @@ -203,9 +203,9 @@ export function extractCodeBlocks(text: string): { const matches: { fullMatch: string; block: CodeBlock }[] = []; while ((match = MARKDOWN_CODE_BLOCK_REGEX.exec(text)) !== null) { - const fullMatch = match[0]; + const fullMatch = expectDefined(match[0], "Markdown code block match"); const language = match[1] || undefined; - const code = match[2]; + const code = expectDefined(match[2], "Markdown code body capture"); matches.push({ fullMatch, @@ -214,8 +214,7 @@ export function extractCodeBlocks(text: string): { } // Remove code blocks in reverse order - for (let i = matches.length - 1; i >= 0; i--) { - const { fullMatch, block } = matches[i]; + for (const { fullMatch, block } of matches.toReversed()) { codeBlocks.unshift(block); textWithoutCode = textWithoutCode.replace(fullMatch, ""); } @@ -286,8 +285,8 @@ export function extractLinks(text: string): { links: MarkdownLink[]; textWithLin let match: RegExpExecArray | null; while ((match = MARKDOWN_LINK_REGEX.exec(text)) !== null) { links.push({ - text: match[1], - url: match[2], + text: expectDefined(match[1], "Markdown link text capture"), + url: expectDefined(match[2], "Markdown link URL capture"), }); } diff --git a/extensions/line/src/outbound.ts b/extensions/line/src/outbound.ts index fa165ac0fbb2..af2307abc24d 100644 --- a/extensions/line/src/outbound.ts +++ b/extensions/line/src/outbound.ts @@ -237,11 +237,11 @@ export const lineOutboundAdapter: NonNullable } if (chunks.length > 0) { - for (let i = 0; i < chunks.length; i += 1) { + for (const [i, chunk] of chunks.entries()) { const isLast = i === chunks.length - 1; if (isLast && hasQuickReplies) { await recordResult( - sendQuickReplies(to, chunks[i], quickReplies, { + sendQuickReplies(to, chunk, quickReplies, { verbose: false, cfg, accountId: accountId ?? undefined, @@ -249,7 +249,7 @@ export const lineOutboundAdapter: NonNullable ); } else { await recordResult( - sendText(to, chunks[i], { + sendText(to, chunk, { verbose: false, cfg, accountId: accountId ?? undefined, diff --git a/extensions/line/src/reply-chunks.ts b/extensions/line/src/reply-chunks.ts index 3648909bfebb..9d17a448e007 100644 --- a/extensions/line/src/reply-chunks.ts +++ b/extensions/line/src/reply-chunks.ts @@ -1,6 +1,7 @@ // Line plugin module implements reply chunks behavior. import type { messagingApi } from "@line/bot-sdk"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; type LineReplyMessage = messagingApi.TextMessage; @@ -35,7 +36,7 @@ export type SendLineReplyChunksParams = { export async function sendLineReplyChunks( params: SendLineReplyChunksParams, ): Promise<{ replyTokenUsed: boolean }> { - const hasQuickReplies = Boolean(params.quickReplies?.length); + const quickReplies = params.quickReplies?.length ? params.quickReplies : undefined; let replyTokenUsed = Boolean(params.replyTokenUsed); if (params.chunks.length === 0) { @@ -52,11 +53,11 @@ export async function sendLineReplyChunks( text: chunk, })); - if (hasQuickReplies && remaining.length === 0 && replyMessages.length > 0) { + if (quickReplies && remaining.length === 0 && replyMessages.length > 0) { const lastIndex = replyMessages.length - 1; replyMessages[lastIndex] = params.createTextMessageWithQuickReplies( - replyBatch[lastIndex], - params.quickReplies!, + expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), + quickReplies, ); } @@ -66,17 +67,15 @@ export async function sendLineReplyChunks( }); replyTokenUsed = true; - for (let i = 0; i < remaining.length; i += 1) { + for (const [i, chunk] of remaining.entries()) { const isLastChunk = i === remaining.length - 1; - if (isLastChunk && hasQuickReplies) { - await params.pushTextMessageWithQuickReplies( - params.to, - remaining[i], - params.quickReplies!, - { cfg: params.cfg, accountId: params.accountId }, - ); + if (isLastChunk && quickReplies) { + await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, { + cfg: params.cfg, + accountId: params.accountId, + }); } else { - await params.pushMessageLine(params.to, remaining[i], { + await params.pushMessageLine(params.to, chunk, { cfg: params.cfg, accountId: params.accountId, }); @@ -90,17 +89,15 @@ export async function sendLineReplyChunks( } } - for (let i = 0; i < params.chunks.length; i += 1) { + for (const [i, chunk] of params.chunks.entries()) { const isLastChunk = i === params.chunks.length - 1; - if (isLastChunk && hasQuickReplies) { - await params.pushTextMessageWithQuickReplies( - params.to, - params.chunks[i], - params.quickReplies!, - { cfg: params.cfg, accountId: params.accountId }, - ); + if (isLastChunk && quickReplies) { + await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, { + cfg: params.cfg, + accountId: params.accountId, + }); } else { - await params.pushMessageLine(params.to, params.chunks[i], { + await params.pushMessageLine(params.to, chunk, { cfg: params.cfg, accountId: params.accountId, }); diff --git a/extensions/line/src/reply-payload-transform.ts b/extensions/line/src/reply-payload-transform.ts index 6895582fdf2d..61862cd388d8 100644 --- a/extensions/line/src/reply-payload-transform.ts +++ b/extensions/line/src/reply-payload-transform.ts @@ -1,4 +1,5 @@ // Line plugin module implements reply payload transform behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; import { @@ -51,10 +52,21 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { } return base.join("&"); }; + const parseConfirmAction = (part: string): { label: string; data: string } => { + const colonIndex = part.indexOf(":"); + if (colonIndex === -1) { + return { label: part, data: normalizeLowercaseStringOrEmpty(part) }; + } + return { + label: part.slice(0, colonIndex).trim(), + data: part.slice(colonIndex + 1).trim(), + }; + }; const quickRepliesMatch = text.match(/\[\[quick_replies:\s*([^\]]+)\]\]/i); if (quickRepliesMatch) { - const options = normalizeStringEntries(quickRepliesMatch[1].split(",")); + const body = expectDefined(quickRepliesMatch[1], "quick replies directive body"); + const options = normalizeStringEntries(body.split(",")); if (options.length > 0) { lineData.quickReplies = [...(lineData.quickReplies || []), ...options]; } @@ -63,9 +75,13 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const locationMatch = text.match(/\[\[location:\s*([^\]]+)\]\]/i); if (locationMatch && !lineData.location) { - const parts = locationMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(locationMatch[1], "location directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 4) { - const [title, address, latStr, lonStr] = parts; + const title = expectDefined(parts[0], "location title field"); + const address = expectDefined(parts[1], "location address field"); + const latStr = expectDefined(parts[2], "location latitude field"); + const lonStr = expectDefined(parts[3], "location longitude field"); const latitude = parseStrictFiniteNumber(latStr); const longitude = parseStrictFiniteNumber(lonStr); if (latitude !== undefined && longitude !== undefined) { @@ -82,23 +98,22 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const confirmMatch = text.match(/\[\[confirm:\s*([^\]]+)\]\]/i); if (confirmMatch && !lineData.templateMessage) { - const parts = confirmMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(confirmMatch[1], "confirm directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 3) { - const [question, yesPart, noPart] = parts; - const [yesLabel, yesData] = yesPart.includes(":") - ? yesPart.split(":").map((s) => s.trim()) - : [yesPart, normalizeLowercaseStringOrEmpty(yesPart)]; - const [noLabel, noData] = noPart.includes(":") - ? noPart.split(":").map((s) => s.trim()) - : [noPart, normalizeLowercaseStringOrEmpty(noPart)]; + const question = expectDefined(parts[0], "confirm question field"); + const yesPart = expectDefined(parts[1], "confirm yes field"); + const noPart = expectDefined(parts[2], "confirm no field"); + const yesAction = parseConfirmAction(yesPart); + const noAction = parseConfirmAction(noPart); lineData.templateMessage = { type: "confirm", text: question, - confirmLabel: yesLabel, - confirmData: yesData, - cancelLabel: noLabel, - cancelData: noData, + confirmLabel: yesAction.label, + confirmData: yesAction.data, + cancelLabel: noAction.label, + cancelData: noAction.data, altText: question, }; } @@ -107,9 +122,12 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const buttonsMatch = text.match(/\[\[buttons:\s*([^\]]+)\]\]/i); if (buttonsMatch && !lineData.templateMessage) { - const parts = buttonsMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(buttonsMatch[1], "buttons directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 3) { - const [title, bodyText, actionsStr] = parts; + const title = expectDefined(parts[0], "buttons title field"); + const bodyText = expectDefined(parts[1], "buttons text field"); + const actionsStr = expectDefined(parts[2], "buttons actions field"); const actions = actionsStr.split(",").map((actionStr) => { const trimmed = actionStr.trim(); @@ -160,9 +178,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const mediaPlayerMatch = text.match(/\[\[media_player:\s*([^\]]+)\]\]/i); if (mediaPlayerMatch && !lineData.flexMessage) { - const parts = mediaPlayerMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(mediaPlayerMatch[1], "media player directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 1) { - const [title, artist, source, imageUrl, statusStr] = parts; + const title = expectDefined(parts[0], "media player title field"); + const [, artist, source, imageUrl, statusStr] = parts; const isPlaying = normalizeLowercaseStringOrEmpty(statusStr) === "playing"; const validImageUrl = imageUrl?.startsWith("https://") ? imageUrl : undefined; const deviceKey = toSlug(source || title || "media"); @@ -190,9 +210,14 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const eventMatch = text.match(/\[\[event:\s*([^\]]+)\]\]/i); if (eventMatch && !lineData.flexMessage) { - const parts = eventMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(eventMatch[1], "event directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 2) { - const [title, date, time, location, description] = parts; + const title = expectDefined(parts[0], "event title field"); + const date = expectDefined(parts[1], "event date field"); + const time = parts[2]; + const location = parts[3]; + const description = parts[4]; const card = createEventCard({ title: title || "Event", @@ -212,9 +237,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const appleTvMatch = text.match(/\[\[appletv_remote:\s*([^\]]+)\]\]/i); if (appleTvMatch && !lineData.flexMessage) { - const parts = appleTvMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(appleTvMatch[1], "Apple TV directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 1) { - const [deviceName, status] = parts; + const deviceName = expectDefined(parts[0], "Apple TV device name field"); + const [, status] = parts; const deviceKey = toSlug(deviceName || "apple_tv"); const card = createAppleTvRemoteCard({ @@ -246,9 +273,11 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const agendaMatch = text.match(/\[\[agenda:\s*([^\]]+)\]\]/i); if (agendaMatch && !lineData.flexMessage) { - const parts = agendaMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(agendaMatch[1], "agenda directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 2) { - const [title, eventsStr] = parts; + const title = expectDefined(parts[0], "agenda title field"); + const eventsStr = expectDefined(parts[1], "agenda events field"); const events = eventsStr.split(",").map((eventStr) => { const trimmed = eventStr.trim(); const colonIdx = trimmed.lastIndexOf(":"); @@ -276,13 +305,17 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const deviceMatch = text.match(/\[\[device:\s*([^\]]+)\]\]/i); if (deviceMatch && !lineData.flexMessage) { - const parts = deviceMatch[1].split("|").map((s) => s.trim()); + const body = expectDefined(deviceMatch[1], "device directive body"); + const parts = body.split("|").map((s) => s.trim()); if (parts.length >= 1) { - const [deviceName, deviceType, status, controlsStr] = parts; + const deviceName = expectDefined(parts[0], "device name field"); + const [, deviceType, status, controlsStr] = parts; const deviceKey = toSlug(deviceName || "device"); const controls = controlsStr ? controlsStr.split(",").map((ctrlStr) => { - const [label, data] = ctrlStr.split(":").map((s) => s.trim()); + const controlParts = ctrlStr.split(":").map((s) => s.trim()); + const label = expectDefined(controlParts[0], "device control label"); + const data = controlParts[1]; const action = data || normalizeLowercaseStringOrEmpty(label).replace(/\s+/g, "_"); return { label, data: lineActionData(action, { "line.device": deviceKey }) }; }) diff --git a/extensions/lmstudio/src/models.ts b/extensions/lmstudio/src/models.ts index 0f2eed6f2f96..b4cb9fd8a421 100644 --- a/extensions/lmstudio/src/models.ts +++ b/extensions/lmstudio/src/models.ts @@ -303,11 +303,15 @@ function normalizeConfiguredReasoningEffortMap(value: unknown): Record [key.trim(), typeof mapped === "string" ? mapped.trim() : ""]) - .filter(([key, mapped]) => key.length > 0 && mapped.length > 0), - ); + const entries: Array<[string, string]> = []; + for (const [key, mapped] of Object.entries(value)) { + const normalizedKey = key.trim(); + const normalizedValue = typeof mapped === "string" ? mapped.trim() : ""; + if (normalizedKey && normalizedValue) { + entries.push([normalizedKey, normalizedValue]); + } + } + const normalized = Object.fromEntries(entries); return Object.keys(normalized).length > 0 ? normalized : undefined; } diff --git a/extensions/logbook/src/analyze.test.ts b/extensions/logbook/src/analyze.test.ts index 1def6e1e436f..7d1b3d4d5000 100644 --- a/extensions/logbook/src/analyze.test.ts +++ b/extensions/logbook/src/analyze.test.ts @@ -235,6 +235,8 @@ describe("selectBatchFrames", () => { describe("sampleFrames", () => { it("keeps small sets and evenly samples large ones", () => { expect(sampleFrames([1, 2, 3], 16)).toEqual([1, 2, 3]); + expect(sampleFrames([1, 2, 3], 1)).toEqual([1]); + expect(sampleFrames([1, 2, 3], 0)).toEqual([]); const sampled = sampleFrames( Array.from({ length: 100 }, (_, i) => i), 16, diff --git a/extensions/logbook/src/analyze.ts b/extensions/logbook/src/analyze.ts index c3defc6cd175..0ac0c9a27095 100644 --- a/extensions/logbook/src/analyze.ts +++ b/extensions/logbook/src/analyze.ts @@ -1,5 +1,6 @@ // Logbook analysis pipeline: frames -> observations -> revised timeline cards. // Pure parsing/validation lives here so tests can cover it without the SDK. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { CARD_CATEGORIES } from "./prompts.js"; import { dayKeyFor } from "./store.js"; import type { LogbookCard, LogbookCardDraft, LogbookDistraction } from "./types.js"; @@ -224,20 +225,28 @@ export function parseCardsJson(params: { return { ok: false, error: "Output contained no valid cards." }; } const sorted = drafts.toSorted((a, b) => a.startMs - b.startMs); - for (let i = 1; i < sorted.length; i += 1) { - const overlapMs = sorted[i - 1].endMs - sorted[i].startMs; + const normalized: LogbookCardDraft[] = []; + for (const current of sorted) { + const previous = normalized.at(-1); + if (!previous) { + normalized.push(current); + continue; + } + const overlapMs = previous.endMs - current.startMs; if (overlapMs > 60 * 1000) { return { ok: false, - error: `Cards ${i - 1} and ${i} overlap by ${Math.round(overlapMs / 60000)} minutes; adjacent cards must meet cleanly.`, + error: `Cards ${normalized.length - 1} and ${normalized.length} overlap by ${Math.round(overlapMs / 60000)} minutes; adjacent cards must meet cleanly.`, }; } if (overlapMs > 0) { // Trim sub-minute overlaps instead of round-tripping to the model again. - sorted[i] = { ...sorted[i], startMs: sorted[i - 1].endMs }; + normalized.push({ ...current, startMs: previous.endMs }); + } else { + normalized.push(current); } } - return { ok: true, drafts: sorted }; + return { ok: true, drafts: normalized }; } /** Sub-minute slack so minute-rounded model times do not fail coverage checks. */ @@ -329,7 +338,7 @@ export function selectBatchFrames(params: { if (params.frames.length === 0) { return null; } - const first = params.frames[0]; + const first = expectDefined(params.frames[0], "first pending Logbook frame"); const firstDay = dayKeyFor(first.capturedAtMs); const nextDayStart = new Date(first.capturedAtMs); nextDayStart.setHours(24, 0, 0, 0); @@ -358,7 +367,7 @@ export function selectBatchFrames(params: { if (selected.length === 0) { return null; } - const last = selected[selected.length - 1]; + const last = expectDefined(selected.at(-1), "last selected Logbook frame"); // Only close a batch once its window has elapsed (or a gap/midnight ended // it), so a window in progress keeps accumulating frames; `force` closes an // in-progress window immediately (analyze now). @@ -380,13 +389,19 @@ export function selectBatchFrames(params: { /** Evenly samples frames so a batch stays within the per-call image budget. */ export function sampleFrames(frames: T[], max: number): T[] { + if (max <= 0) { + return []; + } if (frames.length <= max) { return frames; } + if (max === 1) { + return [expectDefined(frames[0], "first Logbook frame sample")]; + } const sampled: T[] = []; const step = (frames.length - 1) / (max - 1); for (let i = 0; i < max; i += 1) { - sampled.push(frames[Math.round(i * step)]); + sampled.push(expectDefined(frames[Math.round(i * step)], "sampled Logbook frame")); } return [...new Set(sampled)]; } @@ -400,7 +415,7 @@ export function pickKeyframeId( return undefined; } const midpoint = card.startMs + (card.endMs - card.startMs) / 2; - let best = frames[0]; + let best = expectDefined(frames[0], "first Logbook keyframe candidate"); for (const frame of frames) { if (Math.abs(frame.capturedAtMs - midpoint) < Math.abs(best.capturedAtMs - midpoint)) { best = frame; diff --git a/extensions/matrix/src/env-vars.ts b/extensions/matrix/src/env-vars.ts index bb7552e96cbf..d270bfeefc5b 100644 --- a/extensions/matrix/src/env-vars.ts +++ b/extensions/matrix/src/env-vars.ts @@ -84,7 +84,11 @@ export function listMatrixEnvAccountIds(env: NodeJS.ProcessEnv = process.env): s if (!match) { continue; } - const accountId = decodeMatrixEnvAccountToken(match[1]); + const encodedAccountId = match[1]; + if (!encodedAccountId) { + continue; + } + const accountId = decodeMatrixEnvAccountToken(encodedAccountId); if (accountId) { ids.add(accountId); } diff --git a/extensions/matrix/src/matrix/format.ts b/extensions/matrix/src/matrix/format.ts index 21ab0338dee9..3ecae3aa3e94 100644 --- a/extensions/matrix/src/matrix/format.ts +++ b/extensions/matrix/src/matrix/format.ts @@ -349,8 +349,14 @@ function compactLooseListTokens(tokens: MarkdownToken[]): void { item.immediateParagraphOpenIndexes.length === 1 && item.immediateParagraphCloseIndexes.length === 1 ) { - tokens[item.immediateParagraphOpenIndexes[0]].hidden = true; - tokens[item.immediateParagraphCloseIndexes[0]].hidden = true; + const openIndex = item.immediateParagraphOpenIndexes[0]; + const closeIndex = item.immediateParagraphCloseIndexes[0]; + const openToken = openIndex === undefined ? undefined : tokens[openIndex]; + const closeToken = closeIndex === undefined ? undefined : tokens[closeIndex]; + if (openToken && closeToken) { + openToken.hidden = true; + closeToken.hidden = true; + } } continue; } diff --git a/extensions/matrix/src/matrix/monitor/location.ts b/extensions/matrix/src/matrix/monitor/location.ts index 09fcc9aa8aa5..b06a4cb408e0 100644 --- a/extensions/matrix/src/matrix/monitor/location.ts +++ b/extensions/matrix/src/matrix/monitor/location.ts @@ -37,6 +37,9 @@ function parseGeoUri(value: string): GeoUriParams | null { } const payload = trimmed.slice(4); const [coordsPart, ...paramParts] = payload.split(";"); + if (!coordsPart) { + return null; + } const coords = coordsPart.split(","); if (coords.length < 2) { return null; diff --git a/extensions/matrix/src/matrix/sdk/event-helpers.ts b/extensions/matrix/src/matrix/sdk/event-helpers.ts index 06018408a27d..206588668683 100644 --- a/extensions/matrix/src/matrix/sdk/event-helpers.ts +++ b/extensions/matrix/src/matrix/sdk/event-helpers.ts @@ -74,9 +74,14 @@ export function parseMxc(url: string): { server: string; mediaId: string } | nul if (!match) { return null; } + const server = match[1]; + const mediaId = match[2]; + if (!server || !mediaId) { + return null; + } return { - server: match[1], - mediaId: match[2], + server, + mediaId, }; } diff --git a/extensions/matrix/src/matrix/sdk/verification-manager.ts b/extensions/matrix/src/matrix/sdk/verification-manager.ts index 41d1ce5dc580..f145faa2c390 100644 --- a/extensions/matrix/src/matrix/sdk/verification-manager.ts +++ b/extensions/matrix/src/matrix/sdk/verification-manager.ts @@ -1,10 +1,11 @@ -// Matrix plugin module implements verification manager behavior. import { VerificationPhase, VerificationRequestEvent, VerifierEvent, } from "matrix-js-sdk/lib/crypto-api/verification.js"; import { VerificationMethod } from "matrix-js-sdk/lib/types.js"; +// Matrix plugin module implements verification manager behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { resolveDateTimestampMs, resolveTimestampMsToIsoString, @@ -341,7 +342,7 @@ export class MatrixVerificationManager { return txId === id; }); if (transactionMatches.length === 1) { - return transactionMatches[0]; + return expectDefined(transactionMatches[0], "single Matrix verification session"); } if (transactionMatches.length > 1) { throw new Error( diff --git a/extensions/matrix/src/setup-contract.ts b/extensions/matrix/src/setup-contract.ts index 04a8fb1cd730..6bb84d623c33 100644 --- a/extensions/matrix/src/setup-contract.ts +++ b/extensions/matrix/src/setup-contract.ts @@ -78,7 +78,10 @@ export function resolveSingleAccountPromotionTarget(params: { ([accountId, value]) => accountId && typeof value === "object" && value, ); if (namedAccounts.length === 1) { - return namedAccounts[0][0]; + const onlyAccount = namedAccounts[0]; + if (onlyAccount) { + return onlyAccount[0]; + } } if ( namedAccounts.length > 1 && diff --git a/extensions/mattermost/src/mattermost/client.ts b/extensions/mattermost/src/mattermost/client.ts index 7ccb13c3daea..befb5e552f73 100644 --- a/extensions/mattermost/src/mattermost/client.ts +++ b/extensions/mattermost/src/mattermost/client.ts @@ -507,7 +507,11 @@ function isRetryableError(error: Error): boolean { if (!clientErrorMatch) { continue; } - const statusCode = Number.parseInt(clientErrorMatch[1], 10); + const statusCodeText = clientErrorMatch[1]; + if (!statusCodeText) { + continue; + } + const statusCode = Number.parseInt(statusCodeText, 10); if (statusCode >= 400 && statusCode < 500) { return false; } diff --git a/extensions/mattermost/src/mattermost/directory.ts b/extensions/mattermost/src/mattermost/directory.ts index 1dd241e96055..c69244f23059 100644 --- a/extensions/mattermost/src/mattermost/directory.ts +++ b/extensions/mattermost/src/mattermost/directory.ts @@ -134,6 +134,9 @@ export async function listMattermostDirectoryPeers( // All bots see the same user list, so one client suffices (unlike channels // where private channel membership varies per bot). const client = clients[0]; + if (!client) { + return []; + } try { const me = await fetchMattermostMe(client); const teams = await client.request<{ id: string }[]>("/users/me/teams"); @@ -141,7 +144,11 @@ export async function listMattermostDirectoryPeers( return []; } // Uses first team — multi-team setups may need iteration in the future - const teamId = teams[0].id; + const team = teams[0]; + if (!team) { + return []; + } + const teamId = team.id; const q = normalizeLowercaseStringOrEmpty(params.query); let users: MattermostUser[]; diff --git a/extensions/mattermost/src/mattermost/model-picker.ts b/extensions/mattermost/src/mattermost/model-picker.ts index 28e945986b97..29e7c2a967e8 100644 --- a/extensions/mattermost/src/mattermost/model-picker.ts +++ b/extensions/mattermost/src/mattermost/model-picker.ts @@ -45,7 +45,11 @@ function splitModelRef(modelRef?: string | null): { provider: string; model: str if (!match) { return null; } - const provider = normalizeProviderId(match[1]); + const rawProvider = match[1]; + if (!rawProvider) { + return null; + } + const provider = normalizeProviderId(rawProvider); // Mattermost copy should normalize accidental whitespace around the model. const model = normalizeOptionalString(match[2]); if (!provider || !model) { diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 3d5560d00ebd..9f8b2db16a32 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -522,7 +522,11 @@ function buildMattermostAttachmentPlaceholder(mediaList: MattermostMediaInfo[]): return ""; } if (mediaList.length === 1) { - const kind = mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind; + const media = mediaList[0]; + if (!media) { + return ""; + } + const kind = media.kind === "unknown" ? "document" : media.kind; return ``; } const allImages = mediaList.every((media) => media.kind === "image"); diff --git a/extensions/mattermost/src/mattermost/slash-state.ts b/extensions/mattermost/src/mattermost/slash-state.ts index 1dabc47817e5..68affb1a19e2 100644 --- a/extensions/mattermost/src/mattermost/slash-state.ts +++ b/extensions/mattermost/src/mattermost/slash-state.ts @@ -102,11 +102,15 @@ export function resolveSlashHandlerForToken(token: string): SlashHandlerMatch { return { kind: "none" }; } if (matches.length === 1) { + const match = matches[0]; + if (!match) { + return { kind: "none" }; + } return { kind: "single", source: "token", - handler: matches[0].handler, - accountIds: [matches[0].accountId], + handler: match.handler, + accountIds: [match.accountId], }; } @@ -146,11 +150,15 @@ export function resolveSlashHandlerForCommand(params: { return { kind: "none" }; } if (matches.length === 1) { + const match = matches[0]; + if (!match) { + return { kind: "none" }; + } return { kind: "single", source: "command", - handler: matches[0].handler, - accountIds: [matches[0].accountId], + handler: match.handler, + accountIds: [match.accountId], }; } @@ -301,8 +309,8 @@ export function registerSlashCommandRoute(api: OpenClawPluginApi) { // If there's only one active account (common case), route directly. if (accountStates.size === 1) { - const [, state] = [...accountStates.entries()][0]; - if (!state.handler) { + const state = accountStates.values().next().value; + if (!state?.handler) { res.statusCode = 503; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end( diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index 79385561a80b..1df7e43bb0ef 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -3,6 +3,7 @@ import { createHash } from "node:crypto"; import type { Dirent } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; import { buildSessionEntry, @@ -954,13 +955,13 @@ async function collectSessionIngestionBatches(params: { }; const cursorAtEnd = previous !== undefined && previous.lastContentLine >= previous.lineCount; const unchanged = - Boolean(previous) && + previous !== undefined && previous.mtimeMs === fingerprint.mtimeMs && previous.size === fingerprint.size && previous.contentHash.length > 0 && cursorAtEnd; if (unchanged) { - nextFiles[stateKey] = previous!; + nextFiles[stateKey] = expectDefined(previous, "unchanged dreaming file state"); continue; } diff --git a/extensions/memory-core/src/dreaming.ts b/extensions/memory-core/src/dreaming.ts index 85424547839d..7467c805b34e 100644 --- a/extensions/memory-core/src/dreaming.ts +++ b/extensions/memory-core/src/dreaming.ts @@ -1,5 +1,6 @@ -// Memory Core plugin module implements dreaming behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +// Memory Core plugin module implements dreaming behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { DEFAULT_MEMORY_DREAMING_FREQUENCY as DEFAULT_MEMORY_DREAMING_CRON_EXPR, DEFAULT_MEMORY_DEEP_DREAMING_LIMIT as DEFAULT_MEMORY_DREAMING_LIMIT, @@ -481,7 +482,8 @@ export async function reconcileShortTermDreamingCronJob(params: { } } - const patch = buildManagedDreamingPatch(primary, desired); + const managedPrimary = expectDefined(primary, "non-empty managed dreaming jobs"); + const patch = buildManagedDreamingPatch(managedPrimary, desired); if (!patch) { if (removed > 0) { params.logger.info("memory-core: pruned duplicate managed dreaming cron jobs."); @@ -489,7 +491,7 @@ export async function reconcileShortTermDreamingCronJob(params: { return { status: "noop", removed }; } - await cron.update(primary.id, patch); + await cron.update(managedPrimary.id, patch); params.logger.info("memory-core: updated managed dreaming cron job."); return { status: "updated", removed }; } diff --git a/extensions/memory-core/src/memory/manager-embedding-ops.ts b/extensions/memory-core/src/memory/manager-embedding-ops.ts index 99d700bb7081..5dd1e3ebb58e 100644 --- a/extensions/memory-core/src/memory/manager-embedding-ops.ts +++ b/extensions/memory-core/src/memory/manager-embedding-ops.ts @@ -1,6 +1,7 @@ // Memory Core plugin module implements manager embedding ops behavior. import fs from "node:fs/promises"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { enforceEmbeddingMaxInputTokens, hasNonTextEmbeddingParts, @@ -305,7 +306,10 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps { } protected computeProviderKey(): string { - return this.resolveProviderIndexIdentities()[0].providerKey; + return expectDefined( + this.resolveProviderIndexIdentities().at(0), + "primary memory provider identity", + ).providerKey; } protected resolveProviderIndexIdentities(): MemoryIndexProviderIdentity[] { @@ -759,8 +763,7 @@ export abstract class MemoryManagerEmbeddingOps extends MemoryManagerSyncOps { const now = Date.now(); runSqliteImmediateTransactionSync(this.db, () => { this.clearIndexedFileData(entry.path, source); - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; + for (const [i, chunk] of chunks.entries()) { const embedding = embeddings[i] ?? []; const id = hashText( `${source}:${entry.path}:${chunk.startLine}:${chunk.endLine}:${chunk.hash}:${model}`, diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index fd441319b1f7..7a1f5ccad141 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -7,6 +7,7 @@ import path from "node:path"; import readline from "node:readline"; import chokidar, { type FSWatcher } from "chokidar"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { withFileLock } from "openclaw/plugin-sdk/file-lock"; import { createSubsystemLogger, @@ -1265,7 +1266,10 @@ export class QmdMemoryManager implements MemorySearchManager { } const collectionLine = /^\s*([a-z0-9._-]+)\s+\(qmd:\/\/[^)]+\)\s*$/i.exec(line); if (collectionLine) { - currentName = collectionLine[1]; + currentName = collectionLine[1] ?? null; + if (currentName === null) { + continue; + } if (!listed.has(currentName)) { listed.set(currentName, {}); } @@ -1276,7 +1280,10 @@ export class QmdMemoryManager implements MemorySearchManager { } const bareNameLine = /^\s*([a-z0-9._-]+)\s*$/i.exec(line); if (bareNameLine && !line.includes(":")) { - currentName = bareNameLine[1]; + currentName = bareNameLine[1] ?? null; + if (currentName === null) { + continue; + } if (!listed.has(currentName)) { listed.set(currentName, {}); } @@ -1287,15 +1294,23 @@ export class QmdMemoryManager implements MemorySearchManager { } const patternLine = /^\s*(?:pattern|mask)\s*:\s*(.+?)\s*$/i.exec(line); if (patternLine) { + const pattern = patternLine[1]; + if (pattern === undefined) { + continue; + } const existing = listed.get(currentName) ?? {}; - existing.pattern = patternLine[1].trim(); + existing.pattern = pattern.trim(); listed.set(currentName, existing); continue; } const pathLine = /^\s*path\s*:\s*(.+?)\s*$/i.exec(line); if (pathLine) { + const listedPath = pathLine[1]; + if (listedPath === undefined) { + continue; + } const existing = listed.get(currentName) ?? {}; - existing.path = pathLine[1].trim(); + existing.path = listedPath.trim(); listed.set(currentName, existing); } } @@ -1313,12 +1328,18 @@ export class QmdMemoryManager implements MemorySearchManager { for (const rawLine of output.split(/\r?\n/)) { const pathMatch = /^\s*Path\s*:\s*(.+?)\s*$/.exec(rawLine); if (pathMatch) { - result.path = pathMatch[1].trim(); + const shownPath = pathMatch[1]; + if (shownPath !== undefined) { + result.path = shownPath.trim(); + } continue; } const patternMatch = /^\s*Pattern\s*:\s*(.+?)\s*$/.exec(rawLine); if (patternMatch) { - result.pattern = patternMatch[1].trim(); + const shownPattern = patternMatch[1]; + if (shownPattern !== undefined) { + result.pattern = shownPattern.trim(); + } } } return result; @@ -3094,7 +3115,8 @@ export class QmdMemoryManager implements MemorySearchManager { .prepare("SELECT path FROM documents WHERE collection = ? AND path = ? AND active = 1") .all(trimmedCollection, exactPath) as Array<{ path: string }>; if (exactRows.length > 0) { - return this.toDocLocation(trimmedCollection, exactRows[0].path); + const exactRow = expectDefined(exactRows.at(0), "single exact QMD document row"); + return this.toDocLocation(trimmedCollection, exactRow.path); } rows = db .prepare("SELECT path FROM documents WHERE collection = ? AND active = 1") @@ -3113,7 +3135,8 @@ export class QmdMemoryManager implements MemorySearchManager { if (matches.length !== 1) { return null; } - return this.toDocLocation(trimmedCollection, matches[0].path); + const match = expectDefined(matches.at(0), "single preferred QMD document match"); + return this.toDocLocation(trimmedCollection, match.path); } private normalizeDocHints(hints?: { preferredCollection?: string; preferredFile?: string }): { diff --git a/extensions/memory-core/src/memory/tokenize.ts b/extensions/memory-core/src/memory/tokenize.ts index 15253966b89c..9f90595c3759 100644 --- a/extensions/memory-core/src/memory/tokenize.ts +++ b/extensions/memory-core/src/memory/tokenize.ts @@ -34,17 +34,18 @@ export function tokenize(text: string): Set { // Track CJK characters with their original positions const chars = Array.from(lower); const cjkData: { char: string; index: number }[] = []; - for (let i = 0; i < chars.length; i++) { - if (CJK_RE.test(chars[i])) { - cjkData.push({ char: chars[i], index: i }); + for (const [i, char] of chars.entries()) { + if (CJK_RE.test(char)) { + cjkData.push({ char, index: i }); } } // Build bigrams only from originally adjacent CJK characters const bigrams: string[] = []; - for (let i = 0; i < cjkData.length - 1; i++) { - if (cjkData[i + 1].index === cjkData[i].index + 1) { - bigrams.push(cjkData[i].char + cjkData[i + 1].char); + for (const [i, next] of cjkData.slice(1).entries()) { + const previous = cjkData[i]; + if (previous !== undefined && next.index === previous.index + 1) { + bigrams.push(previous.char + next.char); } } diff --git a/extensions/memory-core/src/rem-evidence.ts b/extensions/memory-core/src/rem-evidence.ts index 6e4926ef9ec1..04f0bedbf670 100644 --- a/extensions/memory-core/src/rem-evidence.ts +++ b/extensions/memory-core/src/rem-evidence.ts @@ -614,8 +614,13 @@ function splitSubjectLeadClaim(text: string): string[] { if (!match?.groups) { return [text]; } - const subject = normalizeWhitespace(match.groups.subject); - const rest = normalizeWhitespace(match.groups.rest); + const rawSubject = match.groups.subject; + const rawRest = match.groups.rest; + if (rawSubject === undefined || rawRest === undefined) { + return [text]; + } + const subject = normalizeWhitespace(rawSubject); + const rest = normalizeWhitespace(rawRest); if (!subject || !rest) { return [text]; } diff --git a/extensions/memory-core/src/short-term-promotion.ts b/extensions/memory-core/src/short-term-promotion.ts index 0b898b4f3556..2972635bf02d 100644 --- a/extensions/memory-core/src/short-term-promotion.ts +++ b/extensions/memory-core/src/short-term-promotion.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue"; import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files"; import { @@ -563,7 +564,9 @@ function calculateConsolidationComponent(recallDays: string[]): number { if (parsed.length <= 1) { return 0.2; } - const spanDays = Math.max(0, (parsed.at(-1)! - parsed[0]) / DAY_MS); + const first = expectDefined(parsed.at(0), "multiple parsed recall days"); + const last = expectDefined(parsed.at(-1), "multiple parsed recall days"); + const spanDays = Math.max(0, (last - first) / DAY_MS); const spacing = clampScore(Math.log1p(parsed.length - 1) / Math.log1p(4)); const span = clampScore(spanDays / 7); return clampScore(0.55 * spacing + 0.45 * span); @@ -1164,7 +1167,7 @@ function trimDreamingStatsEntries( for (const entry of entries) { let insertAt = selected.length; for (let index = 0; index < selected.length; index += 1) { - if (compare(entry, selected[index]) < 0) { + if (compare(entry, expectDefined(selected[index], "selected dreaming stats index")) < 0) { insertAt = index; break; } diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts index f3a8f0903e35..366aaeacefdc 100644 --- a/extensions/memory-lancedb/index.ts +++ b/extensions/memory-lancedb/index.ts @@ -16,6 +16,7 @@ import { } from "openclaw/plugin-sdk/channel-actions"; import { BUNDLED_CHAT_CHANNEL_ENVELOPE_PREFIXES } from "openclaw/plugin-sdk/chat-channel-ids"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import type { MemoryEmbeddingProvider } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings"; import { MESSAGE_TOOL_DELIVERY_HINTS } from "openclaw/plugin-sdk/message-tool-delivery-hints"; @@ -955,7 +956,7 @@ function stripEnvelopeBodySenderPrefix(body: string, headerInside: string): stri if (!match) { return body; } - const label = match[1]; + const label = expectDefined(match[1], "envelope body sender capture"); if (label === ENVELOPE_BODY_SELF_PREFIX || label === ENVELOPE_BODY_DIRECT_PREFIX) { return body.slice(match[0].length); } @@ -1701,11 +1702,12 @@ export default definePluginEntry({ }; } - if (results.length === 1 && results[0].score > 0.9) { - await db.delete(results[0].entry.id); + const singleResult = results.length === 1 ? results[0] : undefined; + if (singleResult && singleResult.score > 0.9) { + await db.delete(singleResult.entry.id); return { - content: [{ type: "text", text: `Forgotten: "${results[0].entry.text}"` }], - details: { action: "deleted", id: results[0].entry.id }, + content: [{ type: "text", text: `Forgotten: "${singleResult.entry.text}"` }], + details: { action: "deleted", id: singleResult.entry.id }, }; } diff --git a/extensions/memory-wiki/src/markdown.ts b/extensions/memory-wiki/src/markdown.ts index 9225aa4d9256..932eb2ff0788 100644 --- a/extensions/memory-wiki/src/markdown.ts +++ b/extensions/memory-wiki/src/markdown.ts @@ -199,7 +199,11 @@ export function parseWikiMarkdown(content: string): ParsedWikiMarkdown { if (!match) { return { hasFrontmatter: false, frontmatter: {}, body: content }; } - const parsed = YAML.parse(match[1]) as unknown; + const frontmatter = match[1]; + if (frontmatter === undefined) { + return { hasFrontmatter: false, frontmatter: {}, body: content }; + } + const parsed: unknown = YAML.parse(frontmatter); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { // Every writer spreads this value back into YAML. Reject non-mapping roots // so an edit cannot silently replace scalar or sequence frontmatter. diff --git a/extensions/memory-wiki/src/query.ts b/extensions/memory-wiki/src/query.ts index d5711be72903..a8931e712ae9 100644 --- a/extensions/memory-wiki/src/query.ts +++ b/extensions/memory-wiki/src/query.ts @@ -831,8 +831,9 @@ function buildDigestCandidatePaths(params: { (left, right) => scoreDigestClaimMatch(right, queryLower) - scoreDigestClaimMatch(left, queryLower), ); - if (matchingClaims.length > 0) { - score += scoreDigestClaimMatch(matchingClaims[0], queryLower); + const [bestMatchingClaim] = matchingClaims; + if (bestMatchingClaim) { + score += scoreDigestClaimMatch(bestMatchingClaim, queryLower); score += Math.min(10, (matchingClaims.length - 1) * 2); } score += scoreDigestSearchModeBoost({ @@ -936,8 +937,9 @@ function scorePage(page: QueryableWikiPage, query: string, mode: WikiSearchMode) queryLower, }); const matchingClaims = getMatchingClaims(page, queryLower); - if (matchingClaims.length > 0) { - score += rankClaimMatch(page, matchingClaims[0], queryLower, queryTokens); + const [bestMatchingClaim] = matchingClaims; + if (bestMatchingClaim) { + score += rankClaimMatch(page, bestMatchingClaim, queryLower, queryTokens); score += Math.min(10, (matchingClaims.length - 1) * 2); } score += scorePageSearchModeBoost({ diff --git a/extensions/meta/models.ts b/extensions/meta/models.ts index ad373b2c6d1c..f4981183b534 100644 --- a/extensions/meta/models.ts +++ b/extensions/meta/models.ts @@ -1,6 +1,7 @@ /** * Meta model catalog helpers derived from the plugin manifest. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; import manifest from "./openclaw.plugin.json" with { type: "json" }; @@ -28,5 +29,5 @@ export function buildMetaModelDefinition( providerId: "meta", catalog: { ...META_MANIFEST_CATALOG, models: [model] }, }); - return providerConfig.models[0]; + return expectDefined(providerConfig.models.at(0), "normalized Meta manifest model"); } diff --git a/extensions/microsoft-foundry/image-generation-provider.ts b/extensions/microsoft-foundry/image-generation-provider.ts index 513f4b5e75cf..d86fdd15a399 100644 --- a/extensions/microsoft-foundry/image-generation-provider.ts +++ b/extensions/microsoft-foundry/image-generation-provider.ts @@ -1,6 +1,7 @@ -// Microsoft Foundry image provider routes MAI image deployments to the MAI API. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/core"; +// Microsoft Foundry image provider routes MAI image deployments to the MAI API. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { ImageGenerationProvider, ImageGenerationRequest, @@ -351,7 +352,7 @@ export function buildMicrosoftFoundryImageGenerationProvider(): ImageGenerationP })(), body: buildEditFormData({ req, - image: inputImages[0], + image: expectDefined(inputImages[0], "Microsoft Foundry edit source image"), model, }), timeoutMs, diff --git a/extensions/microsoft-foundry/onboard.ts b/extensions/microsoft-foundry/onboard.ts index d11d41030614..6dee8dead522 100644 --- a/extensions/microsoft-foundry/onboard.ts +++ b/extensions/microsoft-foundry/onboard.ts @@ -1,6 +1,7 @@ -// Microsoft Foundry setup module handles plugin onboarding behavior. import type { ProviderAuthContext } from "openclaw/plugin-sdk/core"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +// Microsoft Foundry setup module handles plugin onboarding behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { @@ -147,7 +148,7 @@ export async function selectFoundryResource( throw new Error(buildCreateFoundryHint(selectedSub)); } if (resources.length === 1) { - const only = resources[0]; + const only = expectDefined(resources[0], "single Microsoft Foundry resource"); await ctx.prompter.note( `Using ${only.kind === "AIServices" ? "Azure AI Foundry" : "Azure OpenAI"} resource: ${only.accountName}`, "Foundry Resource", @@ -167,7 +168,10 @@ export async function selectFoundryResource( .join(" | "), })), }); - return resources.find((resource) => resource.id === selectedResourceId) ?? resources[0]; + return ( + resources.find((resource) => resource.id === selectedResourceId) ?? + expectDefined(resources[0], "fallback Microsoft Foundry resource") + ); } export async function selectFoundryDeployment( @@ -185,7 +189,7 @@ export async function selectFoundryDeployment( ); } if (supported.length === 1) { - const only = supported[0]; + const only = expectDefined(supported[0], "single Microsoft Foundry deployment"); await ctx.prompter.note(`Using deployment: ${only.name}`, "Model Deployment"); return { selected: only, supported }; } @@ -200,7 +204,8 @@ export async function selectFoundryDeployment( })), }); const selected = - supported.find((deployment) => deployment.name === selectedDeploymentName) ?? supported[0]; + supported.find((deployment) => deployment.name === selectedDeploymentName) ?? + expectDefined(supported[0], "fallback Microsoft Foundry deployment"); return { selected, supported }; } diff --git a/extensions/migrate-hermes/helpers.ts b/extensions/migrate-hermes/helpers.ts index 49f39beaf148..31cb31685912 100644 --- a/extensions/migrate-hermes/helpers.ts +++ b/extensions/migrate-hermes/helpers.ts @@ -56,7 +56,11 @@ export function parseEnv(content: string | undefined): Record { continue; } const key = match[1]; - let value = match[2] ?? ""; + const rawValue = match[2]; + if (!key || rawValue === undefined) { + continue; + } + let value = rawValue; if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) diff --git a/extensions/minimax/image-generation-provider.ts b/extensions/minimax/image-generation-provider.ts index e006b6f9a164..48ed8de83829 100644 --- a/extensions/minimax/image-generation-provider.ts +++ b/extensions/minimax/image-generation-provider.ts @@ -160,8 +160,8 @@ function buildMinimaxImageProvider(providerId: string): ImageGenerationProvider } // Map input images to subject_reference for image-to-image generation - if (req.inputImages && req.inputImages.length > 0) { - const ref = req.inputImages[0]; + const ref = req.inputImages?.at(0); + if (ref) { const mime = ref.mimeType || "image/jpeg"; const dataUrl = `data:${mime};base64,${ref.buffer.toString("base64")}`; body.subject_reference = [{ type: "character", image_file: dataUrl }]; diff --git a/extensions/msteams/doctor-contract-api.ts b/extensions/msteams/doctor-contract-api.ts index 3c3e2b4207b0..6cdda08305b9 100644 --- a/extensions/msteams/doctor-contract-api.ts +++ b/extensions/msteams/doctor-contract-api.ts @@ -161,7 +161,8 @@ function resolveLegacySanitizedSessionKey( const matches = knownSessionKeys.filter( (sessionKey) => legacySanitizeSessionKey(sessionKey) === fileStem, ); - return matches.length === 1 ? matches[0] : null; + const [match] = matches; + return matches.length === 1 && match ? match : null; } function listAgentIds(config: { agents?: { list?: Array<{ id?: unknown }> } }): string[] { diff --git a/extensions/msteams/src/graph-messages.ts b/extensions/msteams/src/graph-messages.ts index 403d9fb2368d..1e6b766ccdee 100644 --- a/extensions/msteams/src/graph-messages.ts +++ b/extensions/msteams/src/graph-messages.ts @@ -102,8 +102,10 @@ export function resolveConversationPath(to: string): { channelId?: string; } { const cleaned = stripTargetPrefix(to); - if (cleaned.includes("/")) { - const [teamId, channelId] = cleaned.split("/", 2); + const separatorIndex = cleaned.indexOf("/"); + if (separatorIndex !== -1) { + const teamId = cleaned.slice(0, separatorIndex); + const channelId = cleaned.slice(separatorIndex + 1).replace(/\/.*$/, ""); return { kind: "channel", basePath: `/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}`, diff --git a/extensions/msteams/src/resolve-allowlist.ts b/extensions/msteams/src/resolve-allowlist.ts index 1f9d27e39f97..309100b1ac80 100644 --- a/extensions/msteams/src/resolve-allowlist.ts +++ b/extensions/msteams/src/resolve-allowlist.ts @@ -337,13 +337,14 @@ export async function resolveMSTeamsChannelAllowlist(params: { return { input, resolved: false, note: "team lookup incomplete" }; } const exactTeams = findExactTeams(result.items, team); - if (exactTeams.length === 0) { + const [exactTeam] = exactTeams; + if (!exactTeam) { return { input, resolved: false, note: "team not found" }; } if (exactTeams.length > 1) { return { input, resolved: false, note: "team name is ambiguous" }; } - teamMatch = exactTeams[0]; + teamMatch = exactTeam; } const graphTeamId = teamMatch.id?.trim(); const teamName = teamMatch.displayName?.trim() || team; @@ -538,13 +539,13 @@ export async function resolveMSTeamsUserAllowlist(params: { return { input, resolved: false, note: "user lookup incomplete" }; } const users = findExactUsers(result.items, query); - if (users.length === 0) { + const [match] = users; + if (!match) { return { input, resolved: false, note: "user not found" }; } if (users.length > 1) { return { input, resolved: false, note: "user identity is ambiguous" }; } - const match = users[0]; return { input, resolved: true, diff --git a/extensions/nostr/src/seen-tracker.ts b/extensions/nostr/src/seen-tracker.ts index da63cd783f5f..f37158b97f76 100644 --- a/extensions/nostr/src/seen-tracker.ts +++ b/extensions/nostr/src/seen-tracker.ts @@ -273,8 +273,7 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker { function seed(ids: string[]): void { const now = Date.now(); // Seed in reverse order so first IDs end up at front - for (let i = ids.length - 1; i >= 0; i--) { - const id = ids[i]; + for (const id of ids.toReversed()) { if (!entries.has(id) && entries.size < maxEntries) { insertAtFront(id, now); } diff --git a/extensions/oc-path/src/oc-path/find.ts b/extensions/oc-path/src/oc-path/find.ts index 8de819c80532..6c26cb9804c2 100644 --- a/extensions/oc-path/src/oc-path/find.ts +++ b/extensions/oc-path/src/oc-path/find.ts @@ -7,6 +7,7 @@ * @module @openclaw/oc-path/find */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { isMap, isScalar, isSeq, type Node, type Pair } from "yaml"; import type { MdAst } from "./ast.js"; import type { JsoncValue } from "./jsonc/ast.js"; @@ -190,7 +191,7 @@ function dispatchSeg( walked: readonly SlotSub[], onMatch: OnMatch, ): void { - const cur = subs[i]; + const cur = expectDefined(subs[i], "dispatch index checked by walker"); if (isUnionSeg(cur.value)) { const alts = parseUnionSeg(cur.value); @@ -277,8 +278,8 @@ const jsoncOps: WalkOps = { yield { keySub: quoteSeg(e.key), child: e.value }; } } else if (node.kind === "array") { - for (let idx = 0; idx < node.items.length; idx++) { - yield { keySub: String(idx), child: node.items[idx] }; + for (const [idx, child] of node.items.entries()) { + yield { keySub: String(idx), child }; } } }, @@ -295,7 +296,10 @@ const jsoncOps: WalkOps = { if (idx === null) { return null; } - return { keySub: key, child: node.items[idx] }; + return { + keySub: key, + child: expectDefined(node.items[idx], "parsed JSONC array index is in bounds"), + }; } return null; }, @@ -314,9 +318,9 @@ const jsoncOps: WalkOps = { } } } else if (node.kind === "array") { - for (let idx = 0; idx < node.items.length; idx++) { - if (jsoncChildMatchesPredicate(node.items[idx], pred)) { - yield { keySub: String(idx), child: node.items[idx] }; + for (const [idx, child] of node.items.entries()) { + if (jsoncChildMatchesPredicate(child, pred)) { + yield { keySub: String(idx), child }; } } } @@ -599,7 +603,7 @@ function walkMd( onMatch(walked); return; } - const cur = subs[i]; + const cur = expectDefined(subs[i], "Markdown walk index checked above"); // Frontmatter sentinel short-circuits regular dispatch. if (level.kind === "root" && walked.length === 0 && cur.value === "[frontmatter]") { @@ -701,8 +705,7 @@ const mdOps: WalkOps = { // Disambiguate duplicate slugs via `#N` ordinal so each emitted // path round-trips through resolveOcPath to its own item. const counts = blockSlugCounts(level.block.items); - for (let idx = 0; idx < level.block.items.length; idx++) { - const item = level.block.items[idx]; + for (const [idx, item] of level.block.items.entries()) { const seg = (counts.get(item.slug) ?? 0) > 1 ? `#${idx}` : item.slug; yield { keySub: seg, child: { kind: "item", item, ast: level.ast } }; } @@ -723,7 +726,14 @@ const mdOps: WalkOps = { if (n === null || n < 0 || n >= level.block.items.length) { return null; } - return { keySub: key, child: { kind: "item", item: level.block.items[n], ast: level.ast } }; + return { + keySub: key, + child: { + kind: "item", + item: expectDefined(level.block.items[n], "validated Markdown ordinal is in bounds"), + ast: level.ast, + }, + }; } const target = key.toLowerCase(); const item = level.block.items.find((it) => it.slug === target); @@ -746,7 +756,10 @@ const mdOps: WalkOps = { } // Preserve the positional token in keySub so the resolver // re-evaluates positionally on round-trip. - const item = level.block.items[Number(concrete)]; + const item = expectDefined( + level.block.items[Number(concrete)], + "resolved Markdown position is in bounds", + ); return { keySub: seg, child: { kind: "item", item, ast: level.ast } }; }, *predicate(level, pred) { @@ -760,8 +773,7 @@ const mdOps: WalkOps = { } if (level.kind === "block") { const counts = blockSlugCounts(level.block.items); - for (let idx = 0; idx < level.block.items.length; idx++) { - const item = level.block.items[idx]; + for (const [idx, item] of level.block.items.entries()) { if (mdItemMatchesPredicate(item, pred)) { const seg = (counts.get(item.slug) ?? 0) > 1 ? `#${idx}` : item.slug; yield { keySub: seg, child: { kind: "item", item, ast: level.ast } }; diff --git a/extensions/oc-path/src/oc-path/oc-path.ts b/extensions/oc-path/src/oc-path/oc-path.ts index f16706330866..0f98663808ca 100644 --- a/extensions/oc-path/src/oc-path/oc-path.ts +++ b/extensions/oc-path/src/oc-path/oc-path.ts @@ -10,6 +10,7 @@ * @module @openclaw/oc-path/oc-path */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { OcEmitSentinelError, REDACTED_SENTINEL } from "./sentinel.js"; @@ -175,7 +176,7 @@ export function parseOcPath(input: string): OcPath { fail(`Empty segment in oc:// path: ${printable(input)}`, input, "OC_PATH_EMPTY_SEGMENT"); } } - const fileSeg = rawSegments[0]; + const fileSeg = expectDefined(rawSegments.at(0), "path split always returns a file segment"); const file = isQuotedSeg(fileSeg) ? unquoteSeg(fileSeg) : fileSeg; validateFileSlot(file, input); @@ -231,9 +232,14 @@ function normalizeDeepJsonPathSegments( ); } const section = pathSegments.slice(0, -2).join("."); - const item = pathSegments[pathSegments.length - 2]; - const field = pathSegments[pathSegments.length - 1]; - return [segments[0], section, item, field]; + const item = expectDefined(pathSegments.at(-2), "deep JSON path has an item segment"); + const field = expectDefined(pathSegments.at(-1), "deep JSON path has a field segment"); + return [ + expectDefined(segments.at(0), "normalized path has a file segment"), + section, + item, + field, + ]; } /** Format an `OcPath` struct into its canonical string form. */ @@ -587,7 +593,7 @@ function scanBracketAware(s: string, onChar: ScanCallback, onUnbalanced: () => n let depthBrace = 0; let inQuote = false; for (let i = 0; i < s.length; i++) { - const c = s[i]; + const c = s.charAt(i); if (inQuote) { if (c === '"') { inQuote = false; diff --git a/extensions/oc-path/src/oc-path/parse.ts b/extensions/oc-path/src/oc-path/parse.ts index 91210f4b4330..5cbbaa79b2fd 100644 --- a/extensions/oc-path/src/oc-path/parse.ts +++ b/extensions/oc-path/src/oc-path/parse.ts @@ -12,6 +12,7 @@ */ import MarkdownIt from "markdown-it"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { AstBlock, AstItem, Diagnostic, FrontmatterEntry, ParseResult } from "./ast.js"; import { slugify } from "./slug.js"; @@ -54,13 +55,13 @@ function detectFrontmatter( lines: readonly string[], diagnostics: Diagnostic[], ): FrontmatterRange | null { - if (lines.length < 2 || lines[0] !== FENCE) { + if (lines.length < 2 || lines.at(0) !== FENCE) { return null; } let closeIndex = -1; - for (let i = 1; i < lines.length; i++) { - if (lines[i] === FENCE) { - closeIndex = i; + for (const [offset, line] of lines.slice(1).entries()) { + if (line === FENCE) { + closeIndex = offset + 1; break; } } @@ -74,10 +75,12 @@ function detectFrontmatter( return null; } const entries: FrontmatterEntry[] = []; - for (let i = 1; i < closeIndex; i++) { - const m = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/.exec(lines[i]); + for (const [offset, line] of lines.slice(1, closeIndex).entries()) { + const m = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(.*)$/.exec(line); if (m !== null) { - entries.push({ key: m[1], value: unquote(m[2].trim()), line: i + 1 }); + const key = expectDefined(m[1], "frontmatter key capture"); + const value = expectDefined(m[2], "frontmatter value capture"); + entries.push({ key, value: unquote(value.trim()), line: offset + 2 }); } } return { entries, endLine: closeIndex }; @@ -103,11 +106,14 @@ function walkBlocks( ): { preamble: string; blocks: AstBlock[] } { // Match atx `##` only; setext h2 has `markup: "-"`. const h2: { tokenIdx: number; lineIdx: number; text: string }[] = []; - for (let i = 0; i < tokens.length; i++) { - const t = tokens[i]; + for (const [i, t] of tokens.entries()) { if (t.type === "heading_open" && t.tag === "h2" && t.markup === "##" && t.map !== null) { const inline = tokens[i + 1]; - h2.push({ tokenIdx: i, lineIdx: t.map[0], text: inline?.content ?? "" }); + h2.push({ + tokenIdx: i, + lineIdx: expectDefined(t.map.at(0), "heading token map start"), + text: inline?.content ?? "", + }); } } @@ -115,20 +121,22 @@ function walkBlocks( return { preamble: bodyLines.join("\n"), blocks: [] }; } - const preamble = bodyLines.slice(0, h2[0].lineIdx).join("\n"); + const firstHeading = expectDefined(h2.at(0), "non-empty heading list"); + const preamble = bodyLines.slice(0, firstHeading.lineIdx).join("\n"); const blocks: AstBlock[] = []; - for (let h = 0; h < h2.length; h++) { - const start = h2[h].lineIdx; - const end = h + 1 < h2.length ? h2[h + 1].lineIdx : bodyLines.length; + for (const [h, heading] of h2.entries()) { + const nextHeading = h2.at(h + 1); + const start = heading.lineIdx; + const end = nextHeading?.lineIdx ?? bodyLines.length; // Slice by INDEX so unmapped descendants (cells, markers, inline) // ride along with their parent. h2 = open + inline + close = 3. - const tokenStart = h2[h].tokenIdx + 3; - const tokenEnd = h + 1 < h2.length ? h2[h + 1].tokenIdx : tokens.length; + const tokenStart = heading.tokenIdx + 3; + const tokenEnd = nextHeading?.tokenIdx ?? tokens.length; const blockTokens = tokens.slice(tokenStart, tokenEnd); blocks.push({ - heading: h2[h].text, - slug: slugify(h2[h].text), + heading: heading.text, + slug: slugify(heading.text), line: bodyFileLine + start, bodyText: bodyLines.slice(start + 1, end).join("\n"), items: extractItems(blockTokens, bodyFileLine), @@ -144,8 +152,7 @@ function walkBlocks( // sub-bullets); lint rules flag depth / duplicate-slug collisions. function extractItems(tokens: readonly Token[], bodyFileLine: number): AstItem[] { const items: AstItem[] = []; - for (let i = 0; i < tokens.length; i++) { - const t = tokens[i]; + for (const [i, t] of tokens.entries()) { if (t.type !== "list_item_open" || t.map === null) { continue; } @@ -153,7 +160,7 @@ function extractItems(tokens: readonly Token[], bodyFileLine: number): AstItem[] let nestedDepth = 0; let text = ""; for (let j = i + 1; j < tokens.length; j++) { - const x = tokens[j]; + const x = expectDefined(tokens[j], "item scan index is in bounds"); if (x.type === "list_item_close" && nestedDepth === 0) { break; } @@ -166,11 +173,15 @@ function extractItems(tokens: readonly Token[], bodyFileLine: number): AstItem[] } } const kvMatch = KV_RE.exec(text); + const kvKey = kvMatch === null ? undefined : expectDefined(kvMatch[1], "item key capture"); + const kvValue = kvMatch === null ? undefined : expectDefined(kvMatch[2], "item value capture"); items.push({ text, - slug: kvMatch ? slugify(kvMatch[1]) : slugify(text), - line: bodyFileLine + t.map[0], - ...(kvMatch !== null ? { kv: { key: kvMatch[1].trim(), value: kvMatch[2].trim() } } : {}), + slug: kvKey === undefined ? slugify(text) : slugify(kvKey), + line: bodyFileLine + expectDefined(t.map.at(0), "list item token map start"), + ...(kvKey !== undefined && kvValue !== undefined + ? { kv: { key: kvKey.trim(), value: kvValue.trim() } } + : {}), }); } return items; diff --git a/extensions/oc-path/src/oc-path/universal.ts b/extensions/oc-path/src/oc-path/universal.ts index 35e043748c54..83bf60903527 100644 --- a/extensions/oc-path/src/oc-path/universal.ts +++ b/extensions/oc-path/src/oc-path/universal.ts @@ -15,6 +15,7 @@ * @module @openclaw/oc-path/universal */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { isMap, isScalar, isSeq, type Pair } from "yaml"; import type { MdAst } from "./ast.js"; import { setMdOcPath } from "./edit.js"; @@ -127,7 +128,7 @@ export function detectInsertion(path: OcPath): InsertionInfo | null { return null; } - const last = segments[segments.length - 1]; + const last = expectDefined(segments.at(-1), "non-empty insertion path segments"); if (!last.value.startsWith("+")) { return null; } @@ -391,7 +392,7 @@ function resolveJsonlInsertion(ast: JsonlAst, info: InsertionInfo): OcMatch | nu if (info.parentPath.section !== undefined) { return null; } - const lastLine = ast.lines.length > 0 ? ast.lines[ast.lines.length - 1].line : 0; + const lastLine = ast.lines.at(-1)?.line ?? 0; return { kind: "insertion-point", container: "jsonl-file", line: lastLine + 1 }; } @@ -613,18 +614,25 @@ function setMdInsertion(ast: MdAst, info: InsertionInfo, value: string): SetResu if (info.marker !== "+") { return { ok: false, reason: "not-writable", detail: "md section insertion uses bare `+`" }; } - const blockIdx = ast.blocks.findIndex((b) => b.slug === p.section!.toLowerCase()); + const section = expectDefined(p.section, "Markdown section insertion has a section"); + const blockIdx = ast.blocks.findIndex((b) => b.slug === section.toLowerCase()); if (blockIdx === -1) { return { ok: false, reason: "unresolved" }; } - const block = ast.blocks[blockIdx]; + const block = expectDefined(ast.blocks[blockIdx], "located Markdown block index"); const kvMatch = /^([^:]+?)\s*:\s*(.+)$/.exec(value); const itemLine = `- ${value}`; + const kvKey = + kvMatch === null ? undefined : expectDefined(kvMatch[1], "Markdown item key capture"); + const kvValue = + kvMatch === null ? undefined : expectDefined(kvMatch[2], "Markdown item value capture"); const newItem = { text: value, - slug: slugifyHeading(kvMatch ? kvMatch[1] : value), + slug: slugifyHeading(kvKey ?? value), line: 0, - ...(kvMatch !== null ? { kv: { key: kvMatch[1].trim(), value: kvMatch[2].trim() } } : {}), + ...(kvKey !== undefined && kvValue !== undefined + ? { kv: { key: kvKey.trim(), value: kvValue.trim() } } + : {}), }; const newBodyText = block.bodyText.length === 0 ? itemLine : block.bodyText.replace(/\n*$/, "\n") + itemLine; @@ -924,7 +932,7 @@ function mutateAt( if (idx === -1) { return null; } - const child = current.entries[idx]; + const child = expectDefined(current.entries[idx], "located JSONC object entry index"); const replaced = mutateAt(child.value, segments, i + 1, mutate); if (replaced === null) { return null; @@ -942,7 +950,7 @@ function mutateAt( if (idx === null) { return null; } - const child = current.items[idx]; + const child = expectDefined(current.items[idx], "parsed JSONC array index is in bounds"); const replaced = mutateAt(child, segments, i + 1, mutate); if (replaced === null) { return null; diff --git a/extensions/ollama/src/discovery-shared.ts b/extensions/ollama/src/discovery-shared.ts index ad2c15a4fd69..0cd1f7657709 100644 --- a/extensions/ollama/src/discovery-shared.ts +++ b/extensions/ollama/src/discovery-shared.ts @@ -132,6 +132,9 @@ function isIpv4PrivateRange(host: string): boolean { return false; } const [a, b] = octets; + if (a === undefined || b === undefined) { + return false; + } return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168); } diff --git a/extensions/ollama/src/node-inference.ts b/extensions/ollama/src/node-inference.ts index 5ca9f4fb4782..5a33c9e95ea9 100644 --- a/extensions/ollama/src/node-inference.ts +++ b/extensions/ollama/src/node-inference.ts @@ -1,5 +1,6 @@ -// Ollama node inference exposes local models to agents through paired node hosts. import { jsonResult } from "openclaw/plugin-sdk/channel-actions"; +// Ollama node inference exposes local models to agents through paired node hosts. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { readFiniteNumberParam, readPositiveIntegerParam, @@ -388,7 +389,7 @@ function findNode(nodes: NodeSummary[], query: string): NodeSummary { if (matches.length > 1) { throw new Error(`node ${JSON.stringify(query)} is ambiguous; use its nodeId`); } - return matches[0]; + return expectDefined(matches[0], "single matching Ollama inference node"); } function parseInvokePayload(raw: unknown): Record { diff --git a/extensions/ollama/src/provider-models.ts b/extensions/ollama/src/provider-models.ts index 2ea836bca789..1382a694d3c0 100644 --- a/extensions/ollama/src/provider-models.ts +++ b/extensions/ollama/src/provider-models.ts @@ -114,7 +114,11 @@ export function parseOllamaNumCtxParameter(parameters: unknown): number | undefi if (!match) { continue; } - const parsed = Number.parseInt(match[1], 10); + const rawValue = match[1]; + if (!rawValue) { + continue; + } + const parsed = Number.parseInt(rawValue, 10); if (Number.isFinite(parsed) && parsed > 0) { lastValue = parsed; } diff --git a/extensions/ollama/src/sanitizers/kimi-inline-reasoning.ts b/extensions/ollama/src/sanitizers/kimi-inline-reasoning.ts index e870d1b76c2b..ecc969eed1c9 100644 --- a/extensions/ollama/src/sanitizers/kimi-inline-reasoning.ts +++ b/extensions/ollama/src/sanitizers/kimi-inline-reasoning.ts @@ -26,7 +26,7 @@ function resolveInlineReasoningVisibleText(params: { final: boolean; }): InlineReasoningVisibleTextResolution { const match = INLINE_REASONING_BOUNDARY_RE.exec(params.text); - if (!match) { + if (!match || match[1] === undefined) { if (!params.final && params.text.length <= INLINE_REASONING_MAX_PENDING_CHARS) { return { kind: "pending" }; } diff --git a/extensions/ollama/src/setup.ts b/extensions/ollama/src/setup.ts index 5ece2480a598..fce1ccc31b2a 100644 --- a/extensions/ollama/src/setup.ts +++ b/extensions/ollama/src/setup.ts @@ -1,5 +1,6 @@ -// Ollama setup module handles plugin onboarding behavior. import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +// Ollama setup module handles plugin onboarding behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { OpenClawConfig, SecretInput, @@ -455,7 +456,8 @@ function mergeUniqueModelNames(...groups: string[][]): string[] { const key = getOllamaLatestDedupeKey(name); const existingIndex = indexByKey.get(key); if (existingIndex !== undefined) { - if (shouldReplaceOllamaModelName(merged[existingIndex], name)) { + const existing = expectDefined(merged[existingIndex], "indexed Ollama model name"); + if (shouldReplaceOllamaModelName(existing, name)) { merged[existingIndex] = name; } continue; @@ -669,7 +671,9 @@ export async function configureOllamaNonInteractive(params: { const modelNames = models.map((model) => model.name); const orderedModelNames = mergeUniqueModelNames(OLLAMA_SUGGESTED_MODELS_LOCAL, modelNames); - const requestedDefaultModelId = explicitModel ?? OLLAMA_SUGGESTED_MODELS_LOCAL[0]; + const requestedDefaultModelId = + explicitModel ?? + expectDefined(OLLAMA_SUGGESTED_MODELS_LOCAL[0], "default suggested Ollama model"); const availableModelNames = new Set(modelNames); const availableDefaultModelId = findAvailableOllamaModelName( requestedDefaultModelId, @@ -714,7 +718,7 @@ export async function configureOllamaNonInteractive(params: { defaultModelId = allModelNames.find((name) => findAvailableOllamaModelName(name, availableModelNames)) ?? - Array.from(availableModelNames)[0]; + expectDefined(availableModelNames.values().next().value, "available Ollama setup model"); params.runtime.log( `Ollama model ${requestedDefaultModelId} was not available; using ${defaultModelId} instead.`, ); diff --git a/extensions/openai/openai-chatgpt-auth-identity.ts b/extensions/openai/openai-chatgpt-auth-identity.ts index 5208a102c066..f27759a8688b 100644 --- a/extensions/openai/openai-chatgpt-auth-identity.ts +++ b/extensions/openai/openai-chatgpt-auth-identity.ts @@ -35,7 +35,11 @@ function decodeCodexJwtPayload(accessToken: string): CodexJwtPayload | null { } try { - const decoded = Buffer.from(parts[1], "base64url").toString("utf8"); + const payload = parts.at(1); + if (!payload) { + return null; + } + const decoded = Buffer.from(payload, "base64url").toString("utf8"); const parsed = JSON.parse(decoded); return parsed && typeof parsed === "object" ? (parsed as CodexJwtPayload) : null; } catch { diff --git a/extensions/openai/realtime-voice-provider.ts b/extensions/openai/realtime-voice-provider.ts index 15ed68d392b3..70653c3c290e 100644 --- a/extensions/openai/realtime-voice-provider.ts +++ b/extensions/openai/realtime-voice-provider.ts @@ -273,6 +273,9 @@ function resolveKeychainSecretRef(value: string): string | undefined { return cached; } const [, service, account] = match; + if (!service || !account) { + return undefined; + } try { const resolved = execFileSync( diff --git a/extensions/opencode-go/stream-termination.ts b/extensions/opencode-go/stream-termination.ts index bf10ede9b314..d3bd63208f5b 100644 --- a/extensions/opencode-go/stream-termination.ts +++ b/extensions/opencode-go/stream-termination.ts @@ -68,11 +68,12 @@ function combineAbortSignals(signals: (AbortSignal | undefined)[]): { cleanup(): void; } { const present = signals.filter((signal): signal is AbortSignal => Boolean(signal)); - if (present.length === 0) { + const [firstSignal] = present; + if (!firstSignal) { return { signal: new AbortController().signal, cleanup: () => undefined }; } if (present.length === 1) { - return { signal: present[0], cleanup: () => undefined }; + return { signal: firstSignal, cleanup: () => undefined }; } const anyFn = ( AbortSignal as unknown as { diff --git a/extensions/openshell/src/fs-bridge.ts b/extensions/openshell/src/fs-bridge.ts index bb00af6912c5..96eb5cc463a3 100644 --- a/extensions/openshell/src/fs-bridge.ts +++ b/extensions/openshell/src/fs-bridge.ts @@ -601,13 +601,10 @@ async function assertLocalPathSafety(params: { } const relative = path.relative(params.root, params.target.hostPath); - const segments = relative - .split(path.sep) - .filter(Boolean) - .slice(0, Math.max(0, relative.split(path.sep).filter(Boolean).length)); + const segments = relative.split(path.sep).filter(Boolean); let cursor = params.root; - for (let index = 0; index < segments.length; index += 1) { - cursor = path.join(cursor, segments[index]); + for (const [index, segment] of segments.entries()) { + cursor = path.join(cursor, segment); const stats = await fsPromises.lstat(cursor).catch(() => null); if (!stats) { if (index === segments.length - 1 && params.allowMissingLeaf) { diff --git a/extensions/policy/src/doctor/register.ts b/extensions/policy/src/doctor/register.ts index b0ee8047249e..5066e0c514f2 100644 --- a/extensions/policy/src/doctor/register.ts +++ b/extensions/policy/src/doctor/register.ts @@ -4806,11 +4806,12 @@ function scopedPolicyFields( } function scopedPolicyValue(overlay: Record, path: readonly string[]): unknown { - const scopedRoot = path[0] === "agents" ? overlay.agents : overlay[path[0]]; - if (path[0] === "agents") { - return getPolicyPath(scopedRoot, path.slice(1)); + const [root, ...remainingPath] = path; + if (!root) { + return undefined; } - return getPolicyPath(scopedRoot, path.slice(1)); + const scopedRoot = root === "agents" ? overlay.agents : overlay[root]; + return getPolicyPath(scopedRoot, remainingPath); } function getPolicyPath(value: unknown, path: readonly string[]): unknown { diff --git a/extensions/policy/src/policy-conformance.ts b/extensions/policy/src/policy-conformance.ts index ee203aa1cfab..24905d5b2797 100644 --- a/extensions/policy/src/policy-conformance.ts +++ b/extensions/policy/src/policy-conformance.ts @@ -573,8 +573,12 @@ function normalizeSelectorValues( } function scopedPolicyValue(overlay: Record, path: readonly string[]): unknown { - const scopedRoot = path[0] === "agents" ? overlay.agents : overlay[path[0]]; - return getPolicyPath(scopedRoot, path.slice(1)); + const [root, ...remainingPath] = path; + if (!root) { + return undefined; + } + const scopedRoot = root === "agents" ? overlay.agents : overlay[root]; + return getPolicyPath(scopedRoot, remainingPath); } function getPolicyPath(value: unknown, path: readonly string[]): unknown { diff --git a/extensions/policy/src/policy-state.ts b/extensions/policy/src/policy-state.ts index ddc3977d54e7..d31ffa8214bf 100644 --- a/extensions/policy/src/policy-state.ts +++ b/extensions/policy/src/policy-state.ts @@ -2259,10 +2259,14 @@ function scanPolicyToolHeaders(raw: string): readonly PolicyToolEvidence[] { const heading = /^###\s+([^\s#]+)(.*)$/.exec(line); const bullet = /^[-*+]\s+([^:\s][^:]*?)\s*:(.*)$/.exec(line); const match = heading ?? bullet; - if (match === null || slugify(match[1]).length === 0) { + const toolName = match?.[1]; + if (!toolName) { + continue; + } + const id = slugify(toolName); + if (!id) { continue; } - const id = slugify(match[1]); const entry: { id: string; source: string; diff --git a/extensions/qa-lab/src/character-eval.ts b/extensions/qa-lab/src/character-eval.ts index db5c38327722..d46114abee1e 100644 --- a/extensions/qa-lab/src/character-eval.ts +++ b/extensions/qa-lab/src/character-eval.ts @@ -195,13 +195,16 @@ async function mapWithConcurrency( mapper: (item: T, index: number) => Promise, ) { const results = Array.from({ length: items.length }); - let nextIndex = 0; + const pendingItems = items.entries(); const workerCount = Math.min(normalizeConcurrency(concurrency), items.length); const workers = Array.from({ length: workerCount }, async () => { - while (nextIndex < items.length) { - const index = nextIndex; - nextIndex += 1; - results[index] = await mapper(items[index], index); + while (true) { + const next = pendingItems.next(); + if (next.done) { + return; + } + const [index, item] = next.value; + results[index] = await mapper(item, index); } }); await Promise.all(workers); diff --git a/extensions/qa-lab/src/ci-smoke-plan.ts b/extensions/qa-lab/src/ci-smoke-plan.ts index 68f9e85016e5..4c4bd893e1a0 100644 --- a/extensions/qa-lab/src/ci-smoke-plan.ts +++ b/extensions/qa-lab/src/ci-smoke-plan.ts @@ -102,7 +102,13 @@ export function createQaSmokeCiPart(partId: string): QaSmokeCiPart { (left, right) => estimateScenarioCost(right) - estimateScenarioCost(left) || left.id.localeCompare(right.id), ); - const partitions = QA_SMOKE_CI_PARTS.map(() => ({ cost: 0, scenarios: [] as typeof scenarios })); + const partitions: [ + { cost: number; scenarios: typeof scenarios }, + { cost: number; scenarios: typeof scenarios }, + ] = [ + { cost: 0, scenarios: [] }, + { cost: 0, scenarios: [] }, + ]; for (const scenario of defaultChannelScenarios) { const partition = partitions[0].cost <= partitions[1].cost ? partitions[0] : partitions[1]; partition.scenarios.push(scenario); @@ -111,11 +117,12 @@ export function createQaSmokeCiPart(partId: string): QaSmokeCiPart { const matrixPartIndex = 1; const partIndex = QA_SMOKE_CI_PARTS.indexOf(partId); + const selectedPartition = partId === QA_SMOKE_CI_PARTS[0] ? partitions[0] : partitions[1]; const runs: QaSmokeCiRun[] = [ { channel: OPENCLAW_CRABLINE_DEFAULT_CHANNEL, slug: "primary", - scenario_ids: partitions[partIndex].scenarios.map((scenario) => scenario.id).toSorted(), + scenario_ids: selectedPartition.scenarios.map((scenario) => scenario.id).toSorted(), }, ]; if (partIndex === matrixPartIndex) { diff --git a/extensions/qa-lab/src/cli.ts b/extensions/qa-lab/src/cli.ts index e307bb50d2f2..14acf512841b 100644 --- a/extensions/qa-lab/src/cli.ts +++ b/extensions/qa-lab/src/cli.ts @@ -141,7 +141,7 @@ function collectCliSuppliedQaRunFlags( } function formatFlagList(flags: readonly string[]): string { - return flags.length === 1 ? flags[0] : flags.join(", "); + return flags.join(", "); } function validateQaRunMode(opts: QaRunCliOptions, command: Command) { diff --git a/extensions/qa-lab/src/coverage-report.ts b/extensions/qa-lab/src/coverage-report.ts index 52a85a4052b7..dd90d05a4e90 100644 --- a/extensions/qa-lab/src/coverage-report.ts +++ b/extensions/qa-lab/src/coverage-report.ts @@ -537,7 +537,10 @@ export function renderQaScenarioMatchesMarkdownReport(params: { ]; if (commandGroups.length === 1) { - lines.push(`- Suite command: \`${formatSuiteCommand(commandGroups[0].matches)}\``); + const group = commandGroups[0]; + if (group) { + lines.push(`- Suite command: \`${formatSuiteCommand(group.matches)}\``); + } } else if (commandGroups.length > 1) { lines.push("- Suite commands:"); for (const group of commandGroups) { diff --git a/extensions/qa-lab/src/crabline-transport.ts b/extensions/qa-lab/src/crabline-transport.ts index daa4033d765a..cdb10c78cce9 100644 --- a/extensions/qa-lab/src/crabline-transport.ts +++ b/extensions/qa-lab/src/crabline-transport.ts @@ -85,9 +85,13 @@ function readTelegramLifecycleEvent(params: { if (!previous && providerKey && providerMessageId) { const pending = params.pendingByChat.get(chatId) ?? []; if (pending.length === 1) { - previous = pending[0]; - previous.id = providerMessageId; - params.messageByProviderId.set(providerKey, previous); + const pendingMessage = pending[0]; + if (!pendingMessage) { + return null; + } + previous = pendingMessage; + pendingMessage.id = providerMessageId; + params.messageByProviderId.set(providerKey, pendingMessage); params.pendingByChat.delete(chatId); } } diff --git a/extensions/qa-lab/src/evidence-gallery.ts b/extensions/qa-lab/src/evidence-gallery.ts index 8b54574c2110..499a8ebc4f55 100644 --- a/extensions/qa-lab/src/evidence-gallery.ts +++ b/extensions/qa-lab/src/evidence-gallery.ts @@ -621,13 +621,17 @@ function uxMatrixEntryKey( entry: QaEvidenceSummaryEntry, ): { stage: string; surface: string } | null { const idMatch = /^ux-matrix\.([a-z0-9-]+)\.([a-z0-9-]+)$/u.exec(entry.test.id); - if (idMatch) { - return { surface: idMatch[1], stage: idMatch[2] }; + const idSurface = idMatch?.[1]; + const idStage = idMatch?.[2]; + if (idSurface && idStage) { + return { surface: idSurface, stage: idStage }; } for (const artifact of entry.execution?.artifacts ?? []) { const sourceMatch = /^ux-matrix:([a-z0-9-]+):([a-z0-9-]+)$/u.exec(artifact.source); - if (sourceMatch) { - return { surface: sourceMatch[1], stage: sourceMatch[2] }; + const sourceSurface = sourceMatch?.[1]; + const sourceStage = sourceMatch?.[2]; + if (sourceSurface && sourceStage) { + return { surface: sourceSurface, stage: sourceStage }; } } return null; diff --git a/extensions/qa-lab/src/fixture-utils.ts b/extensions/qa-lab/src/fixture-utils.ts index a847d995f18c..2536be065449 100644 --- a/extensions/qa-lab/src/fixture-utils.ts +++ b/extensions/qa-lab/src/fixture-utils.ts @@ -311,7 +311,7 @@ async function countNeedlesInFile(filePath: string, needles: Record 0) { return params.retryAfterMs; } - const base = RETRY_BACKOFF_MS[Math.min(RETRY_BACKOFF_MS.length - 1, params.attempt - 1)]; + const backoffIndex = Math.max(0, Math.min(RETRY_BACKOFF_MS.length - 1, params.attempt - 1)); + const base = expectDefined(RETRY_BACKOFF_MS[backoffIndex], "QA credential retry backoff"); const jitter = 0.75 + params.randomImpl() * 0.5; return Math.max(100, Math.round(base * jitter)); } diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts index 6bfb01b8ef4f..e1a17348b6ad 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts @@ -1339,7 +1339,8 @@ async function runTelegramQaRttChecks(params: { sutUsername: params.sutUsername, }); const steps = resolveTelegramQaScenarioSteps(run); - if (steps.length !== 1) { + const step = steps[0]; + if (steps.length !== 1 || !step) { throw new Error(`Telegram QA RTT check ${params.scenario.id} must have one step.`); } try { @@ -1353,7 +1354,7 @@ async function runTelegramQaRttChecks(params: { observedMessages: params.observedMessages, replyTimeoutMs: params.rttOptions.timeoutMs, scenario: params.scenario, - step: steps[0], + step, sutBotId: params.sutBotId, }); if (!stepResult.matched) { diff --git a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts index d7e64b7412cd..2a7364121162 100644 --- a/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.runtime.ts @@ -2620,6 +2620,9 @@ async function waitForWhatsAppSutReactionSequenceToTrigger( continue; } const expectedEmoji = params.emojis[matched.length]; + if (!expectedEmoji) { + return false; + } if (matchesWhatsAppSutReactionToTrigger(message, context, { emoji: expectedEmoji })) { matched.push(message); lastMatchedObservedAtMs = observedAtMs; @@ -2706,24 +2709,25 @@ async function callWhatsAppGatewaySendConcurrently( // real Gateway overlap so this probe reaches the shared WhatsApp socket concurrently. const connection = resolveWhatsAppGatewayRpcConnection(context.gateway); const clients = await Promise.all( - sends.map(() => - startQaGatewayRpcClient({ + sends.map(async (send) => ({ + send, + client: await startQaGatewayRpcClient({ logs: connection.logs, token: connection.token, wsUrl: connection.wsUrl, }), - ), + })), ); try { await Promise.all( - clients.map((client, index) => - client.request("send", buildWhatsAppGatewaySendRequest(context, sends[index]), { + clients.map(({ client, send }) => + client.request("send", buildWhatsAppGatewaySendRequest(context, send), { timeoutMs: 60_000, }), ), ); } finally { - await Promise.all(clients.map((client) => client.stop())); + await Promise.all(clients.map(({ client }) => client.stop())); } } diff --git a/extensions/qa-lab/src/providers/aimock/server.ts b/extensions/qa-lab/src/providers/aimock/server.ts index 20f99f5da9d0..6dbeaba358e5 100644 --- a/extensions/qa-lab/src/providers/aimock/server.ts +++ b/extensions/qa-lab/src/providers/aimock/server.ts @@ -208,8 +208,12 @@ function toRequestSnapshots(entries: JournalEntry[]): AimockRequestSnapshot[] { if (snapshot.toolOutputCallId && pendingPlannedIndexes.length > 0) { const plannedIndex = pendingPlannedIndexes.shift(); if (plannedIndex !== undefined) { + const plannedSnapshot = snapshots[plannedIndex]; + if (!plannedSnapshot) { + continue; + } snapshots[plannedIndex] = { - ...snapshots[plannedIndex], + ...plannedSnapshot, plannedToolCallId: snapshot.toolOutputCallId, }; } diff --git a/extensions/qa-lab/src/providers/mock-openai/server.ts b/extensions/qa-lab/src/providers/mock-openai/server.ts index b82b20689452..4761a8d66c72 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.ts @@ -398,15 +398,15 @@ function extractEmbeddingInputTexts(input: unknown): string[] { function buildDeterministicEmbedding(text: string, dimensions = 16) { const values = Array.from({ length: dimensions }, () => 0); for (let index = 0; index < text.length; index += 1) { - values[index % dimensions] += text.charCodeAt(index) / 255; + const embeddingIndex = index % dimensions; + values[embeddingIndex] = (values[embeddingIndex] ?? 0) + text.charCodeAt(index) / 255; } const magnitude = Math.hypot(...values) || 1; return values.map((value) => Number((value / magnitude).toFixed(8))); } function extractLastUserText(input: ResponsesInputItem[]) { - for (let index = input.length - 1; index >= 0; index -= 1) { - const item = input[index]; + for (const item of input.toReversed()) { if (item.role !== "user" || !Array.isArray(item.content)) { continue; } @@ -419,16 +419,12 @@ function extractLastUserText(input: ResponsesInputItem[]) { } function findLastUserIndex(input: ResponsesInputItem[]) { - for (let index = input.length - 1; index >= 0; index -= 1) { - const item = input[index]; - if (item.role !== "user" || !Array.isArray(item.content)) { - continue; - } - if (!isInternalRuntimeContextCarrierText(extractInputText(item.content))) { - return index; - } - } - return -1; + return input.findLastIndex( + (item) => + item.role === "user" && + Array.isArray(item.content) && + !isInternalRuntimeContextCarrierText(extractInputText(item.content)), + ); } function isInternalRuntimeContextCarrierText(text: string) { @@ -530,19 +526,17 @@ function functionCallOutputIsStructuredError(item: ResponsesInputItem) { function extractToolOutput(input: ResponsesInputItem[]) { const lastUserIndex = findLastUserIndex(input); - for (let index = input.length - 1; index > lastUserIndex; index -= 1) { - const item = input[index]; + for (const item of input.slice(lastUserIndex + 1).toReversed()) { const output = extractFunctionCallOutputText(item); if (output) { return output; } } - for (let index = input.length - 1; index >= 0; index -= 1) { - const item = input[index]; - const output = extractFunctionCallOutputText(item); + for (const [candidateIndex, candidateItem] of Array.from(input.entries()).toReversed()) { + const output = extractFunctionCallOutputText(candidateItem); if (output) { const laterUserTexts = input - .slice(index + 1) + .slice(candidateIndex + 1) .filter((laterItem) => laterItem.role === "user" && Array.isArray(laterItem.content)) .map((laterItem) => extractInputText(laterItem.content as unknown[])) .filter(Boolean); @@ -560,19 +554,17 @@ function extractToolOutput(input: ResponsesInputItem[]) { function extractToolOutputStructuredError(input: ResponsesInputItem[]) { const lastUserIndex = findLastUserIndex(input); - for (let index = input.length - 1; index > lastUserIndex; index -= 1) { - const item = input[index]; + for (const item of input.slice(lastUserIndex + 1).toReversed()) { const output = extractFunctionCallOutputText(item); if (output) { return functionCallOutputIsStructuredError(item); } } - for (let index = input.length - 1; index >= 0; index -= 1) { - const item = input[index]; - const output = extractFunctionCallOutputText(item); + for (const [candidateIndex, candidateItem] of Array.from(input.entries()).toReversed()) { + const output = extractFunctionCallOutputText(candidateItem); if (output) { const laterUserTexts = input - .slice(index + 1) + .slice(candidateIndex + 1) .filter((laterItem) => laterItem.role === "user" && Array.isArray(laterItem.content)) .map((laterItem) => extractInputText(laterItem.content as unknown[])) .filter(Boolean); @@ -580,7 +572,7 @@ function extractToolOutputStructuredError(input: ResponsesInputItem[]) { laterUserTexts.length > 0 && laterUserTexts.every((text) => isToolOutputContinuationText(text)) ) { - return functionCallOutputIsStructuredError(item); + return functionCallOutputIsStructuredError(candidateItem); } } } @@ -589,19 +581,17 @@ function extractToolOutputStructuredError(input: ResponsesInputItem[]) { function extractToolOutputCallId(input: ResponsesInputItem[]) { const lastUserIndex = findLastUserIndex(input); - for (let index = input.length - 1; index > lastUserIndex; index -= 1) { - const item = input[index]; + for (const item of input.slice(lastUserIndex + 1).toReversed()) { const output = extractFunctionCallOutputText(item); if (output) { return extractFunctionCallOutputCallId(item); } } - for (let index = input.length - 1; index >= 0; index -= 1) { - const item = input[index]; - const output = extractFunctionCallOutputText(item); + for (const [candidateIndex, candidateItem] of Array.from(input.entries()).toReversed()) { + const output = extractFunctionCallOutputText(candidateItem); if (output) { const laterUserTexts = input - .slice(index + 1) + .slice(candidateIndex + 1) .filter((laterItem) => laterItem.role === "user" && Array.isArray(laterItem.content)) .map((laterItem) => extractInputText(laterItem.content as unknown[])) .filter(Boolean); @@ -609,7 +599,7 @@ function extractToolOutputCallId(input: ResponsesInputItem[]) { laterUserTexts.length > 0 && laterUserTexts.every((text) => isToolOutputContinuationText(text)) ) { - return extractFunctionCallOutputCallId(item); + return extractFunctionCallOutputCallId(candidateItem); } } } @@ -617,8 +607,7 @@ function extractToolOutputCallId(input: ResponsesInputItem[]) { } function extractLatestToolOutput(input: ResponsesInputItem[]) { - for (let index = input.length - 1; index >= 0; index -= 1) { - const item = input[index]; + for (const item of input.toReversed()) { const output = extractFunctionCallOutputText(item); if (output) { return output; @@ -635,13 +624,9 @@ function extractAllToolOutputText(input: ResponsesInputItem[]) { } function extractUserTextAfterLatestToolOutput(input: ResponsesInputItem[]) { - let latestToolOutputIndex = -1; - for (let index = input.length - 1; index >= 0; index -= 1) { - if (extractFunctionCallOutputText(input[index])) { - latestToolOutputIndex = index; - break; - } - } + const latestToolOutputIndex = input.findLastIndex((item) => + Boolean(extractFunctionCallOutputText(item)), + ); if (latestToolOutputIndex < 0) { return ""; } @@ -1998,10 +1983,11 @@ function buildAssistantEvents(specsOrText: MockAssistantMessageSpec[] | string): }, ] : specsOrText; - const output = specs.map((spec) => buildAssistantOutputItem(spec)); + const renderedSpecs = specs.map((spec) => ({ spec, item: buildAssistantOutputItem(spec) })); + const output = renderedSpecs.map(({ item }) => item); const events: StreamEvent[] = []; - for (const [outputIndex, spec] of specs.entries()) { + for (const [outputIndex, { spec, item }] of renderedSpecs.entries()) { events.push({ type: "response.output_item.added", item: { @@ -2033,7 +2019,7 @@ function buildAssistantEvents(specsOrText: MockAssistantMessageSpec[] | string): } events.push({ type: "response.output_item.done", - item: output[outputIndex], + item, }); } diff --git a/extensions/qa-lab/src/qa-transport.ts b/extensions/qa-lab/src/qa-transport.ts index 0136b66561de..c19cd101733c 100644 --- a/extensions/qa-lab/src/qa-transport.ts +++ b/extensions/qa-lab/src/qa-transport.ts @@ -415,6 +415,9 @@ export async function waitForQaTransportOutboundSequence(params: { return undefined; } const candidate = events[finalIndex]; + if (!candidate) { + return undefined; + } const sequenceEvents = events.filter(({ message }) => message.id === candidate.message.id); const latest = sequenceEvents.at(-1); if ( diff --git a/extensions/qa-lab/src/runtime-parity.ts b/extensions/qa-lab/src/runtime-parity.ts index dff2d67cea32..81df648f82a8 100644 --- a/extensions/qa-lab/src/runtime-parity.ts +++ b/extensions/qa-lab/src/runtime-parity.ts @@ -421,21 +421,25 @@ function resolveToolCallOrder(records: RuntimeParityTranscriptRecord[]): Runtime }; const markResolved = (index: number) => { - ordered[index] = { ...ordered[index], _resolved: true }; + const pending = ordered[index]; + if (!pending) { + return; + } + ordered[index] = { ...pending, _resolved: true }; const unresolvedIndex = unresolvedOrder.indexOf(index); if (unresolvedIndex >= 0) { unresolvedOrder.splice(unresolvedIndex, 1); } - const toolIndices = unresolvedByTool.get(ordered[index].tool); + const toolIndices = unresolvedByTool.get(pending.tool); if (!toolIndices) { return; } const nextIndices = toolIndices.filter((candidate) => candidate !== index); if (nextIndices.length > 0) { - unresolvedByTool.set(ordered[index].tool, nextIndices); + unresolvedByTool.set(pending.tool, nextIndices); return; } - unresolvedByTool.delete(ordered[index].tool); + unresolvedByTool.delete(pending.tool); }; const matchPendingIndex = (result: { id?: string; tool?: string }) => { @@ -509,7 +513,11 @@ function resolveToolCallOrderFromMockRequests( }; const markResolved = (index: number) => { - ordered[index] = { ...ordered[index], _resolved: true }; + const pending = ordered[index]; + if (!pending) { + return; + } + ordered[index] = { ...pending, _resolved: true }; const unresolvedIndex = unresolvedOrder.indexOf(index); if (unresolvedIndex >= 0) { unresolvedOrder.splice(unresolvedIndex, 1); diff --git a/extensions/qa-lab/src/suite-planning.ts b/extensions/qa-lab/src/suite-planning.ts index cf51e12837f3..1fb2828fae7e 100644 --- a/extensions/qa-lab/src/suite-planning.ts +++ b/extensions/qa-lab/src/suite-planning.ts @@ -449,7 +449,11 @@ async function mapQaSuiteWithConcurrency( const index = nextIndex; nextIndex += 1; await waitForStartSlot(nextIndex < items.length); - results[index] = await mapper(items[index], index); + const item = items[index]; + if (item === undefined) { + return; + } + results[index] = await mapper(item, index); } }); await Promise.all(workers); diff --git a/extensions/qa-lab/src/suite-runtime-agent-media.ts b/extensions/qa-lab/src/suite-runtime-agent-media.ts index 1b624619a408..0aab8a8701f2 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-media.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-media.ts @@ -93,8 +93,7 @@ async function resolveGeneratedImagePath(params: { const requests = await fetchJson>( `${params.env.mock.baseUrl}/debug/requests`, ); - for (let index = requests.length - 1; index >= 0; index -= 1) { - const request = requests[index]; + for (const request of requests.toReversed()) { if (!(request.allInputText ?? "").includes(params.promptSnippet)) { continue; } diff --git a/extensions/qa-lab/src/suite-runtime-agent-process.ts b/extensions/qa-lab/src/suite-runtime-agent-process.ts index f30cb1537092..b5a130346be4 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-process.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-process.ts @@ -173,8 +173,7 @@ function parseQaCliJsonOutput(text: string, args: readonly string[]) { // Some startup repair logs are emitted on stdout before command JSON. const lines = cleaned.split(/\r?\n/); const candidates: unknown[] = []; - for (let index = 0; index < lines.length; index += 1) { - const line = lines[index]; + for (const [index, line] of lines.entries()) { const candidate = line.trimStart(); if (candidate !== line || (!candidate.startsWith("{") && !candidate.startsWith("["))) { continue; diff --git a/extensions/qa-lab/src/suite.ts b/extensions/qa-lab/src/suite.ts index dc135b955a67..387a2758fc7b 100644 --- a/extensions/qa-lab/src/suite.ts +++ b/extensions/qa-lab/src/suite.ts @@ -1419,7 +1419,9 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise scenario !== undefined, ); const completedScenarioDefinitions = completedScenarioResults.flatMap((scenario, index) => - scenario === undefined ? [] : [selectedScenarios[index]], + scenario === undefined || selectedScenarios[index] === undefined + ? [] + : [selectedScenarios[index]], ); if (partialScenarios.length === 0) { return; @@ -1852,15 +1854,16 @@ export async function runQaFlowSuite(params?: QaSuiteRunParams): Promise 0 + runtimeParityScenario ? await captureRuntimeParityCell({ runtime: params.forcedRuntime, gateway: activeGateway, - scenarioResult: scenarios[0], + scenarioResult: runtimeParityScenario, wallClockMs: Math.max(1, Date.now() - startedAt.getTime()), mockBaseUrl: activeMock?.baseUrl, }) diff --git a/extensions/qa-lab/src/tool-coverage-report.ts b/extensions/qa-lab/src/tool-coverage-report.ts index d9f852c44f0c..511de47ac212 100644 --- a/extensions/qa-lab/src/tool-coverage-report.ts +++ b/extensions/qa-lab/src/tool-coverage-report.ts @@ -1,4 +1,5 @@ // Qa Lab plugin module implements tool coverage report behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { isRecord, normalizeOptionalString as readString, @@ -203,7 +204,11 @@ function buildRow(params: { const metadata = params.group.scenarios .map(readScenarioRuntimeToolCoverageMetadata) .find((entry) => entry.required); - const fallbackMetadata = readScenarioRuntimeToolCoverageMetadata(params.group.scenarios[0]); + const firstScenario = expectDefined( + params.group.scenarios[0], + `QA tool fixture group ${params.group.tool} scenario`, + ); + const fallbackMetadata = readScenarioRuntimeToolCoverageMetadata(firstScenario); const rowMetadata = metadata ?? fallbackMetadata; const runtimeToolName = params.group.scenarios.map(readScenarioRuntimeToolName).find(Boolean); return { diff --git a/extensions/qa-lab/src/tool-search-gateway.fixture.ts b/extensions/qa-lab/src/tool-search-gateway.fixture.ts index 3cb834d5fc24..b3c815ca0677 100644 --- a/extensions/qa-lab/src/tool-search-gateway.fixture.ts +++ b/extensions/qa-lab/src/tool-search-gateway.fixture.ts @@ -483,14 +483,14 @@ export function assertToolSearchLaneResults(params: { normal.providerPlannedTools.includes(targetTool) && normal.gatewayOutputText.includes("FAKE_PLUGIN_OK") && normal.gatewayOutputText.includes(targetTool) && - normal.sessionLogToolMentions[targetTool] > 0, + (normal.sessionLogToolMentions[targetTool] ?? 0) > 0, `normal lane did not call ${targetTool}: ${laneDebug()}`, ); assert( code.providerPlannedTools.includes("tool_search_code") && code.gatewayOutputText.includes("FAKE_PLUGIN_OK") && code.gatewayOutputText.includes(targetTool) && - code.sessionLogToolMentions[targetTool] > 0, + (code.sessionLogToolMentions[targetTool] ?? 0) > 0, `code lane did not bridge-call ${targetTool}: ${laneDebug()}`, ); assert( @@ -506,7 +506,8 @@ export function assertToolSearchLaneResults(params: { `expected Tool Search request to be smaller: normal=${normal.providerRawBytes} code=${code.providerRawBytes}`, ); assert( - code.sessionLogToolMentions.tool_search_code > 0 && code.sessionLogToolMentions[targetTool] > 0, + (code.sessionLogToolMentions.tool_search_code ?? 0) > 0 && + (code.sessionLogToolMentions[targetTool] ?? 0) > 0, "code lane session log did not record bridge and target tool mentions", ); assert( diff --git a/extensions/qa-lab/web/src/app.ts b/extensions/qa-lab/web/src/app.ts index 0af085cde788..745eafcf50c1 100644 --- a/extensions/qa-lab/web/src/app.ts +++ b/extensions/qa-lab/web/src/app.ts @@ -267,11 +267,11 @@ export async function createQaLabApp(root: HTMLDivElement) { const ev = state.snapshot?.events; return JSON.stringify({ mc: msgs?.length ?? 0, - lm: msgs && msgs.length > 0 ? msgs[msgs.length - 1].id : null, + lm: msgs?.at(-1)?.id ?? null, cc: state.snapshot?.conversations.length ?? 0, tc: state.snapshot?.threads.length ?? 0, ec: ev?.length ?? 0, - lc: ev && ev.length > 0 ? ev[ev.length - 1].cursor : -1, + lc: ev?.at(-1)?.cursor ?? -1, rs: state.bootstrap?.runner.status, ra: state.bootstrap?.runner.startedAt, rf: state.bootstrap?.runner.finishedAt, diff --git a/extensions/qa-lab/web/src/ui-render.ts b/extensions/qa-lab/web/src/ui-render.ts index 2ba892a81b80..01d2c5268910 100644 --- a/extensions/qa-lab/web/src/ui-render.ts +++ b/extensions/qa-lab/web/src/ui-render.ts @@ -1916,7 +1916,7 @@ function renderProducerContextFile(params: { function formatMatrixLabel(id: string): string { return id .split("-") - .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part)) + .map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part)) .join(" "); } @@ -2455,7 +2455,7 @@ function renderCaptureView(state: UiState): string { state.captureTimelineSparklineMode === "lane-relative" ? laneSpanMs : totalSpanMs; const rawIndex = spanMs <= 0 ? 0 : Math.floor(((event.ts - spanStart) / spanMs) * binCount); const index = Math.max(0, Math.min(binCount - 1, rawIndex)); - bins[index] += 1; + bins[index] = (bins[index] ?? 0) + 1; } const maxBin = Math.max(...bins, 1); return `
@@ -2819,7 +2819,10 @@ function renderCaptureView(state: UiState): string { }, ]; if (!availableDetailViews.some((view) => view.recommended && view.available)) { - availableDetailViews[0].recommended = true; + const overviewView = availableDetailViews.find((view) => view.value === "overview"); + if (overviewView) { + overviewView.recommended = true; + } } const preferredDetailView = state.capturePreferredDetailView; const effectiveDetailView = availableDetailViews.some( @@ -3582,10 +3585,14 @@ function renderCaptureView(state: UiState): string { const leftPct = ((event.ts - minTs) / totalSpanMs) * 100; const leftPx = (leftPct / 100) * timelineTrackWidthPx; let rowIndex = 0; - while ( - rowIndex < rowRightEdges.length && - rowRightEdges[rowIndex] > leftPx - markerGapPx - ) { + while (rowIndex < rowRightEdges.length) { + const rowRightEdge = rowRightEdges[rowIndex]; + if ( + rowRightEdge === undefined || + rowRightEdge <= leftPx - markerGapPx + ) { + break; + } rowIndex += 1; } rowRightEdges[rowIndex] = leftPx + markerGapPx; @@ -3679,8 +3686,13 @@ function renderCaptureView(state: UiState): string { if (markers.length < 2) { return []; } - return markers.slice(1).map((marker, index) => { - const previous = markers[index]; + const followingMarkers = markers.slice(1).values(); + return markers.slice(0, -1).flatMap((previous) => { + const following = followingMarkers.next(); + if (following.done) { + return []; + } + const marker = following.value; const dx = marker.leftPx - previous.leftPx; const dy = marker.topPx - previous.topPx; const length = Math.sqrt(dx * dx + dy * dy); @@ -3694,10 +3706,12 @@ function renderCaptureView(state: UiState): string { const paired = pairedEventKey != null && captureEventKey(marker.event) === pairedEventKey; - return `
`; + >
`, + ]; }); }) .join(""); diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts index 278d2853d59f..a4c935e2bbc0 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts @@ -41,7 +41,8 @@ function isMatrixQaCliSecretPositionalArg(args: string[], index: number): boolea function redactMatrixQaCliArgs(args: string[]): string[] { return args.map((arg, index) => { - const [flag] = arg.split("=", 1); + const equalsIndex = arg.indexOf("="); + const flag = equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg; if (MATRIX_QA_CLI_SECRET_ARG_FLAGS.has(flag) && arg.includes("=")) { return `${flag}=[REDACTED]`; } diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-state-files.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-state-files.ts index 44184e1f5cf1..bff0768b1f5e 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-state-files.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-state-files.ts @@ -467,7 +467,10 @@ export async function waitForMatrixSyncStoreWithCursor(params: { ...(params.userId ? { userId: params.userId } : {}), }); if (sqliteCursors.length > 0) { - return sqliteCursors[0]; + const cursor = sqliteCursors[0]; + if (cursor) { + return cursor; + } } const pathname = await resolveBestMatrixStateFile({ context: params.context, diff --git a/extensions/qa-matrix/src/substrate/client.ts b/extensions/qa-matrix/src/substrate/client.ts index 61939214de1c..57f87e7b4b3e 100644 --- a/extensions/qa-matrix/src/substrate/client.ts +++ b/extensions/qa-matrix/src/substrate/client.ts @@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto"; import { setTimeout as sleep } from "node:timers/promises"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import { uniqueStrings, uniqueValues } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { MatrixQaObservedEvent } from "./events.js"; @@ -799,7 +800,7 @@ async function provisionMatrixQaTopology(params: { for (const room of params.spec.rooms) { const members = resolveTopologyMemberAccounts(params.accounts, room.members); - const creator = members[0]; + const creator = expectDefined(members[0], "Matrix QA room creator"); const invitees = members.slice(1); const creatorClient = createMatrixQaClient({ accessToken: creator.account.accessToken, diff --git a/extensions/qqbot/src/bridge/setup/finalize.ts b/extensions/qqbot/src/bridge/setup/finalize.ts index bfae4e06f893..96a19b620465 100644 --- a/extensions/qqbot/src/bridge/setup/finalize.ts +++ b/extensions/qqbot/src/bridge/setup/finalize.ts @@ -1,5 +1,6 @@ -// Qqbot plugin module implements finalize behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +// Qqbot plugin module implements finalize behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup"; import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; @@ -55,8 +56,7 @@ async function linkViaQrCode(params: { let next = params.cfg; - for (let i = 0; i < accounts.length; i++) { - const { appId, appSecret } = accounts[i]; + for (const [i, { appId, appSecret }] of accounts.entries()) { // use current account id for first account, and use app id for subsequent accounts const targetAccountId = i === 0 ? params.accountId : appId; @@ -67,7 +67,8 @@ async function linkViaQrCode(params: { } if (accounts.length === 1) { - params.runtime.log(`✔ QQ Bot 绑定成功!(AppID: ${accounts[0].appId})`); + const account = expectDefined(accounts.at(0), "single linked QQ Bot account"); + params.runtime.log(`✔ QQ Bot 绑定成功!(AppID: ${account.appId})`); } else { const idList = accounts.map((a) => a.appId).join(", "); params.runtime.log(`✔ ${accounts.length} 个 QQ Bot 绑定成功!(AppID: ${idList})`); diff --git a/extensions/qqbot/src/engine/approval/index.ts b/extensions/qqbot/src/engine/approval/index.ts index 764327f67175..b71c5e0d5b7d 100644 --- a/extensions/qqbot/src/engine/approval/index.ts +++ b/extensions/qqbot/src/engine/approval/index.ts @@ -265,8 +265,13 @@ export function resolveApprovalTarget( if (!m) { return null; } - const type: ChatScope = m[1].toLowerCase() === "group" ? "group" : "c2c"; - return { type, id: m[2] }; + const scope = m[1]; + const id = m[2]; + if (scope === undefined || id === undefined) { + return null; + } + const type: ChatScope = scope.toLowerCase() === "group" ? "group" : "c2c"; + return { type, id }; } // ============ Interaction Parser ============ @@ -284,8 +289,18 @@ export function parseApprovalButtonData(buttonData: string): ParsedApprovalActio return null; } let approvalId: string; + const kind = m[1]; + const encodedId = m[2]; + const decision = m[3]; + if ( + (kind !== "exec" && kind !== "plugin") || + encodedId === undefined || + (decision !== "allow-once" && decision !== "allow-always" && decision !== "deny") + ) { + return null; + } try { - approvalId = decodeURIComponent(m[2]); + approvalId = decodeURIComponent(encodedId); } catch { return null; } @@ -294,7 +309,7 @@ export function parseApprovalButtonData(buttonData: string): ParsedApprovalActio } return { approvalId, - approvalKind: m[1] as ApprovalKind, - decision: m[3] as ApprovalDecision, + approvalKind: kind, + decision, }; } diff --git a/extensions/qqbot/src/engine/config/resolve.ts b/extensions/qqbot/src/engine/config/resolve.ts index a7ddf64ea6c4..6850d9bce439 100644 --- a/extensions/qqbot/src/engine/config/resolve.ts +++ b/extensions/qqbot/src/engine/config/resolve.ts @@ -121,8 +121,9 @@ export function resolveDefaultAccountId(cfg: Record): string { } if (qqbot?.accounts) { const ids = Object.keys(qqbot.accounts); - if (ids.length > 0) { - return ids[0]; + const firstId = ids.at(0); + if (firstId !== undefined) { + return firstId; } } return DEFAULT_ACCOUNT_ID; diff --git a/extensions/qqbot/src/engine/gateway/message-queue.ts b/extensions/qqbot/src/engine/gateway/message-queue.ts index 90400221e135..702572c843af 100644 --- a/extensions/qqbot/src/engine/gateway/message-queue.ts +++ b/extensions/qqbot/src/engine/gateway/message-queue.ts @@ -1,4 +1,5 @@ // Qqbot plugin module implements message queue behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { formatErrorMessage } from "../utils/format.js"; @@ -132,11 +133,11 @@ export function mergeGroupMessages(batch: QueuedMessage[]): QueuedMessage { throw new Error("mergeGroupMessages: empty batch"); } if (batch.length === 1) { - return batch[0]; + return expectDefined(batch.at(0), "single-message merge batch"); } - const first = batch[0]; - const last = batch[batch.length - 1]; + const first = expectDefined(batch.at(0), "non-empty merge batch first message"); + const last = expectDefined(batch.at(-1), "non-empty merge batch last message"); const mergedContent = batch .map((m) => `[${m.senderName ?? m.senderId}]: ${m.content}`) diff --git a/extensions/qqbot/src/engine/gateway/reconnect.ts b/extensions/qqbot/src/engine/gateway/reconnect.ts index e53d397603af..56be1aea2c64 100644 --- a/extensions/qqbot/src/engine/gateway/reconnect.ts +++ b/extensions/qqbot/src/engine/gateway/reconnect.ts @@ -7,6 +7,7 @@ * Zero external dependencies — uses only the constants from `./constants.ts`. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { EngineLogger } from "../types.js"; import { RECONNECT_DELAYS, @@ -78,7 +79,11 @@ export class ReconnectState { */ getNextDelay(customDelay?: number): number { const delay = - customDelay ?? RECONNECT_DELAYS[Math.min(this.attempts, RECONNECT_DELAYS.length - 1)]; + customDelay ?? + expectDefined( + RECONNECT_DELAYS[Math.min(this.attempts, RECONNECT_DELAYS.length - 1)], + "non-empty reconnect delay schedule", + ); this.attempts++; this.log?.debug?.(`Reconnecting ${this.accountId} in ${delay}ms (attempt ${this.attempts})`); return delay; @@ -141,13 +146,14 @@ export class ReconnectState { [GatewayCloseCode.SEQ_OUT_OF_RANGE]: "invalid seq on resume", [GatewayCloseCode.SESSION_TIMEOUT]: "session timed out", }; - this.log?.info(`Error ${code} (${codeDesc[code]}), will re-identify`); + const reason = expectDefined(codeDesc[code], "recognized session close code"); + this.log?.info(`Error ${code} (${reason}), will re-identify`); return { shouldReconnect: !isAborted, clearSession: true, refreshToken: true, fatal: false, - reason: codeDesc[code], + reason, }; } diff --git a/extensions/qqbot/src/engine/gateway/stages/assembly-stage.ts b/extensions/qqbot/src/engine/gateway/stages/assembly-stage.ts index a800a0269e2e..1cfc25435db5 100644 --- a/extensions/qqbot/src/engine/gateway/stages/assembly-stage.ts +++ b/extensions/qqbot/src/engine/gateway/stages/assembly-stage.ts @@ -15,6 +15,7 @@ * sees directly. */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { buildMergedMessageContext, formatAttachmentTags, @@ -45,7 +46,7 @@ export function buildUserMessage(input: BuildUserMessageInput): string { // ---- Merged group turn ---- if (groupInfo?.isMerged && groupInfo.mergedMessages?.length) { const preceding = groupInfo.mergedMessages.slice(0, -1); - const lastMsg = groupInfo.mergedMessages[groupInfo.mergedMessages.length - 1]; + const lastMsg = expectDefined(groupInfo.mergedMessages.at(-1), "non-empty merged group turn"); const atYouTag = groupInfo.gate.effectiveWasMentioned ? " (@you)" : ""; const envelopeParts = preceding.map((m) => `[${formatSenderLabel(m)}] ${formatSub(m)}`); diff --git a/extensions/qqbot/src/engine/gateway/stages/envelope-stage.ts b/extensions/qqbot/src/engine/gateway/stages/envelope-stage.ts index 5ae2a6e6d941..4bb222e73fa9 100644 --- a/extensions/qqbot/src/engine/gateway/stages/envelope-stage.ts +++ b/extensions/qqbot/src/engine/gateway/stages/envelope-stage.ts @@ -117,6 +117,9 @@ export function classifyMedia(processed: ProcessedAttachments): MediaClassificat for (let i = 0; i < processed.imageUrls.length; i++) { const u = processed.imageUrls[i]; const t = processed.imageMediaTypes[i] ?? "image/png"; + if (u === undefined) { + continue; + } if (u.startsWith("http://") || u.startsWith("https://")) { remoteMediaUrls.push(u); remoteMediaTypes.push(t); diff --git a/extensions/qqbot/src/engine/messaging/decode-media-path.ts b/extensions/qqbot/src/engine/messaging/decode-media-path.ts index b37893bd2c39..1297b95d64e6 100644 --- a/extensions/qqbot/src/engine/messaging/decode-media-path.ts +++ b/extensions/qqbot/src/engine/messaging/decode-media-path.ts @@ -76,7 +76,7 @@ export function decodeMediaPath(raw: string, log?: EngineLogger): string { if (code <= 0xff) { bytes.push(code); } else { - const charBytes = Buffer.from(decoded[i], "utf8"); + const charBytes = Buffer.from(decoded.charAt(i), "utf8"); bytes.push(...charBytes); } } diff --git a/extensions/qqbot/src/engine/messaging/markdown-table-chunking.ts b/extensions/qqbot/src/engine/messaging/markdown-table-chunking.ts index 2ecd3f78d90a..93703d17a512 100644 --- a/extensions/qqbot/src/engine/messaging/markdown-table-chunking.ts +++ b/extensions/qqbot/src/engine/messaging/markdown-table-chunking.ts @@ -262,7 +262,8 @@ class QQBotMarkdownChunkingState { const bodyLines = pendingFenceOpenLine ? [...this.textLines] : this.textLines.slice(1); this.textLines = []; this.pendingTextFenceOpenLine = null; - if (bodyLines.length > 0 && isClosingFenceLine(bodyLines[bodyLines.length - 1], fence)) { + const lastBodyLine = bodyLines.at(-1); + if (lastBodyLine !== undefined && isClosingFenceLine(lastBodyLine, fence)) { bodyLines.pop(); } if (this.activeFence && bodyLines.length === 0) { diff --git a/extensions/qqbot/src/engine/messaging/media-source.ts b/extensions/qqbot/src/engine/messaging/media-source.ts index 8e2c5788d70a..711ec4b82c4e 100644 --- a/extensions/qqbot/src/engine/messaging/media-source.ts +++ b/extensions/qqbot/src/engine/messaging/media-source.ts @@ -81,7 +81,9 @@ function tryParseDataUrl(value: string): { mime: string; data: string } | null { if (!m) { return null; } - return { mime: m[1], data: m[2] }; + const mime = m[1]; + const data = m[2]; + return mime === undefined || data === undefined ? null : { mime, data }; } // ============ Local file safe open ============ diff --git a/extensions/qqbot/src/engine/messaging/outbound-deliver.ts b/extensions/qqbot/src/engine/messaging/outbound-deliver.ts index 03629b930647..16a1e1a45479 100644 --- a/extensions/qqbot/src/engine/messaging/outbound-deliver.ts +++ b/extensions/qqbot/src/engine/messaging/outbound-deliver.ts @@ -818,7 +818,7 @@ async function sendMarkdownReply( } // Handle public image URLs — format as markdown images with dimensions. - const existingMdUrls = new Set(mdMatches.map((m) => m[2])); + const existingMdUrls = new Set(mdMatches.flatMap((m) => (m[2] === undefined ? [] : [m[2]]))); const imagesToAppend: string[] = []; for (const url of httpImageUrls) { @@ -841,6 +841,9 @@ async function sendMarkdownReply( for (const m of mdMatches) { const fullMatch = m[0]; const imgUrl = m[2]; + if (fullMatch === undefined || imgUrl === undefined) { + continue; + } const isRemoteHttpUrl = isHttpUrl(imgUrl); if (isRemoteHttpUrl && !hasQQBotImageSize(fullMatch)) { try { diff --git a/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts b/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts index 1b11bc2251aa..70e5a5ce940d 100644 --- a/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts +++ b/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts @@ -208,7 +208,7 @@ export async function handleStructuredPayload( type StructuredPayloadMediaType = "image" | "video" | "file"; function formatMediaTypeLabel(mediaType: StructuredPayloadMediaType): string { - return mediaType[0].toUpperCase() + mediaType.slice(1); + return mediaType.charAt(0).toUpperCase() + mediaType.slice(1); } function validateStructuredPayloadLocalPath( diff --git a/extensions/qqbot/src/engine/messaging/streaming-media-send.ts b/extensions/qqbot/src/engine/messaging/streaming-media-send.ts index 83ff3b2c7c0c..da0eb55ee7ed 100644 --- a/extensions/qqbot/src/engine/messaging/streaming-media-send.ts +++ b/extensions/qqbot/src/engine/messaging/streaming-media-send.ts @@ -103,7 +103,7 @@ function fixPathEncoding( if (code <= 0xff) { bytes.push(code); } else { - const charBytes = Buffer.from(decoded[i], "utf8"); + const charBytes = Buffer.from(decoded.charAt(i), "utf8"); bytes.push(...charBytes); } } @@ -141,7 +141,11 @@ function isInsideCodeBlock(text: string, position: number): boolean { let openFence: { pos: number; ticks: number } | null = null; while ((fenceMatch = fenceRegex.exec(text)) !== null) { - const ticks = fenceMatch[1].length; + const ticksText = fenceMatch[1]; + if (ticksText === undefined) { + continue; + } + const ticks = ticksText.length; if (!openFence) { openFence = { pos: fenceMatch.index, ticks }; } else if (ticks >= openFence.ticks) { @@ -206,7 +210,11 @@ export function findFirstClosedMediaTag( } const textBefore = text.slice(0, match.index); - const tagName = match[1].toLowerCase(); + const rawTagName = match[1]; + if (rawTagName === undefined) { + continue; + } + const tagName = rawTagName.toLowerCase(); let mediaPath = match[2]?.trim() ?? ""; mediaPath = normalizePath(mediaPath); @@ -446,7 +454,7 @@ export function stripIncompleteMediaTag(text: string): [safeText: string, hasInc let fallbackPos = -1; // 最右边触发回溯的 < 的位置 for (let i = lastLine.length - 1; i >= 0; i--) { - const ch = lastLine[i]; + const ch = lastLine.charAt(i); if (ch !== "<" && ch !== "\uFF1C") { continue; } @@ -461,7 +469,11 @@ export function stripIncompleteMediaTag(text: string): [safeText: string, hasInc if (!nameMatch || isClosing) { continue; } - const cand = nameMatch[1].toLowerCase(); + const candidateName = nameMatch[1]; + if (candidateName === undefined) { + continue; + } + const cand = candidateName.toLowerCase(); if (!isMedia(cand)) { continue; } @@ -505,6 +517,9 @@ export function stripIncompleteMediaTag(text: string): [safeText: string, hasInc } const tag = nameMatch[1]; + if (tag === undefined) { + continue; + } const restAfterName = nameStr.slice(tag.length); const hasGT = /[>>]/.test(restAfterName); diff --git a/extensions/qqbot/src/engine/tools/remind-logic.ts b/extensions/qqbot/src/engine/tools/remind-logic.ts index 864c4191e4ac..91a24089bb90 100644 --- a/extensions/qqbot/src/engine/tools/remind-logic.ts +++ b/extensions/qqbot/src/engine/tools/remind-logic.ts @@ -138,8 +138,12 @@ export function parseRelativeTime(timeStr: string): number | null { } matched = true; consumed = regex.lastIndex; - const value = Number.parseFloat(match[1]); + const valueText = match[1]; const unit = match[2]; + if (valueText === undefined || unit === undefined) { + return null; + } + const value = Number.parseFloat(valueText); switch (unit) { case "d": totalMs += value * 86_400_000; diff --git a/extensions/qqbot/src/engine/utils/audio.ts b/extensions/qqbot/src/engine/utils/audio.ts index 1718d20a1f18..e4adcaacfa7a 100644 --- a/extensions/qqbot/src/engine/utils/audio.ts +++ b/extensions/qqbot/src/engine/utils/audio.ts @@ -11,6 +11,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { readRegularFileSync } from "openclaw/plugin-sdk/security-runtime"; import { formatErrorMessage } from "./format.js"; import { debugLog, debugError, debugWarn } from "./log.js"; @@ -395,14 +396,14 @@ async function wasmDecodeMp3ToPCM(buf: Buffer, targetRate: number): Promise= 30 && buffer[23] === 0x9d && buffer[24] === 0x01 && buffer[25] === 0x2a) { + if (buffer.length >= 30 && buffer.subarray(23, 26).equals(Buffer.from([0x9d, 0x01, 0x2a]))) { const width = buffer.readUInt16LE(26) & 0x3fff; const height = buffer.readUInt16LE(28) & 0x3fff; return { width, height }; @@ -130,8 +130,8 @@ function parseWebpSize(buffer: Buffer): ImageSize | null { if (chunkType === "VP8X") { if (buffer.length >= 30) { // Width and height live at 24..26 and 27..29 as 24-bit little-endian values. - const width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1; - const height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1; + const width = buffer.readUIntLE(24, 3) + 1; + const height = buffer.readUIntLE(27, 3) + 1; return { width, height }; } } @@ -211,6 +211,9 @@ function getImageSizeFromDataUrl(dataUrl: string): ImageSize | null { } const base64Data = matches[1]; + if (base64Data === undefined) { + return null; + } const buffer = Buffer.from(base64Data, "base64"); const size = parseImageSize(buffer); diff --git a/extensions/qqbot/src/engine/utils/upload-cache.ts b/extensions/qqbot/src/engine/utils/upload-cache.ts index 4981beebab10..7387bd4d5bcd 100644 --- a/extensions/qqbot/src/engine/utils/upload-cache.ts +++ b/extensions/qqbot/src/engine/utils/upload-cache.ts @@ -81,8 +81,8 @@ export function setCachedFileInfo( } if (cache.size >= MAX_CACHE_SIZE) { const keys = Array.from(cache.keys()); - for (let i = 0; i < keys.length / 2; i++) { - cache.delete(keys[i]); + for (const key of keys.slice(0, Math.ceil(keys.length / 2))) { + cache.delete(key); } } } diff --git a/extensions/signal/src/approval-reactions.ts b/extensions/signal/src/approval-reactions.ts index 08dedeec045b..44aa4c2d9f79 100644 --- a/extensions/signal/src/approval-reactions.ts +++ b/extensions/signal/src/approval-reactions.ts @@ -414,6 +414,9 @@ function extractSignalApprovalPromptBinding(text: string): { return null; } const approvalId = idHeaderMatch[1]; + if (!approvalId) { + return null; + } const approvalKind = resolveStandaloneApprovalPromptKind(text); if (!approvalKind) { return null; @@ -421,10 +424,12 @@ function extractSignalApprovalPromptBinding(text: string): { const allowedDecisions: ExecApprovalReplyDecision[] = []; for (const line of lines) { const match = line.match(APPROVE_REPLY_COMMAND_LINE_RE); - if (!match || match[1] !== approvalId) { + const commandApprovalId = match?.[1]; + const decisionList = match?.[2]; + if (commandApprovalId !== approvalId || !decisionList) { continue; } - for (const decisionText of match[2].split(/[\s|,]+/)) { + for (const decisionText of decisionList.split(/[\s|,]+/)) { const decision = normalizeApprovalDecision(decisionText); if (decision && !allowedDecisions.includes(decision)) { allowedDecisions.push(decision); diff --git a/extensions/signal/src/client-container.ts b/extensions/signal/src/client-container.ts index 42a37856ae7c..5c978ab48c94 100644 --- a/extensions/signal/src/client-container.ts +++ b/extensions/signal/src/client-container.ts @@ -450,8 +450,7 @@ function renderContainerStyledText( ...new Set([0, text.length, ...spans.flatMap((span) => [span.start, span.end])]), ].toSorted((a, b) => a - b); let rendered = ""; - for (let i = 0; i < positions.length; i += 1) { - const pos = positions[i]; + for (const [index, pos] of positions.entries()) { for (const span of spans .filter((candidate) => candidate.end === pos) .toSorted((a, b) => b.start - a.start)) { @@ -462,7 +461,7 @@ function renderContainerStyledText( .toSorted((a, b) => b.end - a.end)) { rendered += span.marker; } - const next = positions[i + 1]; + const next = positions[index + 1]; if (next !== undefined && next > pos) { rendered += escapeContainerStyledText(text.slice(pos, next)); } @@ -705,9 +704,12 @@ export async function containerRpcRequest( : []; const textStylesRaw = p["text-style"] as string[] | undefined; - const textStyles = textStylesRaw?.map((s) => { + const textStyles = textStylesRaw?.flatMap((s) => { const [start, length, style] = s.split(":"); - return { start: Number(start), length: Number(length), style }; + if (start === undefined || length === undefined || style === undefined) { + return []; + } + return [{ start: Number(start), length: Number(length), style }]; }); const quoteTimestamp = normalizeContainerQuoteTimestamp( diff --git a/extensions/signal/src/client.ts b/extensions/signal/src/client.ts index aca8d4f64d59..d5895e981547 100644 --- a/extensions/signal/src/client.ts +++ b/extensions/signal/src/client.ts @@ -377,6 +377,9 @@ export async function streamSignalEvents(params: { return; } const [rawField, ...rest] = line.split(":"); + if (rawField === undefined) { + return; + } const field = rawField.trim(); const rawValue = rest.join(":"); const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue; diff --git a/extensions/slack/src/channel-migration.ts b/extensions/slack/src/channel-migration.ts index 65bbce26c7e0..3114070617f9 100644 --- a/extensions/slack/src/channel-migration.ts +++ b/extensions/slack/src/channel-migration.ts @@ -53,7 +53,11 @@ export function migrateSlackChannelsInPlace( if (Object.hasOwn(channels, newChannelId)) { return { migrated: false, skippedExisting: true }; } - channels[newChannelId] = channels[oldChannelId]; + const channelConfig = channels[oldChannelId]; + if (!channelConfig) { + return { migrated: false, skippedExisting: false }; + } + channels[newChannelId] = channelConfig; delete channels[oldChannelId]; return { migrated: true, skippedExisting: false }; } diff --git a/extensions/slack/src/interactive-replies.ts b/extensions/slack/src/interactive-replies.ts index c4359fdf9842..9e412d6926a3 100644 --- a/extensions/slack/src/interactive-replies.ts +++ b/extensions/slack/src/interactive-replies.ts @@ -132,8 +132,15 @@ function buildSelectBlock( return null; } const [first, second] = parts; + if (!first || (parts.length >= 2 && !second)) { + return null; + } const placeholder = parts.length >= 2 ? first : "Choose an option"; - const choices = parseChoices(parts.length >= 2 ? second : first, SLACK_SELECT_MAX_ITEMS); + const choicesText = parts.length >= 2 ? second : first; + if (!choicesText) { + return null; + } + const choices = parseChoices(choicesText, SLACK_SELECT_MAX_ITEMS); if (choices.length === 0) { return null; } @@ -220,6 +227,9 @@ export function compileSlackInteractiveReplies(payload: ReplyPayload): ReplyPayl const matchText = match[0]; const directiveType = match[1]; const body = match[2]; + if (!directiveType || !body) { + continue; + } const index = match.index ?? 0; const precedingText = text.slice(cursor, index); visibleTextParts.push(precedingText); diff --git a/extensions/slack/src/monitor/media.ts b/extensions/slack/src/monitor/media.ts index 8c67034c1c21..8524d8df4212 100644 --- a/extensions/slack/src/monitor/media.ts +++ b/extensions/slack/src/monitor/media.ts @@ -339,16 +339,17 @@ async function mapLimit( } const results: R[] = []; results.length = items.length; - let nextIndex = 0; + const pendingItems = items.entries(); const workerCount = Math.max(1, Math.min(limit, items.length)); await Promise.all( Array.from({ length: workerCount }, async () => { while (true) { - const idx = nextIndex++; - if (idx >= items.length) { + const next = pendingItems.next(); + if (next.done) { return; } - results[idx] = await fn(items[idx]); + const [idx, item] = next.value; + results[idx] = await fn(item); } }), ); diff --git a/extensions/slack/src/monitor/replies.ts b/extensions/slack/src/monitor/replies.ts index c5a70dbb50b9..e3d080e1edb7 100644 --- a/extensions/slack/src/monitor/replies.ts +++ b/extensions/slack/src/monitor/replies.ts @@ -557,7 +557,8 @@ export async function deliverSlackSlashReplies(params: { throw failure; }; const initialRemaining = responseBudget.remaining(); - if (initialRemaining !== undefined && minimumRemainingCalls[0] > initialRemaining) { + const initialMinimumCalls = minimumCalls.reduce((total, calls) => total + calls, 0); + if (initialRemaining !== undefined && initialMinimumCalls > initialRemaining) { await failOversizedDelivery(); } diff --git a/extensions/slack/src/resolve-users.ts b/extensions/slack/src/resolve-users.ts index 6518fba7170f..aaadf692d611 100644 --- a/extensions/slack/src/resolve-users.ts +++ b/extensions/slack/src/resolve-users.ts @@ -137,6 +137,9 @@ function resolveSlackUserFromMatches( .map((user) => ({ user, score: scoreSlackUser(user, parsed) })) .toSorted((a, b) => b.score - a.score); const best = scored[0]?.user ?? matches[0]; + if (!best) { + return { input, resolved: false }; + } return { input, resolved: true, diff --git a/extensions/telegram/src/account-throttler.ts b/extensions/telegram/src/account-throttler.ts index 74f99bb2538b..de0a3b20210c 100644 --- a/extensions/telegram/src/account-throttler.ts +++ b/extensions/telegram/src/account-throttler.ts @@ -1,4 +1,5 @@ // Telegram plugin module implements account throttler behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime"; import { apiThrottler } from "./bot.runtime.js"; @@ -71,7 +72,10 @@ class GroupFairQueue { private takeNext(): QueuedApiRequest | undefined { for (let remaining = this.laneOrder.length; remaining > 0; remaining -= 1) { this.nextLaneIndex %= this.laneOrder.length; - const laneKey = this.laneOrder[this.nextLaneIndex]; + const laneKey = expectDefined( + this.laneOrder[this.nextLaneIndex], + "non-empty Telegram throttle lane order", + ); const queue = this.lanes.get(laneKey); if (!queue || queue.length === 0) { this.lanes.delete(laneKey); diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index 9e6d8176804f..c2c129ec2cf0 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -35,6 +35,7 @@ import { resolvePluginConversationBindingApproval, } from "openclaw/plugin-sdk/conversation-runtime"; import { isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue"; import { applyModelOverrideToSessionEntry, @@ -770,7 +771,7 @@ export const registerTelegramHandlers = ({ settleSpooledReplayParticipants(spooledReplayParticipants, { kind: "skipped" }); return; } - const first = entries[0]; + const first = expectDefined(entries.at(0), "multi-entry Telegram debounce batch"); const promptContextMinTimestampMs = latestPromptContextMinTimestampMs( ...entries.map((entry) => entry.promptContextMinTimestampMs), ); diff --git a/extensions/telegram/src/fetch.ts b/extensions/telegram/src/fetch.ts index 107ae637fac8..07ea00f08779 100644 --- a/extensions/telegram/src/fetch.ts +++ b/extensions/telegram/src/fetch.ts @@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto"; import * as dns from "node:dns"; import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-contracts"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { createHttp1EnvHttpProxyAgent, createHttp1ProxyAgent, @@ -223,10 +224,15 @@ function shouldBypassEnvProxyForTelegramApi(env: NodeJS.ProcessEnv = process.env continue; } const parsed = entry.match(/^(.+):(\d+)$/); + const parsedHostname = parsed?.[1]; + const parsedPort = parsed?.[2]; + if (parsed && (parsedHostname === undefined || parsedPort === undefined)) { + continue; + } const entryHostname = normalizeLowercaseStringOrEmpty( - (parsed ? parsed[1] : entry).replace(/^\*?\./, ""), + (parsedHostname ?? entry).replace(/^\*?\./, ""), ); - const entryPort = parsed ? Number.parseInt(parsed[2], 10) : 0; + const entryPort = parsedPort === undefined ? 0 : Number.parseInt(parsedPort, 10); if (entryPort && entryPort !== targetPort) { continue; } @@ -678,7 +684,7 @@ export function resolveTelegramTransport( }; const getAttemptCooldownError = (attemptIndex: number): Error | null => { - const health = attemptHealth[attemptIndex]; + const health = expectDefined(attemptHealth[attemptIndex], "transport attempt health index"); if (!isFutureDateTimestampMs(health.unhealthyUntilMs)) { return null; } @@ -689,7 +695,7 @@ export function resolveTelegramTransport( if (!shouldUseTelegramTransportFallback(err)) { return; } - const health = attemptHealth[attemptIndex]; + const health = expectDefined(attemptHealth[attemptIndex], "transport attempt health index"); health.consecutiveFailures += 1; if (health.consecutiveFailures < TELEGRAM_TRANSPORT_ATTEMPT_FAILURE_THRESHOLD) { return; @@ -715,7 +721,10 @@ export function resolveTelegramTransport( if (nextIndex <= stickyAttemptIndex || nextIndex >= transportAttempts.length) { return false; } - const nextAttempt = transportAttempts[nextIndex]; + const nextAttempt = expectDefined( + transportAttempts[nextIndex], + "validated fallback attempt index", + ); if (nextAttempt.logMessage) { const reasonText = reason ? `, reason=${reason}` : ""; const logLine = `${nextAttempt.logMessage} (codes=${formatErrorCodes(err)}${reasonText})`; @@ -731,7 +740,7 @@ export function resolveTelegramTransport( }; const recordSuccessfulAttempt = (attemptIndex: number): void => { - const health = attemptHealth[attemptIndex]; + const health = expectDefined(attemptHealth[attemptIndex], "transport attempt health index"); health.consecutiveFailures = 0; health.cooldownMs = TELEGRAM_TRANSPORT_ATTEMPT_INITIAL_COOLDOWN_MS; health.unhealthyUntilMs = 0; @@ -811,7 +820,10 @@ export function resolveTelegramTransport( attemptIndex < transportAttempts.length; attemptIndex += 1 ) { - const attempt = transportAttempts[attemptIndex]; + const attempt = expectDefined( + transportAttempts[attemptIndex], + "transport attempt loop index", + ); if (attemptIndex > startIndex) { promoteStickyAttempt(attemptIndex, err); } diff --git a/extensions/telegram/src/format.ts b/extensions/telegram/src/format.ts index 5ff4dce0432f..34d338c2790b 100644 --- a/extensions/telegram/src/format.ts +++ b/extensions/telegram/src/format.ts @@ -1,5 +1,6 @@ -// Telegram helper module supports format behavior. import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-contracts"; +// Telegram helper module supports format behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { FILE_REF_EXTENSIONS_WITH_TLD, @@ -1097,7 +1098,10 @@ function normalizeTelegramRichMarkdownMedia( ); continue; } - const [, indent, alt, src, caption] = match; + const indent = expectDefined(match[1], "rich Markdown media indent capture"); + const alt = match[2]; + const src = expectDefined(match[3], "rich Markdown media source capture"); + const caption = match[4]; const img = `${escapeHtmlAttr(alt)}`; const figcaption = caption ? `
${escapeHtml(caption)}
` : ""; const placeholder = buildTelegramRichMarkdownMediaPlaceholder(mediaBlocks.length); diff --git a/extensions/telegram/src/group-migration.ts b/extensions/telegram/src/group-migration.ts index f5fde07728e9..672022772fb9 100644 --- a/extensions/telegram/src/group-migration.ts +++ b/extensions/telegram/src/group-migration.ts @@ -1,6 +1,7 @@ -// Telegram plugin module implements group migration behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { TelegramGroupConfig } from "openclaw/plugin-sdk/config-contracts"; +// Telegram plugin module implements group migration behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeAccountId } from "openclaw/plugin-sdk/routing"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; @@ -53,7 +54,7 @@ export function migrateTelegramGroupsInPlace( if (Object.hasOwn(groups, newChatId)) { return { migrated: false, skippedExisting: true }; } - groups[newChatId] = groups[oldChatId]; + groups[newChatId] = expectDefined(groups[oldChatId], "owned Telegram group config key"); delete groups[oldChatId]; return { migrated: true, skippedExisting: false }; } diff --git a/extensions/telegram/src/group-policy.ts b/extensions/telegram/src/group-policy.ts index 0847034078ac..619c574050fb 100644 --- a/extensions/telegram/src/group-policy.ts +++ b/extensions/telegram/src/group-policy.ts @@ -1,10 +1,11 @@ -// Telegram plugin module implements group policy behavior. import type { ChannelGroupContext } from "openclaw/plugin-sdk/channel-contract"; import { resolveChannelGroupRequireMention, resolveChannelGroupToolsPolicy, type GroupToolPolicyConfig, } from "openclaw/plugin-sdk/channel-policy"; +// Telegram plugin module implements group policy behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; function parseTelegramGroupId(value?: string | null) { const raw = value?.trim() ?? ""; @@ -12,16 +13,33 @@ function parseTelegramGroupId(value?: string | null) { return { chatId: undefined, topicId: undefined }; } const parts = raw.split(":").filter(Boolean); + const chatId = parts[0]; + const second = parts[1]; + const third = parts[2]; if ( parts.length >= 3 && - parts[1] === "topic" && - /^-?\d+$/.test(parts[0]) && - /^\d+$/.test(parts[2]) + second === "topic" && + chatId !== undefined && + /^-?\d+$/.test(chatId) && + third !== undefined && + /^\d+$/.test(third) ) { - return { chatId: parts[0], topicId: parts[2] }; + return { + chatId: expectDefined(chatId, "validated Telegram group chat id"), + topicId: expectDefined(third, "validated Telegram topic id"), + }; } - if (parts.length >= 2 && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) { - return { chatId: parts[0], topicId: parts[1] }; + if ( + parts.length >= 2 && + chatId !== undefined && + /^-?\d+$/.test(chatId) && + second !== undefined && + /^\d+$/.test(second) + ) { + return { + chatId: expectDefined(chatId, "validated Telegram group chat id"), + topicId: expectDefined(second, "validated Telegram topic id"), + }; } return { chatId: raw, topicId: undefined }; } diff --git a/extensions/telegram/src/model-buttons.ts b/extensions/telegram/src/model-buttons.ts index e39d2d3e75b5..79f351e65eaf 100644 --- a/extensions/telegram/src/model-buttons.ts +++ b/extensions/telegram/src/model-buttons.ts @@ -8,6 +8,7 @@ * - mdl_sel/{model} - select model (compact fallback when standard is >64 bytes) * - mdl_back - back to providers list */ +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { fitsTelegramCallbackData } from "./approval-callback-data.js"; @@ -136,7 +137,7 @@ export function resolveModelSelection(params: { if (matchingProviders.length === 1) { return { kind: "resolved", - provider: matchingProviders[0], + provider: expectDefined(matchingProviders.at(0), "single matching model provider"), model: params.callback.model, }; } diff --git a/extensions/telegram/src/rich-message.ts b/extensions/telegram/src/rich-message.ts index d82c7de16b6a..26348504d48b 100644 --- a/extensions/telegram/src/rich-message.ts +++ b/extensions/telegram/src/rich-message.ts @@ -1,4 +1,3 @@ -// Telegram rich message helpers isolate Bot API 10.1 calls until grammY types catch up. import type { Bot } from "grammy"; import type { ForceReply, @@ -9,6 +8,8 @@ import type { ReplyParameters, } from "grammy/types"; import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-contracts"; +// Telegram rich message helpers isolate Bot API 10.1 calls until grammY types catch up. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { chunkMarkdownTextWithMode, type ChunkMode } from "openclaw/plugin-sdk/reply-chunking"; import { escapeTelegramHtml, @@ -300,8 +301,8 @@ function parseRichMarkdownFenceSpans(markdown: string): RichMarkdownFenceSpan[] const line = markdown.slice(offset, lineEnd); const match = line.match(/^( {0,3})(`{3,}|~{3,})/); if (match) { - const marker = match[2]; - const markerChar = marker[0]; + const marker = expectDefined(match[2], "Markdown fence marker capture"); + const markerChar = marker.charAt(0); if (!open) { open = { start: offset, markerChar, markerLength: marker.length }; } else if (open.markerChar === markerChar && marker.length >= open.markerLength) { diff --git a/extensions/telegram/src/targets.ts b/extensions/telegram/src/targets.ts index a59fbc61562e..d1f8da78a7df 100644 --- a/extensions/telegram/src/targets.ts +++ b/extensions/telegram/src/targets.ts @@ -105,7 +105,12 @@ export function parseTelegramTarget(to: string): TelegramTarget { const topicMatch = /^(.+?):topic:(\d+)$/.exec(normalized); if (topicMatch) { - const messageThreadId = parseStrictNonNegativeInteger(topicMatch[2]); + const chatId = topicMatch[1]; + const threadIdText = topicMatch[2]; + if (chatId === undefined || threadIdText === undefined) { + return { chatId: normalized, chatType: resolveTelegramChatType(normalized) }; + } + const messageThreadId = parseStrictNonNegativeInteger(threadIdText); if (messageThreadId === undefined) { return { chatId: normalized, @@ -113,15 +118,20 @@ export function parseTelegramTarget(to: string): TelegramTarget { }; } return { - chatId: topicMatch[1], + chatId, messageThreadId, - chatType: resolveTelegramChatType(topicMatch[1]), + chatType: resolveTelegramChatType(chatId), }; } const colonMatch = /^(.+):(\d+)$/.exec(normalized); if (colonMatch) { - const messageThreadId = parseStrictNonNegativeInteger(colonMatch[2]); + const chatId = colonMatch[1]; + const threadIdText = colonMatch[2]; + if (chatId === undefined || threadIdText === undefined) { + return { chatId: normalized, chatType: resolveTelegramChatType(normalized) }; + } + const messageThreadId = parseStrictNonNegativeInteger(threadIdText); if (messageThreadId === undefined) { return { chatId: normalized, @@ -129,9 +139,9 @@ export function parseTelegramTarget(to: string): TelegramTarget { }; } return { - chatId: colonMatch[1], + chatId, messageThreadId, - chatType: resolveTelegramChatType(colonMatch[1]), + chatType: resolveTelegramChatType(chatId), }; } diff --git a/extensions/telegram/src/webhook.ts b/extensions/telegram/src/webhook.ts index 7a4547882329..c3cc3f8e6631 100644 --- a/extensions/telegram/src/webhook.ts +++ b/extensions/telegram/src/webhook.ts @@ -248,6 +248,9 @@ function isTrustedProxyAddress( } if (trimmed.includes("/")) { const [address, prefix] = trimmed.split("/", 2); + if (address === undefined || prefix === undefined) { + continue; + } const parsedPrefix = parseStrictNonNegativeInteger(prefix); const family = net.isIP(address); if (family === 4 && parsedPrefix !== undefined && parsedPrefix >= 0 && parsedPrefix <= 32) { diff --git a/extensions/tlon/src/channel.runtime.ts b/extensions/tlon/src/channel.runtime.ts index 1216fb4006ed..3d0ab2198b8d 100644 --- a/extensions/tlon/src/channel.runtime.ts +++ b/extensions/tlon/src/channel.runtime.ts @@ -4,6 +4,7 @@ import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contrac import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-send-result"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { ChannelPlugin } from "openclaw/plugin-sdk/core"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http"; import { monitorTlonProvider } from "./monitor/index.js"; import { tlonSetupWizard } from "./setup-surface.js"; @@ -67,7 +68,7 @@ async function createHttpPokeApi(params: { method: "PUT", headers: { "Content-Type": "application/json", - Cookie: cookie.split(";")[0], + Cookie: expectDefined(cookie.split(";").at(0), "cookie first segment"), }, body: JSON.stringify([pokeData]), }, diff --git a/extensions/tlon/src/monitor/approval-runtime.ts b/extensions/tlon/src/monitor/approval-runtime.ts index 73c8c26f9af7..86c7137003b7 100644 --- a/extensions/tlon/src/monitor/approval-runtime.ts +++ b/extensions/tlon/src/monitor/approval-runtime.ts @@ -1,4 +1,5 @@ // Tlon plugin module implements approval runtime behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime"; import type { PendingApproval, TlonSettingsStore } from "../settings.js"; import { normalizeShip } from "../targets.js"; @@ -216,7 +217,7 @@ export function createTlonApprovalRuntime(params: { ); if (existingIndex !== -1) { - const existing = approvals[existingIndex]; + const existing = expectDefined(approvals[existingIndex], "located pending approval index"); if (approval.originalMessage) { existing.originalMessage = approval.originalMessage; existing.messagePreview = approval.messagePreview; diff --git a/extensions/tlon/src/monitor/approval.ts b/extensions/tlon/src/monitor/approval.ts index bb13b958e410..c818384c9674 100644 --- a/extensions/tlon/src/monitor/approval.ts +++ b/extensions/tlon/src/monitor/approval.ts @@ -7,6 +7,7 @@ // Extensions cannot import core internals directly, so use node:crypto here. import { randomBytes } from "node:crypto"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { PendingApproval } from "../settings.js"; @@ -224,7 +225,7 @@ export function parseAdminCommand(text: string): AdminCommand | null { // "unblock ~ship" - unblock a specific ship const unblockMatch = trimmed.match(/^unblock\s+(~[\w-]+)$/); if (unblockMatch) { - return { type: "unblock", ship: unblockMatch[1] }; + return { type: "unblock", ship: expectDefined(unblockMatch[1], "unblock ship capture") }; } return null; diff --git a/extensions/tlon/src/monitor/utils.ts b/extensions/tlon/src/monitor/utils.ts index dc4826fa6ec2..fb3b3c0e198a 100644 --- a/extensions/tlon/src/monitor/utils.ts +++ b/extensions/tlon/src/monitor/utils.ts @@ -1,9 +1,10 @@ -// Tlon helper module supports utils behavior. import { resolveStableChannelMessageIngress, type StableChannelIngressIdentityParams, } from "openclaw/plugin-sdk/channel-ingress-runtime"; import { formatErrorMessage as sharedFormatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +// Tlon helper module supports utils behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { normalizeShip } from "../targets.js"; export interface ParsedCite { @@ -59,7 +60,9 @@ export function formatModelName(modelString?: string | null): string { if (!modelString) { return "AI"; } - const modelName = modelString.includes("/") ? modelString.split("/")[1] : modelString; + const modelName = modelString.includes("/") + ? expectDefined(modelString.split("/").at(1), "provider/model second segment") + : modelString; const modelMappings: Record = { "claude-opus-4-5": "Claude Opus 4.5", "claude-sonnet-4-5": "Claude Sonnet 4.5", @@ -71,8 +74,9 @@ export function formatModelName(modelString?: string | null): string { "gemini-pro": "Gemini Pro", }; - if (modelMappings[modelName]) { - return modelMappings[modelName]; + const mappedName = modelMappings[modelName]; + if (mappedName !== undefined) { + return mappedName; } return modelName .replace(/-/g, " ") diff --git a/extensions/tlon/src/targets.ts b/extensions/tlon/src/targets.ts index 6c61e6a99ce0..4912685ba233 100644 --- a/extensions/tlon/src/targets.ts +++ b/extensions/tlon/src/targets.ts @@ -1,4 +1,5 @@ // Tlon plugin module implements targets behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; type TlonTarget = | { kind: "dm"; ship: string } | { kind: "group"; nest: string; hostShip: string; channelName: string }; @@ -19,8 +20,8 @@ export function parseChannelNest(raw: string): { hostShip: string; channelName: if (!match) { return null; } - const hostShip = normalizeShip(match[1]); - const channelName = match[2]; + const hostShip = normalizeShip(expectDefined(match[1], "channel host capture")); + const channelName = expectDefined(match[2], "channel name capture"); return { hostShip, channelName }; } @@ -42,12 +43,12 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null { const dmPrefix = withoutPrefix.match(/^dm[/:](.+)$/i); if (dmPrefix) { - return { kind: "dm", ship: normalizeShip(dmPrefix[1]) }; + return { kind: "dm", ship: normalizeShip(expectDefined(dmPrefix[1], "DM ship capture")) }; } const groupPrefix = withoutPrefix.match(/^(group|room)[/:](.+)$/i); if (groupPrefix) { - const groupTarget = groupPrefix[2].trim(); + const groupTarget = expectDefined(groupPrefix[2], "group target capture").trim(); if (groupTarget.startsWith("chat/")) { const parsed = parseChannelNest(groupTarget); if (!parsed) { @@ -57,8 +58,8 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null { } const parts = groupTarget.split("/"); if (parts.length === 2) { - const hostShip = normalizeShip(parts[0]); - const channelName = parts[1]; + const hostShip = normalizeShip(expectDefined(parts[0], "two-part group host")); + const channelName = expectDefined(parts[1], "two-part group channel"); return { kind: "group", nest: `chat/${hostShip}/${channelName}`, diff --git a/extensions/tlon/src/tlon-api.ts b/extensions/tlon/src/tlon-api.ts index 17666b909e81..a054039fa9d1 100644 --- a/extensions/tlon/src/tlon-api.ts +++ b/extensions/tlon/src/tlon-api.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { extensionForMime } from "openclaw/plugin-sdk/media-mime"; import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; @@ -392,7 +393,7 @@ export async function uploadFile(params: UploadFileParams): Promise"\]]+)/); if (urlMatch) { - result.push({ link: { href: urlMatch[1], content: urlMatch[1] } }); + const url = expectDefined(urlMatch[1], "plain URL capture"); + result.push({ link: { href: url, content: url } }); remaining = remaining.slice(urlMatch[0].length); continue; } @@ -131,13 +144,13 @@ function parseInlineMarkdown(text: string): StoryInline[] { // Exclude : and / to allow URL detection to work (stops before https://) const plainMatch = remaining.match(/^[^*_`~[#\n:/]+/); if (plainMatch) { - result.push(plainMatch[0]); + result.push(expectDefined(plainMatch[0], "plain text match")); remaining = remaining.slice(plainMatch[0].length); continue; } // Single special char that didn't match a pattern - result.push(remaining[0]); + result.push(remaining.charAt(0)); remaining = remaining.slice(1); } @@ -145,14 +158,32 @@ function parseInlineMarkdown(text: string): StoryInline[] { return mergeAdjacentStrings(result); } +function headingTag(marker: string): "h1" | "h2" | "h3" | "h4" | "h5" | "h6" { + switch (marker.length) { + case 1: + return "h1"; + case 2: + return "h2"; + case 3: + return "h3"; + case 4: + return "h4"; + case 5: + return "h5"; + default: + return "h6"; + } +} + /** * Merge adjacent string elements in an inline array */ function mergeAdjacentStrings(inlines: StoryInline[]): StoryInline[] { const result: StoryInline[] = []; for (const item of inlines) { - if (typeof item === "string" && typeof result[result.length - 1] === "string") { - result[result.length - 1] = (result[result.length - 1] as string) + item; + const last = result.at(-1); + if (typeof item === "string" && typeof last === "string") { + result.splice(-1, 1, last + item); } else { result.push(item); } @@ -210,15 +241,19 @@ export function markdownToStory(markdown: string): Story { let i = 0; while (i < lines.length) { - const line = lines[i]; + const line = expectDefined(lines[i], "Markdown line index is in bounds"); // Code block: ```lang\ncode\n``` if (line.startsWith("```")) { const lang = line.slice(3).trim() || "plaintext"; const codeLines: string[] = []; i++; - while (i < lines.length && !lines[i].startsWith("```")) { - codeLines.push(lines[i]); + while (true) { + const codeLine = lines.at(i); + if (codeLine === undefined || codeLine.startsWith("```")) { + break; + } + codeLines.push(codeLine); i++; } story.push({ @@ -236,13 +271,12 @@ export function markdownToStory(markdown: string): Story { // Headers: # H1, ## H2, etc. const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headerMatch) { - const level = headerMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6; - const tag = `h${level}` as const; + const tag = headingTag(expectDefined(headerMatch[1], "header marker capture")); story.push({ block: { header: { tag, - content: parseInlineMarkdown(headerMatch[2]), + content: parseInlineMarkdown(expectDefined(headerMatch[2], "header body capture")), }, }, }); @@ -260,8 +294,12 @@ export function markdownToStory(markdown: string): Story { // Blockquote: > text if (line.startsWith("> ")) { const quoteLines: string[] = []; - while (i < lines.length && lines[i].startsWith("> ")) { - quoteLines.push(lines[i].slice(2)); + while (true) { + const quoteLine = lines.at(i); + if (quoteLine === undefined || !quoteLine.startsWith("> ")) { + break; + } + quoteLines.push(quoteLine.slice(2)); i++; } const quoteText = quoteLines.join("\n"); @@ -279,15 +317,19 @@ export function markdownToStory(markdown: string): Story { // Regular paragraph - collect consecutive non-empty lines const paragraphLines: string[] = []; - while ( - i < lines.length && - lines[i].trim() !== "" && - !lines[i].startsWith("#") && - !lines[i].startsWith("```") && - !lines[i].startsWith("> ") && - !/^(-{3,}|\*{3,})$/.test(lines[i].trim()) - ) { - paragraphLines.push(lines[i]); + while (true) { + const paragraphLine = lines.at(i); + if ( + paragraphLine === undefined || + paragraphLine.trim() === "" || + paragraphLine.startsWith("#") || + paragraphLine.startsWith("```") || + paragraphLine.startsWith("> ") || + /^(-{3,}|\*{3,})$/.test(paragraphLine.trim()) + ) { + break; + } + paragraphLines.push(paragraphLine); i++; } @@ -300,9 +342,9 @@ export function markdownToStory(markdown: string): Story { for (const inline of inlines) { if (typeof inline === "string" && inline.includes("\n")) { const parts = inline.split("\n"); - for (let j = 0; j < parts.length; j++) { - if (parts[j]) { - withBreaks.push(parts[j]); + for (const [j, part] of parts.entries()) { + if (part) { + withBreaks.push(part); } if (j < parts.length - 1) { withBreaks.push({ break: null }); diff --git a/extensions/vault/src/cli.ts b/extensions/vault/src/cli.ts index 7e4acc4a2fab..7f166f4af9ba 100644 --- a/extensions/vault/src/cli.ts +++ b/extensions/vault/src/cli.ts @@ -211,7 +211,7 @@ async function pathExists(filePath: string): Promise { } } -function resolverScriptPathCandidates(baseUrl: string): string[] { +function resolverScriptPathCandidates(baseUrl: string): [string, string] { return [ fileURLToPath(new URL("../vault-secret-ref-resolver.js", baseUrl)), fileURLToPath(new URL("./extensions/vault/vault-secret-ref-resolver.js", baseUrl)), diff --git a/extensions/voice-call/src/manager/store.ts b/extensions/voice-call/src/manager/store.ts index 22698470382b..e29de089fcf5 100644 --- a/extensions/voice-call/src/manager/store.ts +++ b/extensions/voice-call/src/manager/store.ts @@ -117,7 +117,8 @@ function buildNewEventKey(order: { persistedAt: number; sequence: number }): str /** Recover the sequence segment from newer event keys. */ function parseEventKeySequence(key: string): number { const match = /^event:[^:]+:(\d+):/.exec(key); - return match ? Number.parseInt(match[1], 10) : 0; + const sequence = match?.[1]; + return sequence ? Number.parseInt(sequence, 10) : 0; } /** Parse a stored call record line from v2 envelope or legacy raw-call JSON. */ diff --git a/extensions/voice-call/src/providers/twilio.ts b/extensions/voice-call/src/providers/twilio.ts index 6de184013b5b..7849a6f9d41c 100644 --- a/extensions/voice-call/src/providers/twilio.ts +++ b/extensions/voice-call/src/providers/twilio.ts @@ -127,12 +127,12 @@ export class TwilioProvider implements VoiceCallProvider { return; } - const callIdMatch = webhookUrl.match(/callId=([^&]+)/); - if (!callIdMatch) { + const callId = webhookUrl.match(/callId=([^&]+)/)?.[1]; + if (!callId) { return; } - this.deleteStoredTwiml(callIdMatch[1]); + this.deleteStoredTwiml(callId); this.streamAuthTokens.delete(providerCallId); } diff --git a/extensions/voice-call/src/response-generator.ts b/extensions/voice-call/src/response-generator.ts index 276bd45c1b58..e0fe23d0adb8 100644 --- a/extensions/voice-call/src/response-generator.ts +++ b/extensions/voice-call/src/response-generator.ts @@ -172,7 +172,11 @@ function sanitizePlainSpokenText(text: string): string | null { const paragraphs = normalizeStringEntries(withoutCodeFences.split(/\n\s*\n+/)); - while (paragraphs.length > 1 && isLikelyMetaReasoningParagraph(paragraphs[0])) { + while (paragraphs.length > 1) { + const firstParagraph = paragraphs.at(0); + if (!firstParagraph || !isLikelyMetaReasoningParagraph(firstParagraph)) { + break; + } paragraphs.shift(); } diff --git a/extensions/voice-call/src/webhook-security.ts b/extensions/voice-call/src/webhook-security.ts index 1203c4eee16f..60e765e7537a 100644 --- a/extensions/voice-call/src/webhook-security.ts +++ b/extensions/voice-call/src/webhook-security.ts @@ -127,15 +127,13 @@ function extractHostname(hostHeader: string): string | null { return null; } - let hostname: string; - // Handle IPv6 addresses: [::1]:8080 if (hostHeader.startsWith("[")) { const endBracket = hostHeader.indexOf("]"); if (endBracket === -1) { return null; // Malformed IPv6 } - hostname = hostHeader.slice(1, endBracket); + const hostname = hostHeader.slice(1, endBracket); return normalizeLowercaseStringOrEmpty(hostname); } @@ -145,10 +143,10 @@ function extractHostname(hostHeader: string): string | null { return null; // Reject potential injection: attacker.com:80@legitimate.com } - hostname = hostHeader.split(":")[0]; + const hostname = hostHeader.split(":").at(0); // Validate the extracted hostname - if (!isValidHostname(hostname)) { + if (!hostname || !isValidHostname(hostname)) { return null; } @@ -721,8 +719,11 @@ function toParamMapFromSearchParams(sp: URLSearchParams): PlivoParamMap { function sortedQueryString(params: PlivoParamMap): string { const parts: string[] = []; - for (const key of Object.keys(params).toSorted()) { - const values = [...params[key]].toSorted(); + const entries = Object.entries(params).toSorted(([left], [right]) => + left < right ? -1 : left > right ? 1 : 0, + ); + for (const [key, entryValues] of entries) { + const values = [...entryValues].toSorted(); for (const value of values) { parts.push(`${key}=${value}`); } @@ -732,8 +733,11 @@ function sortedQueryString(params: PlivoParamMap): string { function sortedParamsString(params: PlivoParamMap): string { const parts: string[] = []; - for (const key of Object.keys(params).toSorted()) { - const values = [...params[key]].toSorted(); + const entries = Object.entries(params).toSorted(([left], [right]) => + left < right ? -1 : left > right ? 1 : 0, + ); + for (const [key, entryValues] of entries) { + const values = [...entryValues].toSorted(); for (const value of values) { parts.push(`${key}${value}`); } diff --git a/extensions/whatsapp/src/approval-reactions.ts b/extensions/whatsapp/src/approval-reactions.ts index 4de3d9bf0381..33bdfa3a2c79 100644 --- a/extensions/whatsapp/src/approval-reactions.ts +++ b/extensions/whatsapp/src/approval-reactions.ts @@ -250,13 +250,21 @@ function visibleApprovalBindingMatches( if (hintIndices.length !== 1) { return false; } - let cursor = hintIndices[0] + 1; - while (cursor < lines.length && !lines[cursor].trim()) { + const hintIndex = hintIndices[0]; + if (hintIndex === undefined) { + return false; + } + let cursor = hintIndex + 1; + while (cursor < lines.length && !lines[cursor]?.trim()) { cursor += 1; } const decisionLines: string[] = []; - while (cursor < lines.length && lines[cursor].trim()) { - decisionLines.push(lines[cursor].trim()); + while (cursor < lines.length) { + const decisionLine = lines[cursor]?.trim(); + if (!decisionLine) { + break; + } + decisionLines.push(decisionLine); cursor += 1; } const knownBindings = listWhatsAppApprovalReactionBindings([ diff --git a/extensions/whatsapp/src/channel-react-action.ts b/extensions/whatsapp/src/channel-react-action.ts index 62c643d53b36..c2869d09ddb2 100644 --- a/extensions/whatsapp/src/channel-react-action.ts +++ b/extensions/whatsapp/src/channel-react-action.ts @@ -76,7 +76,8 @@ function readWhatsAppActionChatJid(params: WhatsAppMessageActionParams): string function extractBase64Payload(encoded: string): string { const match = /^data:[^;]+;base64,(.*)$/i.exec(encoded.trim()); - return match ? match[1] : encoded; + const payload = match?.[1]; + return payload !== undefined ? payload : encoded; } function estimateBase64DecodedBytes(encoded: string): number { diff --git a/extensions/whatsapp/src/inbound/outbound-mentions.ts b/extensions/whatsapp/src/inbound/outbound-mentions.ts index 73470c7e1358..680de06087e4 100644 --- a/extensions/whatsapp/src/inbound/outbound-mentions.ts +++ b/extensions/whatsapp/src/inbound/outbound-mentions.ts @@ -63,9 +63,13 @@ function normalizeKnownUserJid(value: string): string | null { const trimmed = value.replace(/^whatsapp:/i, "").trim(); const jidMatch = trimmed.match(KNOWN_USER_JID_RE); if (jidMatch) { - const domain = - jidMatch[2].toLowerCase() === "c.us" ? "s.whatsapp.net" : jidMatch[2].toLowerCase(); - return `${jidMatch[1]}@${domain}`; + const user = jidMatch[1]; + const rawDomain = jidMatch[2]; + if (!user || !rawDomain) { + return null; + } + const domain = rawDomain.toLowerCase() === "c.us" ? "s.whatsapp.net" : rawDomain.toLowerCase(); + return `${user}@${domain}`; } const digits = trimmed.startsWith("+") ? trimmed.replace(/\D/g, "") @@ -81,7 +85,9 @@ function extractKnownJidParts(value: string): { user: string; domain: string } | return null; } const match = normalized.match(/^(\d+)@(.+)$/); - return match ? { user: match[1], domain: match[2] } : null; + const user = match?.[1]; + const domain = match?.[2]; + return user && domain ? { user, domain } : null; } function extractPhoneDigits(value: string | null | undefined): string | null { @@ -216,7 +222,11 @@ export function resolveWhatsAppOutboundMentions(params: { if (shouldSkipMentionAt(params.text, start, start + token.length, codeRanges)) { continue; } - const digits = match[1].replace(/\D/g, ""); + const rawDigits = match[1]; + if (!rawDigits) { + continue; + } + const digits = rawDigits.replace(/\D/g, ""); const target = token.startsWith("@+") ? (byPhone.get(digits) ?? byLid.get(digits)) : (byLid.get(digits) ?? byPhone.get(digits)); diff --git a/extensions/whatsapp/src/normalize-target.ts b/extensions/whatsapp/src/normalize-target.ts index cfb0fe4d1738..1aa031a4cfd7 100644 --- a/extensions/whatsapp/src/normalize-target.ts +++ b/extensions/whatsapp/src/normalize-target.ts @@ -55,15 +55,18 @@ export function isWhatsAppUserTarget(value: string): boolean { function extractUserJidPhone(jid: string): string | null { const userMatch = jid.match(WHATSAPP_USER_JID_RE); if (userMatch) { - return userMatch[1]; + const phone = userMatch[1]; + return phone ? phone : null; } const legacyUserMatch = jid.match(WHATSAPP_LEGACY_USER_JID_RE); if (legacyUserMatch) { - return legacyUserMatch[1]; + const phone = legacyUserMatch[1]; + return phone ? phone : null; } const lidMatch = jid.match(WHATSAPP_LID_RE); if (lidMatch) { - return lidMatch[1]; + const phone = lidMatch[1]; + return phone ? phone : null; } return null; } diff --git a/extensions/whatsapp/src/targets-runtime.ts b/extensions/whatsapp/src/targets-runtime.ts index 1be94dffb50e..7ec3b9836c09 100644 --- a/extensions/whatsapp/src/targets-runtime.ts +++ b/extensions/whatsapp/src/targets-runtime.ts @@ -135,8 +135,13 @@ export async function resolveEquivalentWhatsAppDirectChatJids( const mappedLid = await tryLookupMappedJid(() => opts?.lidLookup?.getLIDForPN?.(normalized)); addEquivalentDirectChatCandidate(candidates, mappedLid); - const mappedLocalLid = readLidForwardMapping({ phoneDigits: pnMatch[1], opts }); - const localLidDomain = pnMatch[2].toLowerCase() === "hosted" ? "hosted.lid" : "lid"; + const phoneDigits = pnMatch[1]; + const pnDomain = pnMatch[2]; + if (!phoneDigits || !pnDomain) { + return candidates; + } + const mappedLocalLid = readLidForwardMapping({ phoneDigits, opts }); + const localLidDomain = pnDomain.toLowerCase() === "hosted" ? "hosted.lid" : "lid"; addUniqueString(candidates, mappedLocalLid ? `${mappedLocalLid}@${localLidDomain}` : null); return candidates; } @@ -146,9 +151,13 @@ export async function resolveEquivalentWhatsAppDirectChatJids( const mappedPn = await tryLookupMappedJid(() => opts?.lidLookup?.getPNForLID?.(normalized)); addEquivalentDirectChatCandidate(candidates, mappedPn); + const lidDomain = lidMatch[2]; + if (!lidMatch[1] || !lidDomain) { + return candidates; + } const e164 = jidToE164(normalized, { ...opts, logMissing: false }); const localPnJid = - e164 && lidMatch[2].toLowerCase() === "hosted.lid" + e164 && lidDomain.toLowerCase() === "hosted.lid" ? `${e164.replace(/\D/g, "")}@hosted` : e164 ? toWhatsappJid(e164) @@ -221,16 +230,21 @@ function readLidForwardMapping(params: { export function jidToE164(jid: string, opts?: JidToE164Options): string | null { const match = jid.match(/^(\d+)(?::\d+)?@(s\.whatsapp\.net|hosted)$/); - if (match) { - return `+${match[1]}`; + const phoneDigits = match?.[1]; + if (phoneDigits) { + return `+${phoneDigits}`; } const lidMatch = jid.match(/^(\d+)(?::\d+)?@(lid|hosted\.lid)$/); if (!lidMatch) { return null; } + const lid = lidMatch[1]; + if (!lid) { + return null; + } const phone = readLidReverseMapping({ - lid: lidMatch[1], + lid, opts, }); if (phone) { diff --git a/extensions/workspaces/src/serve.ts b/extensions/workspaces/src/serve.ts index 5759c72022fd..bb61585af909 100644 --- a/extensions/workspaces/src/serve.ts +++ b/extensions/workspaces/src/serve.ts @@ -121,6 +121,9 @@ export function parseWidgetRequestPath( return null; } const [frameToken, name, ...entry] = segments; + if (!frameToken || !name) { + return null; + } if (!BRIDGE_TOKEN_PATTERN.test(frameToken)) { return null; } diff --git a/extensions/xai/video-generation-provider.ts b/extensions/xai/video-generation-provider.ts index 444946f7f89b..c591f7cec2a7 100644 --- a/extensions/xai/video-generation-provider.ts +++ b/extensions/xai/video-generation-provider.ts @@ -195,10 +195,11 @@ function validateXaiVideo15Request(req: VideoGenerationRequest): void { throw new Error("xAI grok-imagine-video-1.5 does not support video inputs."); } const inputImages = req.inputImages ?? []; - if (inputImages.length !== 1) { + const [inputImage, ...additionalImages] = inputImages; + if (!inputImage || additionalImages.length > 0) { throw new Error("xAI grok-imagine-video-1.5 requires exactly one first-frame image."); } - if (!isFirstFrameImage(inputImages[0])) { + if (!isFirstFrameImage(inputImage)) { throw new Error("xAI grok-imagine-video-1.5 supports only an ordinary or first_frame image."); } } diff --git a/extensions/xai/x-search.ts b/extensions/xai/x-search.ts index 559b13166fca..8411195d0471 100644 --- a/extensions/xai/x-search.ts +++ b/extensions/xai/x-search.ts @@ -95,7 +95,13 @@ function normalizeOptionalIsoDate(value: string | undefined, label: string): str if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { throw new PluginToolInputError(`${label} must use YYYY-MM-DD`); } - const [year, month, day] = trimmed.split("-").map((entry) => Number.parseInt(entry, 10)); + const [yearText, monthText, dayText] = trimmed.split("-"); + if (yearText === undefined || monthText === undefined || dayText === undefined) { + throw new PluginToolInputError(`${label} must use YYYY-MM-DD`); + } + const year = Number.parseInt(yearText, 10); + const month = Number.parseInt(monthText, 10); + const day = Number.parseInt(dayText, 10); const date = new Date(Date.UTC(year, month - 1, day)); if ( date.getUTCFullYear() !== year || diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 80315d7f7b0a..4c1438849c8b 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -1,4 +1,3 @@ -// Zalouser plugin module implements monitor behavior. import { mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk/allow-from"; import { implicitMentionKindWhen, @@ -9,6 +8,8 @@ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pair import type { MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { KeyedAsyncQueue } from "openclaw/plugin-sdk/core"; import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime"; +// Zalouser plugin module implements monitor behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { createDeferred } from "openclaw/plugin-sdk/extension-shared"; import { DEFAULT_GROUP_HISTORY_LIMIT, @@ -891,7 +892,10 @@ export async function monitorZalouserProvider( const cleaned = normalizeZalouserAllowEntry(entry); if (/^\d+$/.test(cleaned)) { if (!nextGroups[cleaned]) { - nextGroups[cleaned] = groupsConfig[entry]; + nextGroups[cleaned] = expectDefined( + groupsConfig[entry], + "enumerated Zalouser group config", + ); } mapping.push(`${entry}→${cleaned}`); continue; @@ -901,7 +905,7 @@ export async function monitorZalouserProvider( const id = match?.groupId; if (id) { if (!nextGroups[id]) { - nextGroups[id] = groupsConfig[entry]; + nextGroups[id] = expectDefined(groupsConfig[entry], "enumerated Zalouser group config"); } mapping.push(`${entry}→${id}`); } else { diff --git a/extensions/zalouser/src/send.ts b/extensions/zalouser/src/send.ts index 24595a1a03fc..df49ba3eaa3f 100644 --- a/extensions/zalouser/src/send.ts +++ b/extensions/zalouser/src/send.ts @@ -278,7 +278,7 @@ function findLastBreak( function findLastWhitespaceBreak(text: string, start: number, end: number): number | undefined { for (let index = end - 1; index > start; index -= 1) { - if (/\s/.test(text[index])) { + if (/\s/.test(text.charAt(index))) { return index + 1; } } diff --git a/extensions/zalouser/src/text-styles.ts b/extensions/zalouser/src/text-styles.ts index 44eced707c6c..e9f1bc5c15be 100644 --- a/extensions/zalouser/src/text-styles.ts +++ b/extensions/zalouser/src/text-styles.ts @@ -1,4 +1,5 @@ // Zalouser plugin module implements text styles behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { TextStyle, type Style } from "./zca-constants.js"; const ESCAPE_SENTINEL_START = "\u0001"; @@ -55,50 +56,51 @@ const TAG_STYLE_MAP: Record = { const INLINE_MARKERS: InlineMarker[] = [ { pattern: /`([^`\n]+)`/g, - extractText: (match) => match[0], + extractText: (match) => expectDefined(match[0], "inline code match"), literal: true, }, { pattern: /\\([*_~#\\{}>+\-`])/g, - extractText: (match) => match[1], + extractText: (match) => expectDefined(match[1], "escaped Markdown character capture"), literal: true, }, { pattern: new RegExp(`\\{(${Object.keys(TAG_STYLE_MAP).join("|")})\\}(.+?)\\{/\\1\\}`, "g"), - extractText: (match) => match[2], + extractText: (match) => expectDefined(match[2], "tag body capture"), resolveStyles: (match) => { - const style = TAG_STYLE_MAP[match[1]]; + const tag = expectDefined(match[1], "tag name capture"); + const style = TAG_STYLE_MAP[tag]; return style ? [style] : []; }, }, { pattern: /(? match[1], + extractText: (match) => expectDefined(match[1], "bold italic body capture"), resolveStyles: () => [TextStyle.Bold, TextStyle.Italic], }, { pattern: /(? match[1], + extractText: (match) => expectDefined(match[1], "bold body capture"), resolveStyles: () => [TextStyle.Bold], }, { pattern: /(? match[1], + extractText: (match) => expectDefined(match[1], "underscored bold body capture"), resolveStyles: () => [TextStyle.Bold], }, { pattern: /(? match[1], + extractText: (match) => expectDefined(match[1], "strikethrough body capture"), resolveStyles: () => [TextStyle.StrikeThrough], }, { pattern: /(? match[1], + extractText: (match) => expectDefined(match[1], "italic body capture"), resolveStyles: () => [TextStyle.Italic], }, { pattern: /(? match[1], + extractText: (match) => expectDefined(match[1], "underscored italic body capture"), resolveStyles: () => [TextStyle.Italic], }, ]; @@ -112,8 +114,7 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: const processedLines: string[] = []; let activeFence: ActiveFence | null = null; - for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { - const rawLine = lines[lineIndex]; + for (const [lineIndex, rawLine] of lines.entries()) { const { text: unquotedLine, indent: baseIndent } = stripQuotePrefix(rawLine); if (activeFence) { @@ -164,7 +165,7 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: const headingMatch = markdownLine.match(/^(#{1,4})\s(.*)$/); if (headingMatch) { - const depth = headingMatch[1].length; + const depth = expectDefined(headingMatch[1], "heading marker capture").length; lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.Bold }); if (depth === 1) { lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.Big }); @@ -176,7 +177,7 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: indentSize: baseIndent, }); } - processedLines.push(headingMatch[2]); + processedLines.push(expectDefined(headingMatch[2], "heading body capture")); continue; } @@ -184,8 +185,8 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: let indentLevel = 0; let content = markdownLine; if (indentMatch) { - indentLevel = clampIndent(indentMatch[1].length); - content = indentMatch[2]; + indentLevel = clampIndent(expectDefined(indentMatch[1], "indent capture").length); + content = expectDefined(indentMatch[2], "indented content capture"); } const totalIndent = Math.min(5, baseIndent + indentLevel); @@ -211,7 +212,7 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: }); } lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.OrderedList }); - processedLines.push(orderedListMatch[2]); + processedLines.push(expectDefined(orderedListMatch[2], "ordered list body capture")); continue; } @@ -225,7 +226,7 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: }); } lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.UnorderedList }); - processedLines.push(unorderedListMatch[1]); + processedLines.push(expectDefined(unorderedListMatch[1], "unordered list body capture")); continue; } @@ -271,8 +272,12 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: let cumulativeDelta = 0; for (const match of plainText.matchAll(escapeRegex)) { - const escapeIndex = Number.parseInt(match[1], 10); - cumulativeDelta += match[0].length - escapeMap[escapeIndex].length; + const escapeIndex = Number.parseInt(expectDefined(match[1], "escape sentinel index"), 10); + const escaped = escapeMap.at(escapeIndex); + if (escaped === undefined) { + continue; + } + cumulativeDelta += match[0].length - escaped.length; shifts.push({ pos: (match.index ?? 0) + match[0].length, delta: cumulativeDelta }); } @@ -294,14 +299,14 @@ export function parseZalouserTextStyles(input: string): { text: string; styles: plainText = plainText.replace( escapeRegex, - (_match, index) => escapeMap[Number.parseInt(index, 10)], + (match, index) => escapeMap.at(Number.parseInt(index, 10)) ?? match, ); } const finalLines = plainText.split("\n"); let offset = 0; - for (let lineIndex = 0; lineIndex < finalLines.length; lineIndex += 1) { - const lineLength = finalLines[lineIndex].length; + for (const [lineIndex, line] of finalLines.entries()) { + const lineLength = line.length; if (lineLength > 0) { for (const lineStyle of lineStyles) { if (lineStyle.lineIndex !== lineIndex) { @@ -336,15 +341,15 @@ function stripOptionalMarkdownPadding(line: string): { text: string; size: numbe return { text: line, size: 0 }; } return { - text: line.slice(match[1].length), - size: match[1].length, + text: line.slice(expectDefined(match[1], "Markdown padding capture").length), + size: expectDefined(match[1], "Markdown padding capture").length, }; } function hasClosingFence(lines: string[], startIndex: number, fence: ActiveFence): boolean { for (let index = startIndex; index < lines.length; index += 1) { - const candidate = - fence.quoteIndent > 0 ? stripQuotePrefix(lines[index], fence.quoteIndent).text : lines[index]; + const line = expectDefined(lines[index], "closing fence scan index is in bounds"); + const candidate = fence.quoteIndent > 0 ? stripQuotePrefix(line, fence.quoteIndent).text : line; if (isClosingFence(candidate, fence)) { return true; } @@ -409,8 +414,8 @@ function parseFenceMarker(line: string): FenceMarker | null { return null; } - const marker = match[2]; - const char = marker[0]; + const marker = expectDefined(match[2], "fence marker capture"); + const char = marker.charAt(0); if (char !== "`" && char !== "~") { return null; } @@ -418,7 +423,7 @@ function parseFenceMarker(line: string): FenceMarker | null { return { char, length: marker.length, - indent: match[1].length, + indent: expectDefined(match[1], "fence indent capture").length, }; } @@ -427,7 +432,8 @@ function isClosingFence(line: string, fence: FenceMarker): boolean { if (!match) { return false; } - return match[2][0] === fence.char && match[2].length >= fence.length; + const marker = expectDefined(match[2], "closing fence marker capture"); + return marker.charAt(0) === fence.char && marker.length >= fence.length; } function escapeLiteralText(input: string, escapeMap: string[]): string { @@ -515,7 +521,7 @@ function pushSegment(segments: Segment[], text: string, styles: InlineStyle[]): } function sameStyles(left: InlineStyle[], right: InlineStyle[]): boolean { - return left.length === right.length && left.every((style, index) => style === right[index]); + return left.length === right.length && left.every((style, index) => style === right.at(index)); } function normalizeCodeBlockLeadingWhitespace(line: string): string { diff --git a/extensions/zalouser/src/zalo-js.ts b/extensions/zalouser/src/zalo-js.ts index b235863897ae..7430d6fad9bc 100644 --- a/extensions/zalouser/src/zalo-js.ts +++ b/extensions/zalouser/src/zalo-js.ts @@ -1,8 +1,9 @@ -// Zalouser plugin module implements zalo js behavior. import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +// Zalouser plugin module implements zalo js behavior. +import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { extensionForMime } from "openclaw/plugin-sdk/media-mime"; import { asDateTimestampMs, @@ -421,7 +422,7 @@ function stripLeadingAtMentionForCommand(content: string): string { if (!fallbackMatch) { return content; } - return fallbackMatch[1].trim(); + return expectDefined(fallbackMatch[1], "leading mention command capture").trim(); } function resolveGroupNameFromMessageData(data: Record): string | undefined { diff --git a/package.json b/package.json index 9de2e9fcbfab..6ce83e8df6a5 100644 --- a/package.json +++ b/package.json @@ -458,6 +458,10 @@ "types": "./dist/plugin-sdk/heartbeat-runtime.d.ts", "default": "./dist/plugin-sdk/heartbeat-runtime.js" }, + "./plugin-sdk/expect-runtime": { + "types": "./dist/plugin-sdk/expect-runtime.d.ts", + "default": "./dist/plugin-sdk/expect-runtime.js" + }, "./plugin-sdk/number-runtime": { "types": "./dist/plugin-sdk/number-runtime.d.ts", "default": "./dist/plugin-sdk/number-runtime.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index e35a40305749..ee0204c7433d 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -67,6 +67,7 @@ "delivery-queue-runtime", "file-access-runtime", "heartbeat-runtime", + "expect-runtime", "number-runtime", "secure-random-runtime", "system-event-runtime", diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index 88fbde0cb6a2..4b7285b9796b 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -194,17 +194,17 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { const budgets = { publicEntrypoints: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS", - 326, + 327, env, ), publicExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", - 10623, + 10624, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", - 5348, + 5349, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/plugin-sdk/expect-runtime.ts b/src/plugin-sdk/expect-runtime.ts new file mode 100644 index 000000000000..000b61257fd7 --- /dev/null +++ b/src/plugin-sdk/expect-runtime.ts @@ -0,0 +1,3 @@ +// Required-value assertion helper for provable plugin runtime invariants. + +export { expectDefined } from "../../packages/normalization-core/src/expect.js"; diff --git a/src/plugins/contracts/plugin-sdk-subpaths.test.ts b/src/plugins/contracts/plugin-sdk-subpaths.test.ts index f2172127e49f..f49e24aacd3c 100644 --- a/src/plugins/contracts/plugin-sdk-subpaths.test.ts +++ b/src/plugins/contracts/plugin-sdk-subpaths.test.ts @@ -558,6 +558,10 @@ describe("plugin-sdk subpath exports", () => { }); it("keeps helper subpaths aligned", () => { + expectSourceContract("expect-runtime", { + mentions: ["expectDefined"], + omits: ["first", "last"], + }); expectSourceMentions("core", [ "emptyPluginConfigSchema", "definePluginEntry", diff --git a/tsconfig.extensions.json b/tsconfig.extensions.json index 238a68529551..6b756eaf84bc 100644 --- a/tsconfig.extensions.json +++ b/tsconfig.extensions.json @@ -1,6 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { + // Plugin production indexed reads must account for missing entries. + "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "tsBuildInfoFile": ".artifacts/tsgo-cache/extensions.tsbuildinfo" @@ -15,6 +17,7 @@ "**/*test-helpers.ts", "**/*test-harness.ts", "**/*test-support.ts", + "extensions/**/test/**", "extensions/**/src/test-support/**", "test/**" ]