diff --git a/extensions/qa-channel/src/channel-base.ts b/extensions/qa-channel/src/channel-base.ts index 96a9caa416ec..70cf89b7dbe3 100644 --- a/extensions/qa-channel/src/channel-base.ts +++ b/extensions/qa-channel/src/channel-base.ts @@ -1,5 +1,4 @@ // Qa Channel plugin module implements channel base behavior. -import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common"; import { listQaChannelAccountIds, resolveDefaultQaChannelAccountId, @@ -13,15 +12,16 @@ import type { CoreConfig } from "./types.js"; export const QA_CHANNEL_ID = "qa-channel" as const; -export const qaChannelSetupMeta = { ...getChatChannelMeta(QA_CHANNEL_ID) }; +// qa-channel is synthetic and never in the bundled chat-meta catalog; it owns +// its metadata instead of spreading a lookup that could only ever be undefined. export const qaChannelRuntimeMeta = { - ...qaChannelSetupMeta, id: QA_CHANNEL_ID, label: "QA Channel", selectionLabel: "QA Channel", docsPath: "/channels/qa-channel", blurb: "Synthetic QA channel for OpenClaw QA runs.", }; +export const qaChannelSetupMeta = qaChannelRuntimeMeta; type QaChannelPluginBase = Pick< ChannelPlugin, diff --git a/src/acp/control-plane/manager.turn-runner.ts b/src/acp/control-plane/manager.turn-runner.ts index 80e338f9d78d..19b39c085522 100644 --- a/src/acp/control-plane/manager.turn-runner.ts +++ b/src/acp/control-plane/manager.turn-runner.ts @@ -1,5 +1,6 @@ /** Runs ACP turns, failover, timeout cleanup, and detached-task progress mirroring. */ import type { AcpRuntime, AcpRuntimeHandle } from "@openclaw/acp-core/runtime/types"; +import { expectDefined } from "@openclaw/normalization-core"; import { logVerbose } from "../../globals.js"; import { recordSessionHumanDirectMessage, @@ -165,8 +166,7 @@ export async function runManagerTurn(params: { } try { - for (let backendIdx = 0; backendIdx < candidateBackends.length; backendIdx += 1) { - const currentBackend = candidateBackends[backendIdx]; + for (const [backendIdx, currentBackend] of candidateBackends.entries()) { if (backendIdx > 0) { await params.runtimeHandles.close({ sessionKey, @@ -174,7 +174,10 @@ export async function runManagerTurn(params: { }); logVerbose( `acp-manager: switching backend for ${sessionKey} from ${describeBackendCandidate( - candidateBackends[backendIdx - 1], + expectDefined( + candidateBackends[backendIdx - 1], + "candidate backends entry at backend idx 1", + ), )} to ${describeBackendCandidate(currentBackend)}`, ); } diff --git a/src/acp/translator.presentation.ts b/src/acp/translator.presentation.ts index f946a1ce34fd..fc3710bebbd1 100644 --- a/src/acp/translator.presentation.ts +++ b/src/acp/translator.presentation.ts @@ -110,7 +110,7 @@ function formatThinkingLevelName(level: string): string { case "adaptive": return "Adaptive"; default: - return level.length > 0 ? `${level[0].toUpperCase()}${level.slice(1)}` : "Unknown"; + return level.length > 0 ? `${level.charAt(0).toUpperCase()}${level.slice(1)}` : "Unknown"; } } @@ -126,7 +126,7 @@ function formatConfigValueName(value: string): string { case "xhigh": return "Extra High"; default: - return value.length > 0 ? `${value[0].toUpperCase()}${value.slice(1)}` : "Unknown"; + return value.length > 0 ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : "Unknown"; } } diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index 39760c7b3776..0faa0ec4ea8c 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -324,10 +324,11 @@ function mergeCatalogRouteVariants( collector.indexByKey.set(key, collector.entries.length - 1); continue; } - collector.entries[existingIndex] = overlayCatalogMetadata( - collector.entries[existingIndex], - entry, - ); + const existingEntry = collector.entries[existingIndex]; + if (existingEntry === undefined) { + continue; + } + collector.entries[existingIndex] = overlayCatalogMetadata(existingEntry, entry); } } diff --git a/src/agents/provider-model-auth-source-plan.ts b/src/agents/provider-model-auth-source-plan.ts index 05549b3c0d51..795947e4f14b 100644 --- a/src/agents/provider-model-auth-source-plan.ts +++ b/src/agents/provider-model-auth-source-plan.ts @@ -125,7 +125,10 @@ export function buildProviderModelAuthSourcePlan(params: { } else { const available = ordered.filter((profile) => profile.readiness !== "unavailable"); if (available.length === 0) { - profiles = { kind: "all-unavailable", explicitOrder, first: ordered[0] }; + const [firstOrdered] = ordered; + profiles = firstOrdered + ? { kind: "all-unavailable", explicitOrder, first: firstOrdered } + : { kind: "empty", explicitOrder }; } else { const outsideCooldown = available.filter((profile) => profile.cooldown === "clear"); if (outsideCooldown.length > 0) { @@ -133,7 +136,10 @@ export function buildProviderModelAuthSourcePlan(params: { } else if (params.allowCooldown) { profiles = { kind: "usable", explicitOrder, profiles: available.slice(0, 1) }; } else { - profiles = { kind: "all-cooldown", explicitOrder, first: available[0] }; + const [firstAvailable] = available; + profiles = firstAvailable + ? { kind: "all-cooldown", explicitOrder, first: firstAvailable } + : { kind: "empty", explicitOrder }; } } } diff --git a/src/auto-reply/chunk.ts b/src/auto-reply/chunk.ts index ff6536ed37cb..a906a5715377 100644 --- a/src/auto-reply/chunk.ts +++ b/src/auto-reply/chunk.ts @@ -477,7 +477,7 @@ export function chunkMarkdownText(text: string, limit: number): string[] { } let rawChunk = `${reopenPrefix}${rawContent}`; - const brokeOnSeparator = breakIdx < text.length && /\s/.test(text[breakIdx]); + const brokeOnSeparator = breakIdx < text.length && /\s/.test(text.charAt(breakIdx)); let nextStart = Math.min(text.length, breakIdx + (brokeOnSeparator ? 1 : 0)); if (fenceToSplit) { @@ -536,7 +536,7 @@ function scanParenAwareBreakpoints( if (!isAllowed(i)) { continue; } - const char = text[i]; + const char = text.charAt(i); if (char === "(") { depth += 1; continue; diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index d544c384d36a..04a5d0b24499 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Command authorization helpers for owner and allowlist checks. */ import { normalizeOptionalLowercaseString, @@ -97,8 +98,8 @@ function resolveProviderFromContext( const inferred = inferredProviders.candidates; if (inferred.length === 1) { return { - providerId: inferred[0].providerId, - hadResolutionError: inferred[0].hadResolutionError, + providerId: expectDefined(inferred[0], "inferred entry at 0").providerId, + hadResolutionError: expectDefined(inferred[0], "inferred entry at 0").hadResolutionError, }; } return { diff --git a/src/auto-reply/commands-registry-normalize.ts b/src/auto-reply/commands-registry-normalize.ts index 4e8218d3d048..f9427c73ada8 100644 --- a/src/auto-reply/commands-registry-normalize.ts +++ b/src/auto-reply/commands-registry-normalize.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Normalizes slash-command text aliases and builds command detection caches. */ import { normalizeLowercaseStringOrEmpty, @@ -80,7 +81,7 @@ export function normalizeCommandBody(raw: string, options?: CommandNormalizeOpti const normalized = colonMatch ? (() => { const [, command, rest] = colonMatch; - const normalizedRest = rest.trimStart(); + const normalizedRest = expectDefined(rest, "commands registry normalize rest").trimStart(); return normalizedRest ? `/${command} ${normalizedRest}` : `/${command}`; })() : singleLine; diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index dcc2e53ef7ec..350753ba44d4 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -1,4 +1,5 @@ /** Command-registry facade for native specs, text aliases, argument parsing, and menus. */ +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { @@ -205,7 +206,7 @@ function parsePositionalArgs(definitions: CommandArgDefinition[], raw: string): values[definition.name] = tokens.slice(index).join(" "); break; } - values[definition.name] = tokens[index]; + values[definition.name] = expectDefined(tokens[index], "command argument token"); index += 1; } return values; diff --git a/src/auto-reply/heartbeat-filter.ts b/src/auto-reply/heartbeat-filter.ts index 7b15199be88d..b6aba1b5558f 100644 --- a/src/auto-reply/heartbeat-filter.ts +++ b/src/auto-reply/heartbeat-filter.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Transcript filter for removing heartbeat-only prompt/ack artifacts. import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString as readString } from "@openclaw/normalization-core/string-coerce"; @@ -348,7 +349,11 @@ function advancePastAdjacentToolResults( startIndex: number, ): number { let index = startIndex; - while (index < messages.length && isToolResultMessage(messages[index])) { + while (index < messages.length) { + const message = messages.at(index); + if (!message || !isToolResultMessage(message)) { + break; + } index++; } return index; @@ -362,17 +367,19 @@ function hasCompletedVisibleHeartbeatResponseToolCall( messages: HeartbeatTranscriptMessage[], index: number, ): boolean { - const visibleCalls = collectVisibleHeartbeatResponseToolCalls(messages[index]); + const message = messages.at(index); + if (!message) { + return false; + } + const visibleCalls = collectVisibleHeartbeatResponseToolCalls(message); if (visibleCalls.length === 0) { return false; } const callIds = new Set(visibleCalls.flatMap((call) => collectToolCallIds(call))); - for ( - let resultIndex = index + 1; - resultIndex < messages.length && isToolResultCompletionCandidate(messages[resultIndex]); - resultIndex++ - ) { - const result = messages[resultIndex]; + for (const result of messages.slice(index + 1)) { + if (!isToolResultCompletionCandidate(result)) { + break; + } if (!hasSuccessfulToolResultMessage(result)) { continue; } @@ -399,7 +406,10 @@ function resolveHeartbeatArtifactSpanEnd( let sawNonTerminalAssistantOutput = false; while (index < messages.length) { - const message = messages[index]; + const message = messages.at(index); + if (!message) { + break; + } if (isRealNonHeartbeatUserMessage(message, heartbeatPrompt)) { break; } @@ -458,15 +468,17 @@ export function filterHeartbeatTranscriptArtifacts = { agent_thought_chunk: false, }; +function isAcpSessionUpdateTag(tag: string): tag is AcpSessionUpdateTag { + return Object.hasOwn(ACP_TAG_VISIBILITY_DEFAULTS, tag); +} + /** ACP delivery strategy for projected assistant output. */ type AcpDeliveryMode = "live" | "final_only"; export type AcpHiddenBoundarySeparator = "none" | "space" | "newline" | "paragraph"; @@ -148,12 +152,16 @@ export function isAcpTagVisible(settings: AcpProjectionSettings, tag: string | u if (!tag) { return true; } - const override = settings.tagVisibility[tag as AcpSessionUpdateTag]; + if (!isAcpSessionUpdateTag(tag)) { + return true; + } + const override = settings.tagVisibility[tag]; if (typeof override === "boolean") { return override; } - if (Object.hasOwn(ACP_TAG_VISIBILITY_DEFAULTS, tag)) { - return ACP_TAG_VISIBILITY_DEFAULTS[tag as AcpSessionUpdateTag]; + const defaultVisibility = ACP_TAG_VISIBILITY_DEFAULTS[tag]; + if (defaultVisibility === undefined) { + throw new Error(`Missing ACP visibility default for ${tag}`); } - return true; + return defaultVisibility; } diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 7bbe3e11fded..f3b3dadcc14e 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -1,5 +1,6 @@ /** Agent-runner execution loop, fallback handling, and user-facing failure mapping. */ import crypto from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { hasNonEmptyString, normalizeLowercaseStringOrEmpty, @@ -564,7 +565,7 @@ function collapseRepeatedFailureDetail(message: string): string { .map((part) => part.trim()) .filter(Boolean); if (parts.length >= 2 && parts.every((part) => part === parts[0])) { - return parts[0]; + return expectDefined(parts[0], "parts entry at 0"); } return message.trim(); } diff --git a/src/auto-reply/reply/agent-runner-usage-line.ts b/src/auto-reply/reply/agent-runner-usage-line.ts index 8af1d1021a19..1594cbe9c72b 100644 --- a/src/auto-reply/reply/agent-runner-usage-line.ts +++ b/src/auto-reply/reply/agent-runner-usage-line.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { hasNonzeroUsage, type NormalizedUsage } from "../../agents/usage.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { PluginHookReplyUsageState } from "../../plugins/hook-types.js"; @@ -121,7 +122,7 @@ export const appendUsageLine = (payloads: ReplyPayload[], line: string): ReplyPa if (index === -1) { return [...payloads, { text: line }]; } - const existing = payloads[index]; + const existing = expectDefined(payloads[index], "payloads entry at index"); const existingText = existing.text ?? ""; const separator = existingText.endsWith("\n") ? "" : "\n"; const next = { diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 1126ffb3e7c9..f9e7d6af4d5f 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -1,5 +1,6 @@ // Orchestrates reply agent execution, payload building, and delivery callbacks. import crypto from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { hasSessionAutoModelFallbackProvenance, @@ -643,7 +644,10 @@ function derivePromptSegments(prompt: string | undefined): TracePromptSegmentVie end += 1; } if (end < lines.length) { - addChars(tagMatch[1], lines.slice(index, end + 1).join("\n").length); + addChars( + expectDefined(tagMatch[1], "tag match capture group 1"), + lines.slice(index, end + 1).join("\n").length, + ); index = end + 1; while ((lines[index] ?? "") === "") { index += 1; diff --git a/src/auto-reply/reply/commands-allowlist.ts b/src/auto-reply/reply/commands-allowlist.ts index e0406a180b58..fb360b681ecc 100644 --- a/src/auto-reply/reply/commands-allowlist.ts +++ b/src/auto-reply/reply/commands-allowlist.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Handles /allowlist commands across config and pairing-store targets. */ import { normalizeOptionalLowercaseString, @@ -109,7 +110,7 @@ function parseAllowlistCommand(raw: string): AllowlistCommand | null { } for (; i < tokens.length; i += 1) { - const token = tokens[i]; + const token = expectDefined(tokens[i], "tokens entry at i"); const lowered = normalizeOptionalLowercaseString(token) ?? ""; if (lowered === "--resolve" || lowered === "resolve") { resolve = true; diff --git a/src/auto-reply/reply/commands-approve.ts b/src/auto-reply/reply/commands-approve.ts index f3dd6831d1f3..32d06bcfa652 100644 --- a/src/auto-reply/reply/commands-approve.ts +++ b/src/auto-reply/reply/commands-approve.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Implements approval commands for pending tool and execution requests. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { @@ -68,7 +69,7 @@ function parseApproveCommand(raw: string): ParsedApproveCommand | null { return { ok: true, decision: DECISION_ALIASES[second], - id: tokens[0], + id: expectDefined(tokens[0], "tokens entry at 0"), }; } return { ok: false, error: APPROVE_USAGE_TEXT }; diff --git a/src/auto-reply/reply/commands-export-session.ts b/src/auto-reply/reply/commands-export-session.ts index aeb5a87f84c0..a05d8bbdc391 100644 --- a/src/auto-reply/reply/commands-export-session.ts +++ b/src/auto-reply/reply/commands-export-session.ts @@ -2,6 +2,7 @@ import fsp from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { expectDefined } from "@openclaw/normalization-core"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { readAcpSessionMetaForEntry } from "../../acp/runtime/session-meta.js"; import { @@ -191,7 +192,15 @@ async function generateHtml(sessionData: SessionData): Promise { ["MARKED_JS", markedJs], ["HIGHLIGHT_JS", hljsJs], ["JS", templateJs], - ].reduce((html, [name, value]) => replaceHtmlPlaceholder(html, name, value), template); + ].reduce( + (html, [name, value]) => + replaceHtmlPlaceholder( + html, + expectDefined(name, "commands export session name"), + expectDefined(value, "commands export session value"), + ), + template, + ); } function addCollisionSuffix(filePath: string, suffix: number): string { diff --git a/src/auto-reply/reply/config-mutations.ts b/src/auto-reply/reply/config-mutations.ts index e2747a7b78f8..c410675f38ac 100644 --- a/src/auto-reply/reply/config-mutations.ts +++ b/src/auto-reply/reply/config-mutations.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Config mutation helpers used by chat commands that edit OpenClaw config. */ import { setConfigValueAtPath, unsetConfigValueAtPath } from "../../config/config-paths.js"; import { @@ -22,7 +23,7 @@ function assertValidConfig( ): { config: OpenClawConfig } { const validated = validateConfigObjectWithPlugins(next); if (!validated.ok) { - const issue = validated.issues[0]; + const issue = expectDefined(validated.issues[0], "issues entry at 0"); throw new AutoReplyConfigMutationError( `Config invalid after ${action} (${issue.path}: ${issue.message}).`, ); diff --git a/src/auto-reply/reply/context-treemap.ts b/src/auto-reply/reply/context-treemap.ts index a60dc0f628e4..b45c4b49309a 100644 --- a/src/auto-reply/reply/context-treemap.ts +++ b/src/auto-reply/reply/context-treemap.ts @@ -3,6 +3,7 @@ import crypto from "node:crypto"; import { writeFile } from "node:fs/promises"; import path from "node:path"; import zlib from "node:zlib"; +import { expectDefined } from "@openclaw/normalization-core"; import type { SessionSystemPromptReport } from "../../config/sessions/types.js"; import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js"; import { estimateTokensFromChars } from "../../utils/cjk-chars.js"; @@ -150,13 +151,13 @@ function layoutBinary(rawItems: T[], rect: Rect): P return []; } if (items.length === 1) { - return [{ item: items[0], rect }]; + return [{ item: expectDefined(items[0], "items entry at 0"), rect }]; } const total = totalValue(items); let splitIndex = 1; let splitSum = items[0]?.value ?? 0; for (let i = 1; i < items.length - 1; i += 1) { - const next = splitSum + items[i].value; + const next = splitSum + expectDefined(items[i], "items entry at i").value; if (Math.abs(total / 2 - next) > Math.abs(total / 2 - splitSum)) { break; } @@ -234,9 +235,9 @@ class PngCanvas { const cursorY = Math.floor(y); for (const rawChar of text) { const char = rawChar.toUpperCase(); - const glyph = FONT[char] ?? FONT[" "]; + const glyph = expectDefined(FONT[char] ?? FONT[" "], "treemap font glyph"); for (let row = 0; row < glyph.length; row += 1) { - const line = glyph[row]; + const line = expectDefined(glyph[row], "treemap glyph row"); for (let col = 0; col < line.length; col += 1) { if (line[col] !== "1") { continue; diff --git a/src/auto-reply/reply/directive-parsing.ts b/src/auto-reply/reply/directive-parsing.ts index 67a7c9f51fd0..58be6e276024 100644 --- a/src/auto-reply/reply/directive-parsing.ts +++ b/src/auto-reply/reply/directive-parsing.ts @@ -2,12 +2,12 @@ export function skipDirectiveArgPrefix(raw: string): number { let i = 0; const len = raw.length; - while (i < len && /\s/.test(raw[i])) { + while (i < len && /\s/.test(raw.charAt(i))) { i += 1; } if (raw[i] === ":") { i += 1; - while (i < len && /\s/.test(raw[i])) { + while (i < len && /\s/.test(raw.charAt(i))) { i += 1; } } @@ -21,21 +21,21 @@ export function takeDirectiveToken( ): { token: string | null; nextIndex: number } { let i = startIndex; const len = raw.length; - while (i < len && /\s/.test(raw[i])) { + while (i < len && /\s/.test(raw.charAt(i))) { i += 1; } if (i >= len) { return { token: null, nextIndex: i }; } const start = i; - while (i < len && !/\s/.test(raw[i])) { + while (i < len && !/\s/.test(raw.charAt(i))) { i += 1; } if (start === i) { return { token: null, nextIndex: i }; } const token = raw.slice(start, i); - while (i < len && /\s/.test(raw[i])) { + while (i < len && /\s/.test(raw.charAt(i))) { i += 1; } return { token, nextIndex: i }; diff --git a/src/auto-reply/reply/directives.ts b/src/auto-reply/reply/directives.ts index a768adf80bfd..83bdd26c4b9e 100644 --- a/src/auto-reply/reply/directives.ts +++ b/src/auto-reply/reply/directives.ts @@ -46,17 +46,17 @@ const matchLevelDirective = ( const start = match.index; const directiveEnd = match.index + match[0].length; let i = directiveEnd; - while (i < body.length && /\s/.test(body[i])) { + while (i < body.length && /\s/.test(body.charAt(i))) { i += 1; } if (body[i] === ":") { i += 1; - while (i < body.length && /\s/.test(body[i])) { + while (i < body.length && /\s/.test(body.charAt(i))) { i += 1; } } const argStart = i; - while (i < body.length && /[A-Za-z-]/.test(body[i])) { + while (i < body.length && /[A-Za-z-]/.test(body.charAt(i))) { i += 1; } const candidate = i > argStart ? body.slice(argStart, i) : undefined; diff --git a/src/auto-reply/reply/history-media.ts b/src/auto-reply/reply/history-media.ts index 1a88cdb554d5..e31aba1665fa 100644 --- a/src/auto-reply/reply/history-media.ts +++ b/src/auto-reply/reply/history-media.ts @@ -1,5 +1,6 @@ // Extracts media attachment references from reply history entries. import { mimeTypeFromFilePath } from "@openclaw/media-core/mime"; +import { expectDefined } from "@openclaw/normalization-core"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { MsgContext } from "../templating.js"; @@ -67,7 +68,7 @@ export function resolveRecentInboundHistoryImages(params: { const seen = new Set(); const entries = resolveHistoryEntries(params.ctx); for (let index = entries.length - 1; index >= 0 && out.length < limit; index -= 1) { - const entry = entries[index]; + const entry = expectDefined(entries[index], "entries entry at index"); const timestamp = resolveTimestamp(entry?.timestamp); if (timestamp === undefined || Math.abs(nowMs - timestamp) > ttlMs) { continue; diff --git a/src/auto-reply/reply/model-selection-directive.ts b/src/auto-reply/reply/model-selection-directive.ts index 78d00acd1704..6bcd89d39159 100644 --- a/src/auto-reply/reply/model-selection-directive.ts +++ b/src/auto-reply/reply/model-selection-directive.ts @@ -1,5 +1,6 @@ // Normalizes model selection directives into provider and model ids. import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { modelKey } from "../../agents/model-ref-shared.js"; import { @@ -85,14 +86,19 @@ function boundedLevenshteinDistance(a: string, b: string, maxDistance: number): for (let i = 1; i <= aLen; i++) { curr[0] = i; - let rowMin = curr[0]; + let rowMin = expectDefined(curr[0], "curr entry at 0"); const aChar = a.charCodeAt(i - 1); for (let j = 1; j <= bLen; j++) { const cost = aChar === b.charCodeAt(j - 1) ? 0 : 1; - curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); - if (curr[j] < rowMin) { - rowMin = curr[j]; + const distance = Math.min( + expectDefined(prev[j], "prev entry at j") + 1, + expectDefined(curr[j - 1], "curr entry at j 1") + 1, + expectDefined(prev[j - 1], "prev entry at j 1") + cost, + ); + curr[j] = distance; + if (distance < rowMin) { + rowMin = distance; } } @@ -101,11 +107,11 @@ function boundedLevenshteinDistance(a: string, b: string, maxDistance: number): } for (let j = 0; j <= bLen; j++) { - prev[j] = curr[j]; + prev[j] = expectDefined(curr[j], "model selection directive edit-distance row"); } } - const dist = prev[bLen]; + const dist = expectDefined(prev[bLen], "prev entry at b len"); if (dist > maxDistance) { return null; } diff --git a/src/auto-reply/reply/post-compaction-context.ts b/src/auto-reply/reply/post-compaction-context.ts index d0e92d26c4bc..e287df68a074 100644 --- a/src/auto-reply/reply/post-compaction-context.ts +++ b/src/auto-reply/reply/post-compaction-context.ts @@ -1,6 +1,7 @@ // Loads post-compaction context summaries for continuation prompts. import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { resolveAgentContextLimits } from "../../agents/agent-scope.js"; @@ -187,7 +188,7 @@ export function extractSections( const headingMatch = line.match(/^(#{2,3})\s+(.+?)\s*$/); if (headingMatch) { - const level = headingMatch[1].length; // 2 or 3 + const level = expectDefined(headingMatch[1], "heading match capture group 1").length; // 2 or 3 const headingText = headingMatch[2]; if (!inSection) { diff --git a/src/auto-reply/reply/queue/drain.ts b/src/auto-reply/reply/queue/drain.ts index 954652fbadec..2019f9973d9a 100644 --- a/src/auto-reply/reply/queue/drain.ts +++ b/src/auto-reply/reply/queue/drain.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { runAgentHarnessBeforeMessageWriteHook } from "../../../agents/harness/hook-helpers.js"; import { normalizeChatType } from "../../../channels/chat-type.js"; @@ -624,7 +625,10 @@ function consumeQueueSummaryDelivery( (entry) => entry.sources.includes(source) || entry.sourceRefs.has(source), ); if (elisionIndex >= 0) { - const entry = queue.summaryElisions[elisionIndex]; + const entry = expectDefined( + queue.summaryElisions[elisionIndex], + "summary elisions entry at elision index", + ); const elidedSourceIndex = entry.sources.indexOf(entry.sourceRefs.get(source) ?? source); entry.sources.splice(elidedSourceIndex, 1); entry.count = entry.sources.length; @@ -659,7 +663,7 @@ function releaseQueueSummaryDeliveryForRetry( function dropAbortedQueueSummarySources(queue: FollowupQueueSummaryState): number { let dropped = 0; for (let index = queue.summarySources.length - 1; index >= 0; index -= 1) { - const source = queue.summarySources[index]; + const source = expectDefined(queue.summarySources[index], "summary sources entry at index"); if (!isFollowupRunAborted(source)) { continue; } @@ -775,7 +779,7 @@ async function dropAbortedFollowups( ): Promise { let dropped = 0; for (let index = items.length - 1; index >= 0; index -= 1) { - const item = items[index]; + const item = expectDefined(items[index], "items entry at index"); if (isFollowupRunAborted(item)) { await runFollowup(item); completeFollowupRunLifecycle(item); @@ -979,7 +983,7 @@ async function drainElidedOverflowSummary(params: { ) : []; for (let index = entry.sources.length - 1; index >= 0; index -= 1) { - const source = entry.sources[index]; + const source = expectDefined(entry.sources[index], "sources entry at index"); if (!isFollowupRunAborted(source)) { continue; } diff --git a/src/auto-reply/reply/queue/state.ts b/src/auto-reply/reply/queue/state.ts index c1d513c26aad..501d4840a0a5 100644 --- a/src/auto-reply/reply/queue/state.ts +++ b/src/auto-reply/reply/queue/state.ts @@ -77,8 +77,7 @@ export function trimSummaryElisionsToCap(queue: SummaryElisionCapState): void { ); while (sourceCount > queue.cap) { let evicted = false; - for (let entryIndex = 0; entryIndex < queue.summaryElisions.length; entryIndex += 1) { - const entry = queue.summaryElisions[entryIndex]; + for (const [entryIndex, entry] of queue.summaryElisions.entries()) { const sourceIndex = entry.sources.findIndex( (source) => !queue.activeSummarySources.has(source), ); diff --git a/src/auto-reply/reply/reply-payloads-dedupe.ts b/src/auto-reply/reply/reply-payloads-dedupe.ts index 1822f0f96bd1..1460dec49c5b 100644 --- a/src/auto-reply/reply/reply-payloads-dedupe.ts +++ b/src/auto-reply/reply/reply-payloads-dedupe.ts @@ -56,8 +56,7 @@ export function filterMessagingToolMediaDuplicates(params: { } let nextPayloads: ReplyPayload[] | undefined; - for (let index = 0; index < payloads.length; index++) { - const payload = payloads[index]; + for (const [index, payload] of payloads.entries()) { const mediaUrl = payload.mediaUrl; const mediaUrls = payload.mediaUrls; const stripSingle = mediaUrl && sentSet.has(normalizeMediaForDedupe(mediaUrl)); @@ -65,8 +64,7 @@ export function filterMessagingToolMediaDuplicates(params: { let filteredUrls: string[] | undefined; let strippedMediaUrls = false; if (mediaUrls?.length) { - for (let mediaIndex = 0; mediaIndex < mediaUrls.length; mediaIndex++) { - const url = mediaUrls[mediaIndex]; + for (const [mediaIndex, url] of mediaUrls.entries()) { if (sentSet.has(normalizeMediaForDedupe(url))) { strippedMediaUrls = true; if (!filteredUrls) { diff --git a/src/auto-reply/reply/streaming-directives.ts b/src/auto-reply/reply/streaming-directives.ts index e118bcd6a221..41fd9e6570e5 100644 --- a/src/auto-reply/reply/streaming-directives.ts +++ b/src/auto-reply/reply/streaming-directives.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Converts streaming reply directives into payload delivery decisions. import { hasOutboundReplyContent } from "openclaw/plugin-sdk/reply-payload"; import { parseInlineDirectives } from "../../utils/directive-tags.js"; @@ -75,7 +76,8 @@ export const splitTrailingDirective = ( const prefixMatch = text.match(/(?:^|\n)(MEDIA|MEDI|MED|ME|M)$/i); if (prefixMatch) { - const prefixStart = text.length - prefixMatch[1].length; + const prefixStart = + text.length - expectDefined(prefixMatch[1], "prefix match capture group 1").length; if (prefixStart < bufferStart) { bufferStart = prefixStart; } diff --git a/src/auto-reply/reply/strip-inbound-meta.ts b/src/auto-reply/reply/strip-inbound-meta.ts index 1b69742a88a3..c6e35b594630 100644 --- a/src/auto-reply/reply/strip-inbound-meta.ts +++ b/src/auto-reply/reply/strip-inbound-meta.ts @@ -179,8 +179,12 @@ function stripActiveMemoryPromptPrefixBlocks(lines: string[]): string[] { const result: string[] = []; for (let index = 0; index < lines.length; index += 1) { + const line = lines.at(index); + if (line === undefined) { + break; + } if ( - lines[index]?.trim() === UNTRUSTED_CONTEXT_HEADER && + line.trim() === UNTRUSTED_CONTEXT_HEADER && lines[index + 1]?.trim() === ACTIVE_MEMORY_OPEN_TAG ) { let closeIndex = -1; @@ -199,7 +203,7 @@ function stripActiveMemoryPromptPrefixBlocks(lines: string[]): string[] { } } - result.push(lines[index]); + result.push(line); } return result; @@ -238,8 +242,10 @@ export function stripInboundMetadata(text: string): string { let inFencedJson = false; for (let i = 0; i < strippedLeadingPrefixLines.length; i++) { - const line = strippedLeadingPrefixLines[i]; - + const line = strippedLeadingPrefixLines.at(i); + if (line === undefined) { + break; + } // Channel untrusted context is appended by OpenClaw as a terminal metadata suffix. // When this structured header appears, drop it and everything that follows. if (!inMetaBlock && shouldStripTrailingUntrustedContext(strippedLeadingPrefixLines, i)) { @@ -310,25 +316,34 @@ export function stripLeadingInboundMetadata(text: string): string { const lines = stripActiveMemoryPromptPrefixBlocks(text.split("\n")); let index = 0; - while (index < lines.length && lines[index] === "") { + while (lines.at(index) === "") { index++; } - if (index >= lines.length) { + const firstLine = lines.at(index); + if (firstLine === undefined) { return ""; } - const strippedDeliveryHint = isMessageToolDeliveryHintLine(lines[index]); - while (index < lines.length && isMessageToolDeliveryHintLine(lines[index])) { + const strippedDeliveryHint = isMessageToolDeliveryHintLine(firstLine); + while (true) { + const line = lines.at(index); + if (line === undefined || !isMessageToolDeliveryHintLine(line)) { + break; + } index++; - while (index < lines.length && lines[index] === "") { + while (lines.at(index) === "") { index++; } } - if (index >= lines.length) { + const firstContentLine = lines.at(index); + if (firstContentLine === undefined) { return ""; } - if (!isInboundMetaSentinelLine(lines[index]) && !isChatWindowContextHeaderLine(lines[index])) { + if ( + !isInboundMetaSentinelLine(firstContentLine) && + !isChatWindowContextHeaderLine(firstContentLine) + ) { const strippedNoLeading = stripTrailingUntrustedContextSuffix( strippedDeliveryHint ? lines.slice(index) : lines, ); @@ -336,7 +351,10 @@ export function stripLeadingInboundMetadata(text: string): string { } while (index < lines.length) { - const line = lines[index]; + const line = lines.at(index); + if (line === undefined) { + break; + } if (isChatWindowContextHeaderLine(line)) { index = skipChatWindowContextBlock(lines, index); continue; @@ -351,19 +369,19 @@ export function stripLeadingInboundMetadata(text: string): string { } index++; - if (index < lines.length && lines[index].trim() === "```json") { + if (lines.at(index)?.trim() === "```json") { index++; - while (index < lines.length && lines[index].trim() !== "```") { + while (index < lines.length && lines.at(index)?.trim() !== "```") { index++; } - if (index < lines.length && lines[index].trim() === "```") { + if (lines.at(index)?.trim() === "```") { index++; } } else { return text; } - while (index < lines.length && lines[index].trim() === "") { + while (lines.at(index)?.trim() === "") { index++; } } diff --git a/src/auto-reply/test-helpers/command-auth-registry-fixture.ts b/src/auto-reply/test-helpers/command-auth-registry-fixture.ts index 9f9d9819f31c..379a99589207 100644 --- a/src/auto-reply/test-helpers/command-auth-registry-fixture.ts +++ b/src/auto-reply/test-helpers/command-auth-registry-fixture.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Test registry fixture for command authorization across Discord and phone-based channels. */ import { lowercasePreservingWhitespace } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; @@ -24,12 +25,16 @@ function normalizePhoneAllowFromEntries(allowFrom: Array): stri } if (/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i.test(stripped)) { const match = stripped.match(/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i); - return match ? normalizeE164(match[1]) : null; + return match + ? normalizeE164(expectDefined(match[1], "command auth registry fixture regex capture 1")) + : null; } // WhatsApp LID values are numeric identifiers; test fixtures map them like phone ids. if (/^(\d+)@lid$/i.test(stripped)) { const match = stripped.match(/^(\d+)@lid$/i); - return match ? normalizeE164(match[1]) : null; + return match + ? normalizeE164(expectDefined(match[1], "command auth registry fixture regex capture 1")) + : null; } if (stripped.includes("@")) { return null; diff --git a/src/auto-reply/usage-bar/translator.ts b/src/auto-reply/usage-bar/translator.ts index 3846d75c96bf..6586b163ec63 100644 --- a/src/auto-reply/usage-bar/translator.ts +++ b/src/auto-reply/usage-bar/translator.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; export type UsageBarTemplate = Record; export type UsageContract = Record; type Vocab = Record; @@ -92,8 +93,8 @@ function meter(value: unknown, width: number, scale: unknown): string { if (glyphs.length < 2 || width < 1) { return ""; } - const empty = glyphs[0]; - const full = glyphs[glyphs.length - 1]; + const empty = expectDefined(glyphs[0], "glyphs entry at 0"); + const full = expectDefined(glyphs[glyphs.length - 1], "glyphs entry at glyphs.length 1"); const total = norm(value) * width; const fullc = Math.trunc(total); const cells: string[] = []; @@ -101,7 +102,12 @@ function meter(value: unknown, width: number, scale: unknown): string { cells.push(full); } if (cells.length < width) { - cells.push(glyphs[Math.round((total - fullc) * (glyphs.length - 1))]); + cells.push( + expectDefined( + glyphs[Math.round((total - fullc) * (glyphs.length - 1))], + "glyphs entry at math.round((total fullc) * (glyphs.length 1))", + ), + ); } while (cells.length < width) { cells.push(empty); @@ -138,7 +144,7 @@ function applyVerb(name: string, args: string[], value: unknown, vocab: Vocab): } case "meter": { const width = args[0] ? Number.parseInt(args[0], 10) || 5 : 5; - const scale = args.length > 1 ? vocab[args[1]] : undefined; + const scale = args.length > 1 ? vocab[expectDefined(args[1], "args entry at 1")] : undefined; return meter(value, width, scale); } default: @@ -170,7 +176,7 @@ function interp(text: string, ctx: unknown, vocab: Vocab): string { let fallback: string | undefined; for (const segRaw of parts.slice(1)) { const seg = segRaw.trim(); - const name = seg.split(":")[0]; + const name = expectDefined(seg.split(":")[0], 'seg.split(":") entry at 0'); if (VERB_NAMES.has(name)) { ops.push({ name, args: seg.split(":").slice(1) }); } else { @@ -212,7 +218,15 @@ function renderSegment(seg: Segment, ctx: unknown, vocab: Vocab): string | null items.forEach((el, i) => { let iv = vocab; if (names && names.length > 0) { - iv = { ...vocab, "*": vocab[names[Math.min(i, names.length - 1)]] }; + iv = { + ...vocab, + "*": vocab[ + expectDefined( + names[Math.min(i, names.length - 1)], + "names entry at math.min(i, names.length 1)", + ) + ], + }; } const r = interp(itemTpl, el, iv); if (r) { diff --git a/src/channels/chat-meta.ts b/src/channels/chat-meta.ts index ece54c22b741..d4df46cc9878 100644 --- a/src/channels/chat-meta.ts +++ b/src/channels/chat-meta.ts @@ -3,6 +3,7 @@ * * Provides ordered channel metadata for setup, status, and selection surfaces. */ +import { expectDefined } from "@openclaw/normalization-core"; import { buildChatChannelMetaById, type ChatChannelMeta } from "./chat-meta-shared.js"; import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "./ids.js"; @@ -26,6 +27,16 @@ export function listChatChannels(): ChatChannelMeta[] { /** * Returns metadata for one built-in chat channel id. */ -export function getChatChannelMeta(id: ChatChannelId): ChatChannelMeta { +/** Drift-tolerant lookup: undefined when the id is missing from the bundled catalog. */ +export function findChatChannelMeta(id: ChatChannelId): ChatChannelMeta | undefined { return getChatChannelMetaById()[id]; } + +/** + * Returns metadata for one built-in chat channel id. + * Shipped plugin-SDK contract: callers pass bundled ids, so absence is an invariant + * violation; drift-tolerant core paths use findChatChannelMeta instead. + */ +export function getChatChannelMeta(id: ChatChannelId): ChatChannelMeta { + return expectDefined(findChatChannelMeta(id), `chat channel meta for ${id}`); +} diff --git a/src/channels/message-access/runtime-identity.ts b/src/channels/message-access/runtime-identity.ts index e929e5d14442..854a0bdec94b 100644 --- a/src/channels/message-access/runtime-identity.ts +++ b/src/channels/message-access/runtime-identity.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** * Channel ingress identity adapter helpers. * @@ -112,7 +113,7 @@ export function createIdentityAdapter( return [ adapterEntry({ identity, - field: fields[0], + field: expectDefined(fields[0], "fields entry at 0"), fieldIndex: 0, entry, entryIndex, diff --git a/src/channels/plugins/dm-access.ts b/src/channels/plugins/dm-access.ts index 2ebfeff7adff..5fb4d347bbac 100644 --- a/src/channels/plugins/dm-access.ts +++ b/src/channels/plugins/dm-access.ts @@ -93,34 +93,49 @@ function readPath(entry: DmAccessRecord | null | undefined, path: readonly strin } function deletePath(entry: DmAccessRecord, path: readonly string[]): boolean { - if (path.length === 1) { - if (entry[path[0]] === undefined) { - return false; - } - delete entry[path[0]]; - return true; - } - const parent = asObjectRecord(entry[path[0]]); - if (!parent || parent[path[1]] === undefined) { + const [head, tail] = path; + if (head === undefined) { return false; } - delete parent[path[1]]; + if (path.length === 1) { + if (entry[head] === undefined) { + return false; + } + delete entry[head]; + return true; + } + if (tail === undefined) { + return false; + } + const parent = asObjectRecord(entry[head]); + if (!parent || parent[tail] === undefined) { + return false; + } + delete parent[tail]; if (Object.keys(parent).length === 0) { - delete entry[path[0]]; + delete entry[head]; } else { - entry[path[0]] = parent; + entry[head] = parent; } return true; } function writePath(entry: DmAccessRecord, path: readonly string[], value: unknown): void { - if (path.length === 1) { - entry[path[0]] = value; + const [head, tail] = path; + if (head === undefined) { return; } - const parent = asObjectRecord(entry[path[0]]) ? { ...(entry[path[0]] as DmAccessRecord) } : {}; - parent[path[1]] = value; - entry[path[0]] = parent; + if (path.length === 1) { + entry[head] = value; + return; + } + if (tail === undefined) { + return; + } + const existingParent = asObjectRecord(entry[head]); + const parent = existingParent ? { ...existingParent } : {}; + parent[tail] = value; + entry[head] = parent; } function allowFromListsMatch(left: unknown, right: unknown): boolean { diff --git a/src/channels/plugins/setup-helpers.ts b/src/channels/plugins/setup-helpers.ts index a60295b22f7d..490ab4531783 100644 --- a/src/channels/plugins/setup-helpers.ts +++ b/src/channels/plugins/setup-helpers.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** * Channel setup config mutation helpers. * @@ -486,7 +487,9 @@ function resolveSingleAccountPromotionTarget(params: { channel: ChannelSectionBa ); } const namedAccounts = Object.keys(accounts).filter(Boolean); - return namedAccounts.length === 1 ? namedAccounts[0] : DEFAULT_ACCOUNT_ID; + return namedAccounts.length === 1 + ? expectDefined(namedAccounts[0], "named accounts entry at 0") + : DEFAULT_ACCOUNT_ID; } /** diff --git a/src/channels/plugins/setup-wizard.ts b/src/channels/plugins/setup-wizard.ts index d0f6d6735061..d7f3ece07380 100644 --- a/src/channels/plugins/setup-wizard.ts +++ b/src/channels/plugins/setup-wizard.ts @@ -562,8 +562,9 @@ export function buildChannelSetupWizardAdapterFromSetupWizard(params: { const allowFrom = wizard.allowFrom; // Allowlist resolution often needs the freshly entered credential, not // only the persisted config, because setup may not have been written yet. + const credentialInputKey = allowFrom.credentialInputKey ?? wizard.credentials[0]?.inputKey; const allowFromCredentialValue = normalizeOptionalString( - credentialValues[allowFrom.credentialInputKey ?? wizard.credentials[0]?.inputKey], + credentialInputKey === undefined ? undefined : credentialValues[credentialInputKey], ); if (allowFrom.helpLines && allowFrom.helpLines.length > 0) { await prompter.note( diff --git a/src/channels/registry.ts b/src/channels/registry.ts index fd99558bd82b..de9874e36039 100644 --- a/src/channels/registry.ts +++ b/src/channels/registry.ts @@ -11,7 +11,7 @@ import { findRegisteredChannelPluginEntryById, listRegisteredChannelPluginEntries, } from "./registry-lookup.js"; -export { getChatChannelMeta } from "./chat-meta.js"; +export { findChatChannelMeta, getChatChannelMeta } from "./chat-meta.js"; export { CHAT_CHANNEL_ORDER } from "./ids.js"; export type { ChatChannelId } from "./ids.js"; export { normalizeChatChannelId }; diff --git a/src/channels/streaming.ts b/src/channels/streaming.ts index 476fa10293a2..e7dc8d12b938 100644 --- a/src/channels/streaming.ts +++ b/src/channels/streaming.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Channel streaming config normalization and progress-draft formatting helpers. import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; @@ -1139,7 +1140,10 @@ export function mergeChannelProgressDraftLine lineKeys.includes(entryKey)), ); if (existingIndex >= 0) { - const replacement = mergeProgressDraftLineUpdate(lines[existingIndex], line); + const replacement = mergeProgressDraftLineUpdate( + expectDefined(lines[existingIndex], "lines entry at existing index"), + line, + ); if (replacement === lines[existingIndex]) { return lines; } diff --git a/src/chat/canvas-render.ts b/src/chat/canvas-render.ts index 04915dd36239..5b785dd9744a 100644 --- a/src/chat/canvas-render.ts +++ b/src/chat/canvas-render.ts @@ -1,5 +1,5 @@ // Renders chat canvas payloads into text and metadata for transcript output. -import { safeParseJson } from "@openclaw/normalization-core"; +import { expectDefined, safeParseJson } from "@openclaw/normalization-core"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import { parseFenceSpans } from "../../packages/markdown-core/src/fences.js"; @@ -166,7 +166,7 @@ function previewFromShortcode(attrs: Record): CanvasPreview | un kind: "canvas", surface, render: "url", - url: url ?? defaultCanvasEntryUrl(ref), + url: url ?? defaultCanvasEntryUrl(expectDefined(ref, "canvas reference")), ...(ref ? { viewId: ref } : {}), ...(title ? { title } : {}), ...(preferredHeight ? { preferredHeight } : {}), diff --git a/src/cli/argv.ts b/src/cli/argv.ts index 0df3e7195000..20e7e12d628a 100644 --- a/src/cli/argv.ts +++ b/src/cli/argv.ts @@ -229,8 +229,12 @@ export function normalizeGeneratedHelpCommandArgv(argv: string[]): string[] { ) { return argv; } + const [runtimePath, entryPath] = argv; + if (runtimePath === undefined || entryPath === undefined) { + return argv; + } - return [argv[0], argv[1], ...rootOptions, primary.value, target.value, "--help"]; + return [runtimePath, entryPath, ...rootOptions, primary.value, target.value, "--help"]; } export function normalizeRootHelpTargetArgv(argv: string[]): string[] { @@ -241,16 +245,24 @@ export function normalizeRootHelpTargetArgv(argv: string[]): string[] { const { positionals, rootOptions, helpFlagIndex } = scan; const [help, target] = positionals; + // The --help flag must trail the LAST positional so nested targets like + // `help plugins list --help` still normalize (target is only the first one). + const lastPositional = positionals.at(-1); if ( help?.value !== "help" || !target || - (helpFlagIndex !== null && helpFlagIndex !== positionals.at(-1)!.index + 1) + !lastPositional || + (helpFlagIndex !== null && helpFlagIndex !== lastPositional.index + 1) ) { return argv; } + const [runtimePath, entryPath] = argv; + if (runtimePath === undefined || entryPath === undefined) { + return argv; + } const targetPath = positionals.slice(1).map((positional) => positional.value); - return [argv[0], argv[1], ...rootOptions, ...targetPath, "--help"]; + return [runtimePath, entryPath, ...rootOptions, ...targetPath, "--help"]; } export type NormalizeRootNoColorArgvOptions = { @@ -328,7 +340,10 @@ export function normalizeRootNoColorArgv( const movedNoColorArgs: string[] = []; const nextArgs: string[] = []; for (let index = 0; index < remainingArgs.length; index += 1) { - const arg = remainingArgs[index]; + const arg = remainingArgs.at(index); + if (arg === undefined) { + break; + } if (arg === FLAG_TERMINATOR) { nextArgs.push(...remainingArgs.slice(index)); break; @@ -363,7 +378,10 @@ export function normalizeRootLogLevelArgv( const movedLogLevelArgs: string[] = []; const nextArgs: string[] = []; for (let index = 0; index < remainingArgs.length; index += 1) { - const arg = remainingArgs[index]; + const arg = remainingArgs.at(index); + if (arg === undefined) { + break; + } if (arg === FLAG_TERMINATOR) { nextArgs.push(...remainingArgs.slice(index)); break; @@ -398,7 +416,10 @@ export function getFlagValue(argv: string[], name: string): string | null | unde const args = argv.slice(2); let value: string | undefined; for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; + const arg = args.at(i); + if (arg === undefined) { + break; + } if (arg === FLAG_TERMINATOR) { break; } diff --git a/src/cli/channel-auth.ts b/src/cli/channel-auth.ts index 5582d06051ee..0305173fa8ea 100644 --- a/src/cli/channel-auth.ts +++ b/src/cli/channel-auth.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Channel login/logout command helpers for local config and gateway reconciliation. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { sanitizeForLog } from "../../packages/terminal-core/src/ansi.js"; @@ -74,7 +75,7 @@ function resolveConfiguredAuthChannelInput(cfg: OpenClawConfig, mode: ChannelAut .map((plugin) => plugin.id); if (configured.length === 1) { - return configured[0]; + return expectDefined(configured[0], "configured entry at 0"); } if (configured.length === 0) { throw new Error( diff --git a/src/cli/command-path-policy.ts b/src/cli/command-path-policy.ts index e17f8a9cb15f..7dc0a3bf1f28 100644 --- a/src/cli/command-path-policy.ts +++ b/src/cli/command-path-policy.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Resolves CLI command path policy from the declarative command catalog. import { isGatewayConfigBypassCommandPath } from "../gateway/explicit-connection-policy.js"; import { getCommandPathWithRootOptions } from "./argv.js"; @@ -58,7 +59,7 @@ export function resolveCliCatalogCommandPath(argv: string[]): string[] { bestMatch = entry.commandPath; } } - return bestMatch ? [...bestMatch] : [tokens[0]]; + return bestMatch ? [...bestMatch] : [expectDefined(tokens[0], "tokens entry at 0")]; } export function resolveCliNetworkProxyPolicy(argv: string[]): CliNetworkProxyPolicy { diff --git a/src/cli/config-cli.ts b/src/cli/config-cli.ts index cb9ffa70b97b..ebd7d3ceebea 100644 --- a/src/cli/config-cli.ts +++ b/src/cli/config-cli.ts @@ -1,5 +1,6 @@ // Config CLI command implementation for get/set/unset/patch/validate and secret refs. import fs from "node:fs"; +import { expectDefined } from "@openclaw/normalization-core"; import { isRecord as isPlainRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { @@ -282,7 +283,9 @@ function normalizeConfigMutationModelRefs(cfg: OpenClawConfig): OpenClawConfig { function normalizeConfigMutationExplicitSetPath(path: PathSegment[]): PathSegment[] { if (path.length >= 4 && path[0] === "agents" && path[1] === "defaults" && path[2] === "models") { - const normalizedModelId = normalizeAgentModelRefForConfig(path[3]); + const normalizedModelId = normalizeAgentModelRefForConfig( + expectDefined(path[3], "path entry at 3"), + ); return normalizedModelId === path[3] ? path : [...path.slice(0, 3), normalizedModelId, ...path.slice(4)]; @@ -687,9 +690,12 @@ function setAtPath( value: unknown, options?: SetAtPathOptions, ): void { + const last = path.at(-1); + if (last === undefined) { + throw new Error("Config path must contain at least one segment"); + } let current: unknown = root; - for (let i = 0; i < path.length - 1; i += 1) { - const segment = path[i]; + for (const [i, segment] of path.slice(0, -1).entries()) { const next = path[i + 1]; const nextIsIndex = shouldCreateArrayForMissingPathSegment({ path, @@ -723,7 +729,6 @@ function setAtPath( current = record[segment]; } - const last = path[path.length - 1]; if (Array.isArray(current)) { if (!isIndexSegment(last)) { throw new Error(`Expected numeric index for array segment "${last}"`); @@ -892,9 +897,12 @@ function assertNonDestructiveReplacement(params: { type UnsetAtPathResult = { removed: true; leafContainer: "array" | "object" } | { removed: false }; function unsetAtPath(root: Record, path: PathSegment[]): UnsetAtPathResult { + const last = path.at(-1); + if (last === undefined) { + return { removed: false }; + } let current: unknown = root; - for (let i = 0; i < path.length - 1; i += 1) { - const segment = path[i]; + for (const segment of path.slice(0, -1)) { if (!current || typeof current !== "object") { return { removed: false }; } @@ -916,7 +924,6 @@ function unsetAtPath(root: Record, path: PathSegment[]): UnsetA current = record[segment]; } - const last = path[path.length - 1]; if (Array.isArray(current)) { if (!isIndexSegment(last)) { return { removed: false }; diff --git a/src/cli/container-target.ts b/src/cli/container-target.ts index c56b28ec07ce..0f410af8dd5e 100644 --- a/src/cli/container-target.ts +++ b/src/cli/container-target.ts @@ -1,6 +1,7 @@ // CLI container targeting: parse --container and re-exec the command inside Docker/Podman. import { spawnSync } from "node:child_process"; import { isIP } from "node:net"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; @@ -124,7 +125,7 @@ function resolveRunningContainer(params: { `Container "${params.containerName}" is running under multiple runtimes (${runtimes}); use a unique container name.`, ); } - return matches[0]; + return expectDefined(matches[0], "matches capture group 0"); } function buildContainerExecArgs(params: { @@ -196,7 +197,7 @@ function isLoopbackProxyHostname(hostname: string): boolean { if (!mapped) { return false; } - const high = Number.parseInt(mapped[1], 16); + const high = Number.parseInt(expectDefined(mapped[1], "mapped capture group 1"), 16); return Number.isInteger(high) && high >= 0x7f00 && high <= 0x7fff; } diff --git a/src/cli/daemon-cli/lifecycle.ts b/src/cli/daemon-cli/lifecycle.ts index d0a0a6af7f9f..3ffc8eda1f9c 100644 --- a/src/cli/daemon-cli/lifecycle.ts +++ b/src/cli/daemon-cli/lifecycle.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Gateway service lifecycle runners, including unmanaged-process fallbacks and restart health checks. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { theme } from "../../../packages/terminal-core/src/theme.js"; @@ -252,7 +253,7 @@ async function restartGatewayWithoutServiceManager( reason: "gateway.restart", ...(restartIntent ? { intent: restartIntent } : {}), }); - signalVerifiedGatewayPidSync(pids[0], "SIGUSR1"); + signalVerifiedGatewayPidSync(expectDefined(pids[0], "pids entry at 0"), "SIGUSR1"); return { result: "restarted" as const, message: `Gateway restart signal sent to unmanaged process on port ${port}: ${pids[0]}.`, diff --git a/src/cli/exec-approvals-cli.ts b/src/cli/exec-approvals-cli.ts index 02a1807edd35..5126af90fd61 100644 --- a/src/cli/exec-approvals-cli.ts +++ b/src/cli/exec-approvals-cli.ts @@ -1,5 +1,6 @@ // CLI for reading and mutating exec approval allowlists locally, via gateway, or via node. import fs from "node:fs/promises"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { Command } from "commander"; @@ -342,7 +343,7 @@ async function saveSnapshotTargeted(params: SaveSnapshotTargetedParams): Promise function formatCliError(err: unknown): string { const msg = formatErrorMessage(err); const firstLine = msg.includes("\n") ? msg.split("\n")[0] : msg; - const safe = sanitizeForLog(firstLine); + const safe = sanitizeForLog(expectDefined(firstLine, "exec approvals cli first line")); return safe.length > 300 ? `${truncateUtf16Safe(safe, 300)}...` : safe; } diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index fc96d533b60f..d0f3f4f72e54 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import { request } from "node:http"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -183,7 +184,7 @@ function formatModeErrorList(modes: readonly string[]): string { return ""; } if (quoted.length === 1) { - return quoted[0]; + return expectDefined(quoted[0], "quoted entry at 0"); } if (quoted.length === 2) { return `${quoted[0]} or ${quoted[1]}`; diff --git a/src/cli/lobster-art.ts b/src/cli/lobster-art.ts index 3b10a16577cb..01b37d2ee5e8 100644 --- a/src/cli/lobster-art.ts +++ b/src/cli/lobster-art.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // The Control UI lobster pet has a CLI cousin: on roughly one day in sixteen // the interactive banner gains a tiny ASCII lobster. The day comes from the // shared lobster-day hash (the sidebar pet dresses up on the same days), so @@ -22,5 +23,8 @@ export function pickCliLobsterArt(now: Date, env: NodeJS.ProcessEnv = process.en if (!isLobsterDay(now)) { return null; } - return LOBSTER_ARTS[(lobsterDayHash(now) >>> 8) % LOBSTER_ARTS.length]; + return expectDefined( + LOBSTER_ARTS[(lobsterDayHash(now) >>> 8) % LOBSTER_ARTS.length], + "lobster arts entry at (lobster day hash(now) >>> 8) % lobster arts.length", + ); } diff --git a/src/cli/logs-cli.runtime.ts b/src/cli/logs-cli.runtime.ts index 722adbb5f19c..e559dcd37507 100644 --- a/src/cli/logs-cli.runtime.ts +++ b/src/cli/logs-cli.runtime.ts @@ -1,5 +1,6 @@ // Runtime helpers for bounded subprocess log tails and service runtime lookups. import { spawn } from "node:child_process"; +import { expectDefined } from "@openclaw/normalization-core"; export { buildGatewayConnectionDetails } from "../gateway/call.js"; export { resolveGatewaySystemdServiceName } from "../daemon/constants.js"; @@ -15,7 +16,7 @@ function appendByteTail(tail: ByteTail, chunk: Buffer, maxBytes: number): void { tail.chunks.push(chunk); tail.bytes += chunk.length; while (tail.bytes > maxBytes && tail.chunks.length > 0) { - const first = tail.chunks[0]; + const first = expectDefined(tail.chunks[0], "chunks entry at 0"); const overflow = tail.bytes - maxBytes; if (first.length <= overflow) { tail.chunks.shift(); @@ -36,7 +37,10 @@ function decodeUtf8Tail(tail: ByteTail): string { // A byte cap can cut the leading code point. Skip only its continuation // bytes so decoding cannot invent a replacement character at the boundary. let offset = 0; - while (offset < buffer.length && (buffer[offset] & 0xc0) === 0x80) { + while ( + offset < buffer.length && + (expectDefined(buffer[offset], "buffer entry at offset") & 0xc0) === 0x80 + ) { offset += 1; } return buffer.subarray(offset).toString("utf8"); diff --git a/src/cli/mcp-cli.ts b/src/cli/mcp-cli.ts index 26ecc66bf3c8..f7f64846c3ff 100644 --- a/src/cli/mcp-cli.ts +++ b/src/cli/mcp-cli.ts @@ -2,6 +2,7 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty, @@ -550,7 +551,7 @@ async function probeMcpServersOrFail(params: { try { const result = formatMcpProbeResult(await runtime.getCatalog()); if (result.diagnostics.length > 0) { - const first = result.diagnostics[0]; + const first = expectDefined(result.diagnostics[0], "diagnostics entry at 0"); fail(`MCP probe failed for "${first.serverName}" in ${params.path}: ${first.message}`); } for (const name of Object.keys(params.servers)) { diff --git a/src/cli/parse-duration.ts b/src/cli/parse-duration.ts index 64342e083cdf..11ff755e8ff8 100644 --- a/src/cli/parse-duration.ts +++ b/src/cli/parse-duration.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Duration parser shared by CLI flags, command directives, and config-backed timing values. import { normalizeLowercaseStringOrEmpty, @@ -46,7 +47,10 @@ export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): num throw invalidDuration(raw); } const unit = (single[2] ?? opts?.defaultUnit ?? "ms") as "ms" | "s" | "m" | "h" | "d"; - return roundSafeDurationMs(raw, value * DURATION_MULTIPLIERS[unit]); + return roundSafeDurationMs( + raw, + value * expectDefined(DURATION_MULTIPLIERS[unit], "duration multipliers entry at unit"), + ); } // Composite form (e.g. "1h30m", "2m500ms"); each token must include a unit. diff --git a/src/cli/plugin-install-config-policy.ts b/src/cli/plugin-install-config-policy.ts index 10e4d27b4cd2..b8f81bf7421b 100644 --- a/src/cli/plugin-install-config-policy.ts +++ b/src/cli/plugin-install-config-policy.ts @@ -180,7 +180,10 @@ function resolvePluginInstallArgvRequest(commandPath: string[], argv: string[]) let rawSpec: string | null = null; let marketplace: string | undefined; for (let index = 0; index < tokens.length; index += 1) { - const token = tokens[index]; + const token = tokens.at(index); + if (token === undefined) { + break; + } if (token.startsWith("--marketplace=")) { marketplace = token.slice("--marketplace=".length); continue; diff --git a/src/cli/plugins-update-selection.ts b/src/cli/plugins-update-selection.ts index 0899178f9b1f..03895b04dffc 100644 --- a/src/cli/plugins-update-selection.ts +++ b/src/cli/plugins-update-selection.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Plugin and hook-pack update selectors for id and npm-spec command inputs. import type { HookInstallRecord } from "../config/types.hooks.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; @@ -35,7 +36,7 @@ export function resolvePluginUpdateSelection(params: { return { pluginIds: [params.rawId] }; } - const [pluginId] = matches[0]; + const [pluginId] = expectDefined(matches[0], "matches capture group 0"); if (!pluginId) { return { pluginIds: [params.rawId] }; } @@ -83,7 +84,7 @@ export function resolveHookPackUpdateSelection(params: { return { hookIds: [] }; } - const [hookId] = matches[0]; + const [hookId] = expectDefined(matches[0], "matches capture group 0"); if (!hookId) { return { hookIds: [] }; } diff --git a/src/cli/profile.ts b/src/cli/profile.ts index 92a22d42a64c..b354badcf4f3 100644 --- a/src/cli/profile.ts +++ b/src/cli/profile.ts @@ -40,7 +40,7 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult { const [primary, secondary] = resolveCliArgvInvocation(out).commandPath; if (primary === "qa" && secondary === "matrix") { out.push(arg); - if (consumedNext) { + if (consumedNext && next !== undefined) { out.push(next); } return { kind: "handled", consumedNext }; diff --git a/src/cli/program/action-reparse.ts b/src/cli/program/action-reparse.ts index 3527eb817900..8614c58e2346 100644 --- a/src/cli/program/action-reparse.ts +++ b/src/cli/program/action-reparse.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Reparse support for lazy commands after their placeholder has been replaced. import type { Command, Option } from "commander"; import { buildParseArgv } from "../argv.js"; @@ -26,7 +27,10 @@ function buildFallbackArgv(program: Command, actionCommand: Command | undefined) return [ ...commandPath.slice(0, -1), ...parentOptionArgs, - commandPath[commandPath.length - 1], + expectDefined( + commandPath[commandPath.length - 1], + "command path entry at command path.length 1", + ), ...actionArgsList, ]; } diff --git a/src/cli/program/route-args.ts b/src/cli/program/route-args.ts index 6da5bc0c7405..8c8483c70bc0 100644 --- a/src/cli/program/route-args.ts +++ b/src/cli/program/route-args.ts @@ -32,7 +32,7 @@ function parseRepeatedFlagValues(argv: string[], name: string): string[] | null } if (arg === name) { const next = args[i + 1]; - if (!isValueToken(next)) { + if (next === undefined || !isValueToken(next)) { // Invalid fast-path shapes fall back to Commander so its normal errors and help text win. return null; } diff --git a/src/cli/proxy-cli.runtime.ts b/src/cli/proxy-cli.runtime.ts index 40639db181b1..4c74169cd8fa 100644 --- a/src/cli/proxy-cli.runtime.ts +++ b/src/cli/proxy-cli.runtime.ts @@ -2,6 +2,7 @@ import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import process from "node:process"; +import { expectDefined } from "@openclaw/normalization-core"; import { colorize, isRich, theme } from "../../packages/terminal-core/src/theme.js"; import { getRuntimeConfig } from "../config/config.js"; import { @@ -100,7 +101,7 @@ export async function runDebugProxyRunCommand(opts: { }); try { await new Promise((resolve, reject) => { - const child = spawn(command, args, { + const child = spawn(expectDefined(command, "proxy cli.runtime command"), args, { stdio: "inherit", env: childEnv, cwd: process.cwd(), diff --git a/src/cli/tagline.ts b/src/cli/tagline.ts index e08610e7f885..5d7cd20f327c 100644 --- a/src/cli/tagline.ts +++ b/src/cli/tagline.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // CLI tagline selection helpers, including deterministic random/default/holiday modes. import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; @@ -321,13 +322,13 @@ export function pickTagline(options: TaglineOptions = {}): string { const parsed = parseStrictNonNegativeInteger(override); if (parsed !== undefined) { const pool = TAGLINES.length > 0 ? TAGLINES : [DEFAULT_TAGLINE]; - return pool[parsed % pool.length]; + return expectDefined(pool[parsed % pool.length], "pool entry at parsed % pool.length"); } } const pool = activeTaglines(options); const rand = options.random ?? Math.random; const index = Math.floor(rand() * pool.length) % pool.length; - return pool[index]; + return expectDefined(pool[index], "pool entry at index"); } export { DEFAULT_TAGLINE }; diff --git a/src/commands/agents.bindings.ts b/src/commands/agents.bindings.ts index 80f0f9818549..63e528e3c4cf 100644 --- a/src/commands/agents.bindings.ts +++ b/src/commands/agents.bindings.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Pure helpers for parsing, adding, removing, and generating agent route bindings. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeSortedUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; @@ -327,7 +328,11 @@ export function parseBindingSpecs(params: { } const channel = normalizeBindingChannelId(channelRaw, params.config); if (!channel) { - errors.push(formatUnknownChannelMessage({ channel: channelRaw })); + errors.push( + formatUnknownChannelMessage({ + channel: expectDefined(channelRaw, "agents.bindings channel raw"), + }), + ); continue; } let accountId: string | undefined = accountRaw?.trim(); diff --git a/src/commands/agents.commands.identity.ts b/src/commands/agents.commands.identity.ts index ef5d0de84f2a..a82d3499ff0a 100644 --- a/src/commands/agents.commands.identity.ts +++ b/src/commands/agents.commands.identity.ts @@ -1,13 +1,14 @@ // Implements identity metadata updates for configured agents. import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { identityHasValues, parseIdentityMarkdown } from "../agents/identity-file.js"; import { DEFAULT_IDENTITY_FILENAME } from "../agents/workspace.js"; import { replaceConfigFile } from "../config/config.js"; import { logConfigUpdated } from "../config/logging.js"; -import type { IdentityConfig } from "../config/types.js"; +import type { AgentConfig, IdentityConfig } from "../config/types.js"; import { normalizeAgentId } from "../routing/session-key.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; @@ -163,9 +164,11 @@ export async function agentsSetIdentityCommand( return; } + const resolvedAgentId = expectDefined(agentId, "agent id"); const list = listAgentEntries(cfg); - const index = findAgentEntryIndex(list, agentId); - const base = index >= 0 ? list[index] : { id: agentId }; + const index = findAgentEntryIndex(list, resolvedAgentId); + const base: AgentConfig = + index >= 0 ? expectDefined(list[index], "agent config") : { id: resolvedAgentId }; const nextIdentity: IdentityConfig = { ...base.identity, ...incomingIdentity, @@ -181,7 +184,7 @@ export async function agentsSetIdentityCommand( nextList[index] = nextEntry; } else { const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg)); - if (nextList.length === 0 && agentId !== defaultId) { + if (nextList.length === 0 && resolvedAgentId !== defaultId) { nextList.push({ id: defaultId }); } nextList.push(nextEntry); diff --git a/src/commands/agents.config.ts b/src/commands/agents.config.ts index 5e24f43de388..d0e41d312e46 100644 --- a/src/commands/agents.config.ts +++ b/src/commands/agents.config.ts @@ -126,7 +126,7 @@ export function applyAgentConfig( const name = params.name?.trim(); const list = listAgentEntries(cfg); const index = findAgentEntryIndex(list, agentId); - const base = index >= 0 ? list[index] : { id: agentId }; + const base = (index >= 0 ? list[index] : undefined) ?? { id: agentId }; const mergedIdentity = params.identity ? { ...base.identity, ...params.identity } : undefined; const nextEntry: AgentEntry = { ...base, diff --git a/src/commands/auth-choice-prompt.ts b/src/commands/auth-choice-prompt.ts index 5a088060e6b0..d3d25cc74bf8 100644 --- a/src/commands/auth-choice-prompt.ts +++ b/src/commands/auth-choice-prompt.ts @@ -1,5 +1,6 @@ // Interactive grouped auth-choice prompt used by onboarding and agent setup. import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +import { expectDefined } from "@openclaw/normalization-core"; import type { AuthProfileStore } from "../agents/auth-profiles/types.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -90,7 +91,7 @@ export async function promptAuthChoiceGrouped( } satisfies WizardSelectOption) : undefined; if (group.options.length === 1 && !keepCurrentOption) { - return group.options[0].value; + return expectDefined(group.options[0], "options entry at 0").value; } return (await params.prompter.select({ message: `${group.label} auth method`, diff --git a/src/commands/daemon-install-plan.shared.ts b/src/commands/daemon-install-plan.shared.ts index adb366cc869a..e9231f408810 100644 --- a/src/commands/daemon-install-plan.shared.ts +++ b/src/commands/daemon-install-plan.shared.ts @@ -12,7 +12,11 @@ import type { GatewayDaemonRuntime } from "./daemon-runtime.js"; export function resolveGatewayDevMode(argv: string[] = process.argv): boolean { const entry = argv[1]; const normalizedEntry = entry?.replaceAll("\\", "/"); - return normalizedEntry?.includes("/src/") && normalizedEntry.endsWith(".ts"); + return ( + normalizedEntry !== undefined && + normalizedEntry.includes("/src/") && + normalizedEntry.endsWith(".ts") + ); } /** Resolve dev-mode and Node path inputs for daemon service install planning. */ diff --git a/src/commands/doctor-disk-space.ts b/src/commands/doctor-disk-space.ts index 10ab2e17a9df..e29f1ccf730a 100644 --- a/src/commands/doctor-disk-space.ts +++ b/src/commands/doctor-disk-space.ts @@ -1,6 +1,6 @@ // Doctor contribution for low disk space around the OpenClaw state directory. import os from "node:os"; -import { formatByteSize } from "@openclaw/normalization-core"; +import { expectDefined, formatByteSize } from "@openclaw/normalization-core"; import { note } from "../../packages/terminal-core/src/note.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; @@ -117,7 +117,7 @@ export function collectDiskSpaceHealthFindings( { checkId: DISK_SPACE_CHECK_ID, severity: "warning", - message: message.replace(/^- /, ""), + message: expectDefined(message, "disk-space warning message").replace(/^- /, ""), path: result.stateDir, target: formatBytes(result.availableBytes), requirement: diff --git a/src/commands/doctor-session-sqlite.ts b/src/commands/doctor-session-sqlite.ts index dc29e558a75a..fb74627e5f92 100644 --- a/src/commands/doctor-session-sqlite.ts +++ b/src/commands/doctor-session-sqlite.ts @@ -310,8 +310,12 @@ function resolveFullyCoveredLegacyStorePaths( targetsByStore.set(storePath, [...(targetsByStore.get(storePath) ?? []), target]); } for (const [storePath, storeTargets] of targetsByStore) { + const [firstStoreTarget] = storeTargets; + if (!firstStoreTarget) { + continue; + } const issues: DoctorSessionSqliteIssue[] = []; - const records = readLegacySessionRecords(storeTargets[0], issues); + const records = readLegacySessionRecords(firstStoreTarget, issues); const coversEveryRecord = records.every((record) => storeTargets.some( (target) => @@ -704,7 +708,11 @@ function archiveImportedLegacySessionStores( if (entries.some((entry) => blockingIssueCount(entry.report) > 0)) { continue; } - const archivePath = archiveLegacySessionStore(entries[0].target, entries[0].report, activeRun); + const [firstEntry] = entries; + if (!firstEntry) { + continue; + } + const archivePath = archiveLegacySessionStore(firstEntry.target, firstEntry.report, activeRun); if (!archivePath) { continue; } diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index 91e1305afbb8..2dd29b3d8149 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { asNullableObjectRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; @@ -434,9 +435,9 @@ function parseLinuxMountInfo(rawMountInfo: string): LinuxMountInfoEntry[] { } entries.push({ - mountPoint: decodeMountInfoPath(leftFields[4]), - fsType: rightFields[0], - source: decodeMountInfoPath(rightFields[1]), + mountPoint: decodeMountInfoPath(expectDefined(leftFields[4], "left fields entry at 4")), + fsType: expectDefined(rightFields[0], "right fields entry at 0"), + source: decodeMountInfoPath(expectDefined(rightFields[1], "right fields entry at 1")), }); } return entries; diff --git a/src/commands/doctor/shared/allowfrom-fallback-migration.ts b/src/commands/doctor/shared/allowfrom-fallback-migration.ts index aab7e1f346ac..12067284ca3f 100644 --- a/src/commands/doctor/shared/allowfrom-fallback-migration.ts +++ b/src/commands/doctor/shared/allowfrom-fallback-migration.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Doctor migration from legacy DM allowFrom fallback to explicit groupAllowFrom lists. import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import { resolveChannelDmAllowFrom } from "../../../channels/plugins/dm-access.js"; @@ -87,10 +88,11 @@ function schemaAllowsConfigPath(schema: unknown, path: SchemaPath): boolean { return allOf.every((branch) => schemaAllowsConfigPath(branch, path)); } - const [segment, ...rest] = path; + const segment = expectDefined(path[0], "schema path segment"); + const rest = path.slice(1); const properties = asObjectRecord(node.properties); if (segment !== ACCOUNT_SCHEMA_WILDCARD && properties && Object.hasOwn(properties, segment)) { - return schemaAllowsConfigPath(properties[segment], rest); + return schemaAllowsConfigPath(expectDefined(properties[segment], "schema property"), rest); } const additionalProperties = node.additionalProperties; diff --git a/src/commands/doctor/shared/stale-oauth-profile-shadows.ts b/src/commands/doctor/shared/stale-oauth-profile-shadows.ts index a57c5c118741..707065607bcb 100644 --- a/src/commands/doctor/shared/stale-oauth-profile-shadows.ts +++ b/src/commands/doctor/shared/stale-oauth-profile-shadows.ts @@ -1,6 +1,7 @@ // Doctor cleanup for per-agent OAuth profiles shadowing fresher main-agent credentials. import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { resolveAgentDir, @@ -216,7 +217,9 @@ function removeStaleProfilesFromStore(params: { } function formatProfileList(profileIds: string[]): string { - return profileIds.length === 1 ? profileIds[0] : `${profileIds.length} profiles`; + return profileIds.length === 1 + ? expectDefined(profileIds[0], "profile ids entry at 0") + : `${profileIds.length} profiles`; } async function repairStaleOAuthProfilesForAgent(params: { diff --git a/src/commands/health-format.ts b/src/commands/health-format.ts index 84c149bce9e9..1cc6b4a64c1d 100644 --- a/src/commands/health-format.ts +++ b/src/commands/health-format.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Formatting helpers for `openclaw health` failures and channel summaries. */ import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js"; @@ -173,6 +174,7 @@ export const formatHealthChannelLines = ( : (filteredSummaries ?? (channelSummary.accounts ? Object.values(accountSummaries) : [])); const baseSummary = filteredSummaries && filteredSummaries.length > 0 ? filteredSummaries[0] : channelSummary; + const selectedSummary = expectDefined(baseSummary, "channel health summary"); const botUsernames = listSummaries ? listSummaries .map((account) => { @@ -183,10 +185,11 @@ export const formatHealthChannelLines = ( .filter((value): value is string => Boolean(value)) : []; const statusState = - typeof baseSummary.statusState === "string" ? baseSummary.statusState : null; + typeof selectedSummary.statusState === "string" ? selectedSummary.statusState : null; if (statusState) { if (statusState === "linked") { - const authAgeMs = typeof baseSummary.authAgeMs === "number" ? baseSummary.authAgeMs : null; + const authAgeMs = + typeof selectedSummary.authAgeMs === "number" ? selectedSummary.authAgeMs : null; const authLabel = authAgeMs != null ? ` (auth age ${Math.round(authAgeMs / 60000)}m)` : ""; lines.push(`${label}: ${formatChannelStatusState(statusState)}${authLabel}`); } else { @@ -195,10 +198,11 @@ export const formatHealthChannelLines = ( continue; } - const linked = typeof baseSummary.linked === "boolean" ? baseSummary.linked : null; + const linked = typeof selectedSummary.linked === "boolean" ? selectedSummary.linked : null; if (linked !== null) { if (linked) { - const authAgeMs = typeof baseSummary.authAgeMs === "number" ? baseSummary.authAgeMs : null; + const authAgeMs = + typeof selectedSummary.authAgeMs === "number" ? selectedSummary.authAgeMs : null; const authLabel = authAgeMs != null ? ` (auth age ${Math.round(authAgeMs / 60000)}m)` : ""; lines.push(`${label}: linked${authLabel}`); } else { @@ -207,7 +211,8 @@ export const formatHealthChannelLines = ( continue; } - const configured = typeof baseSummary.configured === "boolean" ? baseSummary.configured : null; + const configured = + typeof selectedSummary.configured === "boolean" ? selectedSummary.configured : null; if (configured === false) { lines.push(`${label}: not configured`); continue; @@ -233,7 +238,9 @@ export const formatHealthChannelLines = ( continue; } - const probeLine = formatProbeLine(baseSummary.probe, { botUsernames }); + const probeLine = formatProbeLine(selectedSummary.probe, { + botUsernames, + }); if (probeLine) { lines.push(`${label}: ${probeLine}`); continue; diff --git a/src/commands/health.ts b/src/commands/health.ts index 547901683d3d..f5325393339c 100644 --- a/src/commands/health.ts +++ b/src/commands/health.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Collects and renders gateway health for channels, agents, plugins, and sessions. */ import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { asNullableRecord } from "@openclaw/normalization-core/record-coerce"; @@ -700,7 +701,11 @@ export async function getHealthSnapshot(params?: { accountSummaries[preferredAccountId] ?? accountSummaries[defaultAccountId] ?? accountSummaries[accountIdsToProbe[0] ?? preferredAccountId]; - const fallbackSummary = defaultSummary ?? accountSummaries[Object.keys(accountSummaries)[0]]; + const fallbackSummary = + defaultSummary ?? + accountSummaries[ + expectDefined(Object.keys(accountSummaries)[0], "object.keys(account summaries) entry at 0") + ]; if (fallbackSummary) { channels[plugin.id] = { ...fallbackSummary, diff --git a/src/commands/migrate/selection.ts b/src/commands/migrate/selection.ts index 6897b30b3740..2272cbf4c53e 100644 --- a/src/commands/migrate/selection.ts +++ b/src/commands/migrate/selection.ts @@ -1,5 +1,6 @@ /** Selection helpers for filtering migration plan items before apply. */ import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; @@ -130,7 +131,9 @@ function resolveSelectedMigrationItemIds(params: { const available = params.items .map(params.formatSelectionLabel) .toSorted((a, b) => a.localeCompare(b)); - const titleKind = params.kindLabel[0].toUpperCase() + params.kindLabel.slice(1); + const titleKind = + expectDefined(params.kindLabel[0], "kind label entry at 0").toUpperCase() + + params.kindLabel.slice(1); const parts: string[] = []; if (unknownRefs.length > 0) { parts.push( diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index 3147b493ec0c..01a6698564a3 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -7,6 +7,7 @@ import { select as clackSelect, text as clackText, } from "@clack/prompts"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveExpiresAtMsFromDurationMs } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString, @@ -999,7 +1000,7 @@ export function resolveLoginProfiles(params: { } const [profile] = params.result.profiles; - return [{ ...profile, profileId: requestedProfileId }]; + return [{ ...expectDefined(profile, "auth profile"), profileId: requestedProfileId }]; } function maybeLogOpenAICodexNativeSearchTip(runtime: RuntimeEnv, providerId: string) { diff --git a/src/commands/models/list.probe.ts b/src/commands/models/list.probe.ts index 43df135584d5..31d9cee8283b 100644 --- a/src/commands/models/list.probe.ts +++ b/src/commands/models/list.probe.ts @@ -1,6 +1,7 @@ /** Auth probe planning and execution helpers for model diagnostics. */ import crypto from "node:crypto"; import fs from "node:fs/promises"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import { resolveAgentDir, @@ -194,7 +195,7 @@ function selectProbeModel(params: { const { provider, candidates, catalog } = params; const direct = candidates.get(provider); if (direct && direct.length > 0) { - return { provider, model: direct[0] }; + return { provider, model: expectDefined(direct[0], "direct entry at 0") }; } const fromCatalog = catalog .map((entry, index) => ({ entry, index })) @@ -601,7 +602,7 @@ async function runTargetsWithConcurrency(params: { if (index >= targets.length) { return; } - const target = targets[index]; + const target = expectDefined(targets[index], "targets entry at index"); onProgress?.({ completed, total: targets.length, diff --git a/src/commands/models/list.provider-catalog.ts b/src/commands/models/list.provider-catalog.ts index 0a923874e398..f18fc43d21b2 100644 --- a/src/commands/models/list.provider-catalog.ts +++ b/src/commands/models/list.provider-catalog.ts @@ -1,6 +1,7 @@ /** Provider plugin catalog loading for model-list output. */ import { createHash } from "node:crypto"; import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +import { expectDefined } from "@openclaw/normalization-core"; import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { loadAuthProfileStoreWithoutExternalProfiles } from "../../agents/auth-profiles/store.js"; import { @@ -47,7 +48,11 @@ function buildProviderCatalogEnvCacheFingerprint(env: NodeJS.ProcessEnv): string const entries = Object.entries(env) .filter((entry): entry is [string, string] => entry[1] !== undefined) .map(([key, value]) => [key, createHash("sha256").update(value).digest("hex")]) - .toSorted(([left], [right]) => left.localeCompare(right)); + .toSorted(([left], [right]) => + expectDefined(left, "list.provider catalog left").localeCompare( + expectDefined(right, "list.provider catalog right"), + ), + ); return createHash("sha256").update(JSON.stringify(entries)).digest("hex"); } diff --git a/src/commands/onboard-inference.ts b/src/commands/onboard-inference.ts index 3f3836f67b8a..d7168fe15bdf 100644 --- a/src/commands/onboard-inference.ts +++ b/src/commands/onboard-inference.ts @@ -2,6 +2,7 @@ import { randomInt } from "node:crypto"; // Inference backend detection shared by onboarding bootstrap and Crestodian setup. import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveAgentConfig, resolveDefaultAgentId } from "../agents/agent-scope-config.js"; import { readClaudeCliCredentialsCached, @@ -120,10 +121,10 @@ function randomizeClaudeCodexTie( if (claudeIndex === -1 || codexIndex === -1 || pickRandomInt(2) === 0) { return; } - [candidates[claudeIndex], candidates[codexIndex]] = [ - candidates[codexIndex], - candidates[claudeIndex], - ]; + const claudeCandidate = candidates[claudeIndex]; + const codexCandidate = candidates[codexIndex]; + candidates[claudeIndex] = expectDefined(codexCandidate, "Codex onboarding candidate"); + candidates[codexIndex] = expectDefined(claudeCandidate, "Claude onboarding candidate"); } // ChatGPT.app is the current desktop owner; keep Codex stable/beta as fallbacks. diff --git a/src/commands/onboarding-plugin-install.ts b/src/commands/onboarding-plugin-install.ts index d6192dae4789..0e41323c5630 100644 --- a/src/commands/onboarding-plugin-install.ts +++ b/src/commands/onboarding-plugin-install.ts @@ -6,6 +6,7 @@ */ import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js"; @@ -464,7 +465,7 @@ async function promptInstallChoice(params: { if (realSources.length === 1) { // Callers that already selected a plugin/channel can skip an extra prompt // when there is only one viable source. - return realSources[0]; + return expectDefined(realSources[0], "real sources entry at 0"); } } diff --git a/src/commands/sessions-cleanup.ts b/src/commands/sessions-cleanup.ts index e493e27ed8bf..90e9772fcddf 100644 --- a/src/commands/sessions-cleanup.ts +++ b/src/commands/sessions-cleanup.ts @@ -304,8 +304,7 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti return; } - for (let i = 0; i < previewResults.length; i += 1) { - const result = previewResults[i]; + for (const [i, result] of previewResults.entries()) { if (i > 0) { runtime.log(""); } diff --git a/src/commands/status.scan-overview.ts b/src/commands/status.scan-overview.ts index 44cbc8820a4f..660c9ad14805 100644 --- a/src/commands/status.scan-overview.ts +++ b/src/commands/status.scan-overview.ts @@ -238,10 +238,11 @@ export async function collectStatusScanOverview(params: { includeRegistryUpdate: params.includeRegistryUpdate, includeLocalStatusRpcFallback: params.includeLocalStatusRpcFallback, gatewayProbeTimeoutMs, - getTailnetHostname: async (runner) => - await loadStatusScanDepsRuntimeModule().then(({ getTailnetHostname }) => + getTailnetHostname: async (runner) => { + return await loadStatusScanDepsRuntimeModule().then(({ getTailnetHostname }) => getTailnetHostname(runner), - ), + ); + }, getUpdateCheckResult: async (updateParams) => await loadStatusUpdateModule().then(({ getUpdateCheckResult }) => getUpdateCheckResult(updateParams), diff --git a/src/commitments/store.ts b/src/commitments/store.ts index d0b98e4f1916..9a21f86c7fde 100644 --- a/src/commitments/store.ts +++ b/src/commitments/store.ts @@ -1,6 +1,7 @@ // Persists commitment records and claims due work for heartbeat processing. import { randomBytes } from "node:crypto"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../config/config.js"; @@ -390,7 +391,10 @@ export async function upsertInferredCommitments(params: { isActiveStatus(commitment.status), ); if (existingIndex >= 0) { - const existing = store.commitments[existingIndex]; + const existing = expectDefined( + store.commitments[existingIndex], + "commitments entry at existing index", + ); store.commitments[existingIndex] = { ...existing, reason: entry.candidate.reason.trim() || existing.reason, diff --git a/src/config/config-paths.ts b/src/config/config-paths.ts index 6037dde34bd8..5fcd5a7bb0a9 100644 --- a/src/config/config-paths.ts +++ b/src/config/config-paths.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; // Resolves and classifies config paths for reads, writes, and metadata. import { isPlainObject } from "../utils.js"; @@ -47,9 +48,12 @@ export function parseConfigPath(raw: string): { /** Sets a value at a validated config path, creating missing plain-object parents. */ export function setConfigValueAtPath(root: PathNode, path: string[], value: unknown): void { + const leafKey = path.at(-1); + if (leafKey === undefined) { + throw new Error("Config path must contain at least one segment"); + } let cursor: PathNode = root; - for (let idx = 0; idx < path.length - 1; idx += 1) { - const key = path[idx]; + for (const key of path.slice(0, -1)) { const existing = Object.hasOwn(cursor, key) ? cursor[key] : undefined; const next: PathNode = isPlainObject(existing) ? existing : {}; if (next !== existing) { @@ -57,15 +61,18 @@ export function setConfigValueAtPath(root: PathNode, path: string[], value: unkn } cursor = next; } - setOwnConfigProperty(cursor, path[path.length - 1], value); + setOwnConfigProperty(cursor, leafKey, value); } /** Removes a value at a config path and prunes empty parent objects created by setters. */ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean { + const leafKey = path.at(-1); + if (leafKey === undefined) { + return false; + } const stack: Array<{ node: PathNode; key: string }> = []; let cursor: PathNode = root; - for (let idx = 0; idx < path.length - 1; idx += 1) { - const key = path[idx]; + for (const key of path.slice(0, -1)) { if (!Object.hasOwn(cursor, key)) { return false; } @@ -76,7 +83,6 @@ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean stack.push({ node: cursor, key }); cursor = next; } - const leafKey = path[path.length - 1]; if (!Object.hasOwn(cursor, leafKey)) { return false; } @@ -84,7 +90,7 @@ export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean // Keep config writes tidy: removing foo.bar should also remove foo when it became empty, while // preserving any parent that still carries sibling config. for (let idx = stack.length - 1; idx >= 0; idx -= 1) { - const { node, key } = stack[idx]; + const { node, key } = expectDefined(stack[idx], "stack entry at idx"); const child = node[key]; if (isPlainObject(child) && Object.keys(child).length === 0) { delete node[key]; diff --git a/src/config/env-preserve.ts b/src/config/env-preserve.ts index 463ca0a923fb..d3778585ab17 100644 --- a/src/config/env-preserve.ts +++ b/src/config/env-preserve.ts @@ -1,5 +1,6 @@ // Normalizes preserved environment-variable config for subprocess launches. import { isDeepStrictEqual } from "node:util"; +import { expectDefined } from "@openclaw/normalization-core"; import { isPlainObject } from "../infra/plain-object.js"; /** @@ -615,7 +616,7 @@ function matchAuthoredEscapedTemplateArrayItems(params: { const sameIndexMatch = incomingMatches.includes(parsedIndex) ? parsedIndex : incomingMatches[0]; - addMatch(parsedIndex, sameIndexMatch); + addMatch(parsedIndex, expectDefined(sameIndexMatch, "env preserve same index match")); continue; } if (incomingMatches.length > 0) { @@ -696,7 +697,7 @@ function tryResolveString(template: string, env: NodeJS.ProcessEnv): string { } } } - chunks.push(template[i]); + chunks.push(template.charAt(i)); } return chunks.join(""); diff --git a/src/config/env-substitution.ts b/src/config/env-substitution.ts index 37f57dd77d1a..942830b7b65c 100644 --- a/src/config/env-substitution.ts +++ b/src/config/env-substitution.ts @@ -101,7 +101,7 @@ function substituteString( const chunks: string[] = []; for (let i = 0; i < value.length; i += 1) { - const char = value[i]; + const char = value.charAt(i); if (char !== "$") { chunks.push(char); continue; diff --git a/src/config/includes.ts b/src/config/includes.ts index ee6aa87eeebf..42da321f066f 100644 --- a/src/config/includes.ts +++ b/src/config/includes.ts @@ -13,6 +13,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { canUseRootFileOpen, openRootFileSync } from "../infra/boundary-file-read.js"; import { resolvePathViaExistingAncestorSync } from "../infra/boundary-path.js"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; @@ -122,7 +123,10 @@ export class ConfigIncludeError extends Error { export class CircularIncludeError extends ConfigIncludeError { constructor(public readonly chain: string[]) { - super(`Circular include detected: ${chain.join(" -> ")}`, chain[chain.length - 1]); + super( + `Circular include detected: ${chain.join(" -> ")}`, + expectDefined(chain[chain.length - 1], "chain entry at chain.length 1"), + ); this.name = "CircularIncludeError"; } } diff --git a/src/config/io.write-prepare.ts b/src/config/io.write-prepare.ts index 39cfbae35ac4..8a8b378d045c 100644 --- a/src/config/io.write-prepare.ts +++ b/src/config/io.write-prepare.ts @@ -1,6 +1,7 @@ // Prepares config writes by diffing current state and preserving metadata. import { isDeepStrictEqual } from "node:util"; import { normalizeConfiguredProviderCatalogModelId } from "@openclaw/model-catalog-core/provider-model-id-normalization"; +import { expectDefined } from "@openclaw/normalization-core"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; import { parseConfigPathArrayIndex } from "../shared/path-array-index.js"; import { isRecord } from "../utils.js"; @@ -215,7 +216,8 @@ function setPathValue(value: unknown, path: string[], nextValue: unknown): unkno if (path.length === 0) { return cloneUnknown(nextValue); } - const [head, ...tail] = path; + const head = expectDefined(path[0], "config path head"); + const tail = path.slice(1); if (Array.isArray(value)) { const index = parseArrayIndexPathSegment(head); if (index === undefined || index >= value.length) { @@ -273,7 +275,8 @@ function setPathValueCreatingParents(value: unknown, path: string[], nextValue: if (path.length === 0) { return cloneUnknown(nextValue); } - const [head, ...tail] = path; + const head = expectDefined(path[0], "config path head"); + const tail = path.slice(1); if (Array.isArray(value) || isNumericPathSegment(head)) { const index = parseArrayIndexPathSegment(head); if (index === undefined) { @@ -294,7 +297,8 @@ function deletePathValue(value: unknown, path: string[]): unknown { if (path.length === 0) { return value; } - const [head, ...tail] = path; + const head = expectDefined(path[0], "config path head"); + const tail = path.slice(1); if (Array.isArray(value)) { const index = parseArrayIndexPathSegment(head); if (index === undefined || index >= value.length || tail.length === 0) { @@ -786,7 +790,8 @@ function hasPathValue(value: unknown, path: readonly string[]): boolean { if (path.length === 0) { return true; } - const [head, ...tail] = path; + const head = expectDefined(path[0], "config path head"); + const tail = path.slice(1); if (Array.isArray(value)) { const index = parseArrayIndexPathSegment(head); if (index === undefined || index >= value.length) { @@ -1019,7 +1024,7 @@ function unsetPathForWriteAt( if (depth >= pathSegments.length) { return { changed: false, value }; } - const segment = pathSegments[depth]; + const segment = expectDefined(pathSegments[depth], "path segments entry at depth"); const isLeaf = depth === pathSegments.length - 1; if (Array.isArray(value)) { diff --git a/src/config/mcp-config.ts b/src/config/mcp-config.ts index 78f9db818ef5..f64a1a007e25 100644 --- a/src/config/mcp-config.ts +++ b/src/config/mcp-config.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Normalizes MCP server config for runtime launch and validation. import { isRecord } from "../utils.js"; import { readSourceConfigSnapshot } from "./io.js"; @@ -157,7 +158,7 @@ async function updateConfiguredMcpServerConfig(params: { const validated = validateConfigObjectWithPlugins(next); if (!validated.ok) { - const issue = validated.issues[0]; + const issue = expectDefined(validated.issues[0], "issues entry at 0"); return { ok: false, path: loaded.path, @@ -276,7 +277,7 @@ export async function setConfiguredMcpServer(params: { const validated = validateConfigObjectWithPlugins(next); if (!validated.ok) { - const issue = validated.issues[0]; + const issue = expectDefined(validated.issues[0], "issues entry at 0"); return { ok: false, path: loaded.path, @@ -334,7 +335,7 @@ export async function unsetConfiguredMcpServer(params: { const validated = validateConfigObjectWithPlugins(next); if (!validated.ok) { - const issue = validated.issues[0]; + const issue = expectDefined(validated.issues[0], "issues entry at 0"); return { ok: false, path: loaded.path, diff --git a/src/config/mutate.ts b/src/config/mutate.ts index c2a842975b6a..6957e16ea2b1 100644 --- a/src/config/mutate.ts +++ b/src/config/mutate.ts @@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import fs from "node:fs/promises"; import path from "node:path"; import { isDeepStrictEqual } from "node:util"; +import { expectDefined } from "@openclaw/normalization-core"; import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue"; import { formatErrorMessage } from "../infra/errors.js"; import { withFileLock } from "../infra/file-lock.js"; @@ -577,7 +578,7 @@ async function tryWriteSingleTopLevelIncludeMutation(params: { return null; } - const key = changedKeys[0]; + const key = expectDefined(changedKeys[0], "changed keys entry at 0"); const includePath = getSingleTopLevelIncludeTarget({ snapshot: params.snapshot, key }); if (!includePath || !isRecord(nextConfig) || !(key in nextConfig)) { return null; diff --git a/src/config/plugin-auto-enable.prefer-over.ts b/src/config/plugin-auto-enable.prefer-over.ts index 9a69760cfdcf..fe74d8f733ac 100644 --- a/src/config/plugin-auto-enable.prefer-over.ts +++ b/src/config/plugin-auto-enable.prefer-over.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; -import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; +import { findChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { isRecord, resolveConfigDir, resolveUserPath } from "../utils.js"; import type { PluginAutoEnableCandidate } from "./plugin-auto-enable.types.js"; @@ -97,7 +97,7 @@ function resolveBuiltInChannelPreferOver(channelId: string): readonly string[] { if (!builtInChannelId) { return []; } - return getChatChannelMeta(builtInChannelId)?.preferOver ?? []; + return findChatChannelMeta(builtInChannelId)?.preferOver ?? []; } function resolvePreferredOverIds( diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 7fcf9cfff14d..58a12e76ac67 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -1,6 +1,7 @@ // Shares plugin auto-enable detection across config and runtime code. import { collectConfiguredModelRefs } from "@openclaw/model-catalog-core/configured-model-refs"; import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js"; import { @@ -11,7 +12,7 @@ import { hasBundledChannelConfiguredState, listBundledChannelIdsWithConfiguredState, } from "../channels/plugins/configured-state.js"; -import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; +import { findChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; @@ -692,7 +693,7 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: { }); if (owningPluginIds?.length === 1) { changes.push({ - pluginId: owningPluginIds[0], + pluginId: expectDefined(owningPluginIds[0], "owning plugin ids entry at 0"), kind: "provider-model-configured", modelRef, }); @@ -1013,7 +1014,7 @@ function resolveChannelAutoEnableDisplayLabel( const builtInChannelId = normalizeChatChannelId(entry.channelId); const plugin = manifestRegistry.plugins.find((record) => record.id === entry.pluginId); return ( - (builtInChannelId ? getChatChannelMeta(builtInChannelId)?.label : undefined) ?? + (builtInChannelId ? findChatChannelMeta(builtInChannelId)?.label : undefined) ?? plugin?.channelConfigs?.[entry.channelId]?.label ?? plugin?.channelCatalogMeta?.label ); diff --git a/src/config/sessions/combined-store-gateway.ts b/src/config/sessions/combined-store-gateway.ts index 56b314b3801b..884f0fd5834e 100644 --- a/src/config/sessions/combined-store-gateway.ts +++ b/src/config/sessions/combined-store-gateway.ts @@ -1,6 +1,7 @@ // Builds the gateway-visible combined session store across agent-specific stores. // Gateway callers need canonical per-agent keys even when stores are split by `{agentId}`. +import { expectDefined } from "@openclaw/normalization-core"; import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { canonicalizeSpawnedByForAgent, @@ -140,7 +141,7 @@ export function loadCombinedSessionStoreForGateway( const storePath = targets.length === 1 - ? targets[0].storePath + ? expectDefined(targets[0], "targets entry at 0").storePath : typeof storeConfig === "string" && storeConfig.trim() ? storeConfig.trim() : "(multiple)"; diff --git a/src/config/sessions/session-accessor.sqlite.ts b/src/config/sessions/session-accessor.sqlite.ts index ca1cb43ba1c6..d9e178169fa9 100644 --- a/src/config/sessions/session-accessor.sqlite.ts +++ b/src/config/sessions/session-accessor.sqlite.ts @@ -1299,6 +1299,9 @@ export async function purgeSqliteDeletedAgentSessionEntries( continue; } const entry = store[sessionKey]; + if (!entry) { + continue; + } entryRemovals.push({ expectedEntry: cloneSessionEntry(entry), sessionKey }); removedEntriesToArchive.push(entry); delete remainingStore[sessionKey]; diff --git a/src/config/sessions/store-cache.ts b/src/config/sessions/store-cache.ts index cfe8821fbec5..67cb3cfa937c 100644 --- a/src/config/sessions/store-cache.ts +++ b/src/config/sessions/store-cache.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Session store caches share parsed stores, immutable snapshots, and serialized JSON. import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; import { createExpiringMapCache, isCacheEnabled, resolveCacheTtlMs } from "../cache-utils.js"; @@ -249,7 +250,9 @@ export function cloneSessionStoreSnapshot( } export function cloneSessionStoreSnapshotEntry(entry: SessionEntry): SessionStoreSnapshotEntry { - return deepFreeze(cloneSessionStoreRecord({ entry }).entry); + return deepFreeze( + expectDefined(cloneSessionStoreRecord({ entry }).entry, "cloned session cache entry"), + ); } function getSessionStoreTtl(): number { diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index 8b15e6b4c0dc..ca19565a71ab 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -1,6 +1,7 @@ // Session store facade coordinates reads, writes, maintenance, delivery metadata, and exports. import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { normalizeOptionalAgentRuntimeId } from "../../agents/agent-runtime-id.js"; import type { MsgContext } from "../../auto-reply/templating.js"; @@ -343,7 +344,7 @@ export type DeletedAgentSessionEntryPurgeParams = { }; function cloneSessionEntry(entry: SessionEntry): SessionEntry { - return cloneSessionStoreRecord({ entry }).entry; + return expectDefined(cloneSessionStoreRecord({ entry }).entry, "cloned session entry"); } function cloneSessionEntries(store: Record): Record { @@ -1292,7 +1293,11 @@ export async function applySessionEntryPatchProjection< ...target, entries: cloneSessionEntryProjectionSnapshot(store).entries, ...(store[target.primaryKey] - ? { existingEntry: cloneSessionEntry(store[target.primaryKey]) } + ? { + existingEntry: cloneSessionEntry( + expectDefined(store[target.primaryKey], "store entry at target.primary key"), + ), + } : {}), }); if (projected.ok) { diff --git a/src/config/types.secrets.ts b/src/config/types.secrets.ts index b723f0c871e7..e5989ce0ce7d 100644 --- a/src/config/types.secrets.ts +++ b/src/config/types.secrets.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Defines secret reference and resolution configuration types. import { isRecord } from "../utils.js"; @@ -97,7 +98,7 @@ export function parseEnvTemplateSecretRef( return { source: "env", provider: provider.trim() || DEFAULT_SECRET_PROVIDER_ALIAS, - id: match[1], + id: expectDefined(match[1], "types.secrets regex capture 1"), }; } diff --git a/src/config/validation.ts b/src/config/validation.ts index 8857c9ef3251..84855baee7a1 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -783,7 +783,9 @@ function filterUnsupportedMutableSecretRefSchemaIssue(params: { if (!unrecognizedKeys.includes(childKey)) { return issue; } - const remainingKeys = unrecognizedKeys.filter((key) => key !== childKey); + const remainingKeys = unrecognizedKeys.filter( + (key): key is string => key !== undefined && key !== childKey, + ); if (remainingKeys.length === 0) { return null; } diff --git a/src/config/version.ts b/src/config/version.ts index e1a3e324439e..580267b53717 100644 --- a/src/config/version.ts +++ b/src/config/version.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Normalizes config version metadata and compatibility comparisons. import { comparePrereleaseIdentifiers, @@ -27,9 +28,9 @@ export function parseOpenClawVersion(raw: string | null | undefined): OpenClawVe const [, major, minor, patch, suffix] = match; const revision = suffix && /^[0-9]+$/.test(suffix) ? Number.parseInt(suffix, 10) : null; return { - major: Number.parseInt(major, 10), - minor: Number.parseInt(minor, 10), - patch: Number.parseInt(patch, 10), + major: Number.parseInt(expectDefined(major, "version major"), 10), + minor: Number.parseInt(expectDefined(minor, "version minor"), 10), + patch: Number.parseInt(expectDefined(patch, "version patch"), 10), revision, prerelease: suffix && revision == null ? suffix.split(".").filter(Boolean) : null, }; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 2ab136c18f9a..23335b27eb57 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -1612,8 +1612,7 @@ export const OpenClawSchema = z if (!Array.isArray(ids)) { continue; } - for (let idx = 0; idx < ids.length; idx += 1) { - const agentId = ids[idx]; + for (const [idx, agentId] of ids.entries()) { if (!agentIds.has(agentId)) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/src/crestodian/setup-inference.ts b/src/crestodian/setup-inference.ts index a50cd04b642a..d4ba78050771 100644 --- a/src/crestodian/setup-inference.ts +++ b/src/crestodian/setup-inference.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { isDeepStrictEqual } from "node:util"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveAgentEffectiveModelPrimary, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { normalizeAuthProfileCredential } from "../agents/auth-profiles/credential-normalize.js"; import { loadPersistedAuthProfileStore } from "../agents/auth-profiles/persisted.js"; @@ -708,7 +709,12 @@ function copySelectedModelMetadata(params: { ...params.target.agents?.defaults, models: { ...params.target.agents?.defaults?.models, - [params.modelRef]: structuredClone(preparedDefaultModels[params.modelRef]), + [params.modelRef]: structuredClone( + expectDefined( + preparedDefaultModels[params.modelRef], + "prepared default models entry at params.model ref", + ), + ), }, }, }; @@ -725,13 +731,18 @@ function copySelectedModelMetadata(params: { return; } const nextAgents = structuredClone(targetAgents); - const targetAgent = nextAgents[targetAgentIndex]; + const targetAgent = expectDefined( + nextAgents[targetAgentIndex], + "next agents entry at target agent index", + ); if (!targetAgent) { return; } targetAgent.models = { ...targetAgent.models, - [params.modelRef]: structuredClone(preparedAgent.models[params.modelRef]), + [params.modelRef]: structuredClone( + expectDefined(preparedAgent.models[params.modelRef], "models entry at params.model ref"), + ), }; params.target.agents = { ...params.target.agents, list: nextAgents }; } @@ -782,13 +793,15 @@ function projectManualInferenceConfig(params: { const providerConfigKey = findSelectedProviderConfigKey(params.preparedConfig, params.providerId); if (providerConfigKey) { + const preparedProvider = params.preparedConfig.models?.providers?.[providerConfigKey]; + if (preparedProvider === undefined) { + throw new Error(`Prepared provider config missing for ${providerConfigKey}`); + } config.models = { ...config.models, providers: { ...config.models?.providers, - [providerConfigKey]: structuredClone( - params.preparedConfig.models!.providers![providerConfigKey], - ), + [providerConfigKey]: structuredClone(preparedProvider), }, }; } diff --git a/src/cron/isolated-agent.test-setup.ts b/src/cron/isolated-agent.test-setup.ts index 75915125719e..59cd73f9bdde 100644 --- a/src/cron/isolated-agent.test-setup.ts +++ b/src/cron/isolated-agent.test-setup.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Isolated agent test setup centralizes common mocks for cron agent tests. import { vi } from "vitest"; import { runEmbeddedAgent } from "../agents/embedded-agent.js"; @@ -46,24 +47,35 @@ function parseTelegramTargetForTest(raw: string): { const match = /^group:([^:]+):topic:(\d+)$/i.exec(trimmed); if (match) { return { - chatId: match[1], - messageThreadId: Number.parseInt(match[2], 10), + chatId: expectDefined(match[1], "isolated agent.test setup regex capture 1"), + messageThreadId: Number.parseInt( + expectDefined(match[2], "isolated agent.test setup regex capture 2"), + 10, + ), chatType: "group", }; } const topicMatch = /^([^:]+):topic:(\d+)$/i.exec(trimmed); if (topicMatch) { return { - chatId: topicMatch[1], - messageThreadId: Number.parseInt(topicMatch[2], 10), - chatType: topicMatch[1].startsWith("-") ? "group" : "direct", + chatId: expectDefined(topicMatch[1], "topic match capture group 1"), + messageThreadId: Number.parseInt( + expectDefined(topicMatch[2], "topic match capture group 2"), + 10, + ), + chatType: expectDefined(topicMatch[1], "topic match capture group 1").startsWith("-") + ? "group" + : "direct", }; } const colonPair = /^([^:]+):(\d+)$/i.exec(trimmed); - if (colonPair && colonPair[1].startsWith("-")) { + if (colonPair && expectDefined(colonPair[1], "colon pair capture group 1").startsWith("-")) { return { - chatId: colonPair[1], - messageThreadId: Number.parseInt(colonPair[2], 10), + chatId: expectDefined(colonPair[1], "colon pair capture group 1"), + messageThreadId: Number.parseInt( + expectDefined(colonPair[2], "colon pair capture group 2"), + 10, + ), chatType: "group", }; } diff --git a/src/cron/isolated-agent/model-preflight.runtime.ts b/src/cron/isolated-agent/model-preflight.runtime.ts index 18c6cb70f653..b4d5c7b36263 100644 --- a/src/cron/isolated-agent/model-preflight.runtime.ts +++ b/src/cron/isolated-agent/model-preflight.runtime.ts @@ -1,5 +1,6 @@ /** Preflights local model-provider endpoints before scheduled cron runner startup. */ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { ModelProviderConfig } from "../../config/types.models.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -75,7 +76,13 @@ function isPrivateIpv4Host(host: string): boolean { return false; } const [a, b] = octets; - return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168); + return ( + a === 10 || + (a === 172 && + expectDefined(b, "model preflight.runtime b") >= 16 && + expectDefined(b, "model preflight.runtime b") <= 31) || + (a === 192 && b === 168) + ); } function isLocalProviderBaseUrl(baseUrl: string): boolean { diff --git a/src/cron/service/jobs.ts b/src/cron/service/jobs.ts index 24bdf3d0363b..9bc4e5a9343f 100644 --- a/src/cron/service/jobs.ts +++ b/src/cron/service/jobs.ts @@ -1,5 +1,6 @@ /** Cron job scheduling, validation, creation, and patch helpers. */ import crypto from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString, normalizeOptionalThreadValue, @@ -76,7 +77,10 @@ export function errorBackoffMs( scheduleMs = DEFAULT_ERROR_BACKOFF_SCHEDULE_MS, ): number { const idx = Math.min(consecutiveErrors - 1, scheduleMs.length - 1); - return scheduleMs[Math.max(0, idx)] ?? DEFAULT_ERROR_BACKOFF_SCHEDULE_MS[0]; + return ( + expectDefined(scheduleMs[Math.max(0, idx)], "schedule ms entry at math.max(0, idx)") ?? + DEFAULT_ERROR_BACKOFF_SCHEDULE_MS[0] + ); } /** Returns the earliest retry timestamp after a failed cron run and its runtime duration. */ diff --git a/src/cron/stagger.ts b/src/cron/stagger.ts index 31c20e574698..720434b2b144 100644 --- a/src/cron/stagger.ts +++ b/src/cron/stagger.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Resolves deterministic cron stagger windows for recurring schedules. */ import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js"; import type { CronSchedule } from "./types.js"; @@ -24,11 +25,18 @@ export function isRecurringTopOfHourCronExpr(expr: string) { const fields = parseCronFields(expr); if (fields.length === 5) { const [minuteField, hourField] = fields; - return minuteField === "0" && hasRecurringWildcardHour(hourField); + return ( + minuteField === "0" && + hasRecurringWildcardHour(expectDefined(hourField, "stagger hour field")) + ); } if (fields.length === 6) { const [secondField, minuteField, hourField] = fields; - return secondField === "0" && minuteField === "0" && hasRecurringWildcardHour(hourField); + return ( + secondField === "0" && + minuteField === "0" && + hasRecurringWildcardHour(expectDefined(hourField, "stagger hour field")) + ); } return false; } diff --git a/src/daemon/arg-split.ts b/src/daemon/arg-split.ts index 91e821756b08..56f62876ab50 100644 --- a/src/daemon/arg-split.ts +++ b/src/daemon/arg-split.ts @@ -20,7 +20,7 @@ export function splitArgsPreservingQuotes( const quoteStart = options?.quoteStart ?? "anywhere"; for (let i = 0; i < value.length; i++) { - const char = value[i]; + const char = value.charAt(i); if (escapeMode === "backslash" && char === "\\") { // POSIX-style service parsers consume any escaped next byte. if (i + 1 < value.length) { diff --git a/src/daemon/launchd-plist.ts b/src/daemon/launchd-plist.ts index 0984e7b31ad1..68dea087f441 100644 --- a/src/daemon/launchd-plist.ts +++ b/src/daemon/launchd-plist.ts @@ -236,24 +236,44 @@ export async function readLaunchAgentProgramArgumentsFromFile( if (!programMatch) { return null; } - const args = Array.from(programMatch[1].matchAll(/([\s\S]*?)<\/string>/gi)).map( - (match) => plistUnescape(match[1] ?? "").trim(), - ); + const programArgumentsXml = programMatch.at(1); + if (programArgumentsXml === undefined) { + return null; + } + const args: string[] = []; + for (const match of programArgumentsXml.matchAll(/([\s\S]*?)<\/string>/gi)) { + const rawArgument = match.at(1); + if (rawArgument === undefined) { + return null; + } + args.push(plistUnescape(rawArgument).trim()); + } const workingDirMatch = plist.match( /WorkingDirectory<\/key>\s*([\s\S]*?)<\/string>/i, ); - const workingDirectory = workingDirMatch ? plistUnescape(workingDirMatch[1] ?? "").trim() : ""; + const workingDirectoryXml = workingDirMatch?.at(1); + const workingDirectory = + workingDirectoryXml === undefined ? "" : plistUnescape(workingDirectoryXml).trim(); const envMatch = plist.match(/EnvironmentVariables<\/key>\s*([\s\S]*?)<\/dict>/i); const inlineEnvironment: Record = {}; if (envMatch) { - for (const pair of envMatch[1].matchAll( + const environmentXml = envMatch.at(1); + if (environmentXml === undefined) { + return null; + } + for (const pair of environmentXml.matchAll( /([\s\S]*?)<\/key>\s*([\s\S]*?)<\/string>/gi, )) { - const key = plistUnescape(pair[1] ?? "").trim(); + const rawKey = pair.at(1); + const rawValue = pair.at(2); + if (rawKey === undefined || rawValue === undefined) { + return null; + } + const key = plistUnescape(rawKey).trim(); if (!key) { continue; } - const value = plistUnescape(pair[2] ?? "").trim(); + const value = plistUnescape(rawValue).trim(); inlineEnvironment[key] = value; } } diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 849447fd5b75..13af56dbd262 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -3,6 +3,7 @@ import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { isGatewayArgv } from "../infra/gateway-process-argv.js"; @@ -526,7 +527,7 @@ async function launchFallbackTaskScript( installedCommand === undefined ? await readScheduledTaskCommand(env) : installedCommand; if (command?.programArguments.length) { const [executable, ...args] = command.programArguments; - const child = spawn(executable, args, { + const child = spawn(expectDefined(executable, "schtasks executable"), args, { cwd: command.workingDirectory || undefined, detached: true, env: { diff --git a/src/daemon/service-audit.ts b/src/daemon/service-audit.ts index 222fb61a4af2..cfd76c1430f5 100644 --- a/src/daemon/service-audit.ts +++ b/src/daemon/service-audit.ts @@ -299,8 +299,7 @@ function readGatewayServiceCommandPortState( if (!programArguments || programArguments.length === 0) { return { kind: "missing" }; } - for (let index = 0; index < programArguments.length; index += 1) { - const arg = programArguments[index]; + for (const [index, arg] of programArguments.entries()) { if (arg === "--port") { return parseGatewayPortArg(programArguments[index + 1]); } diff --git a/src/entry.compile-cache.ts b/src/entry.compile-cache.ts index 6d383dc1c7a5..c2211143cd0e 100644 --- a/src/entry.compile-cache.ts +++ b/src/entry.compile-cache.ts @@ -5,6 +5,7 @@ import { enableCompileCache, getCompileCacheDir } from "node:module"; import os from "node:os"; import path from "node:path"; import process from "node:process"; +import { expectDefined } from "@openclaw/normalization-core"; import { isTerminalInteractiveRespawnArgv } from "./cli/respawn-policy.js"; import { attachChildProcessBridge } from "./process/child-process-bridge.js"; import { @@ -48,8 +49,8 @@ export function isNodeVersionAffectedByCompileCacheDeadlock( if (!match) { return false; } - const major = Number.parseInt(match[1], 10); - const minor = Number.parseInt(match[2], 10); + const major = Number.parseInt(expectDefined(match[1], "compile-cache major version capture"), 10); + const minor = Number.parseInt(expectDefined(match[2], "compile-cache minor version capture"), 10); if (major !== 24) { return false; } diff --git a/src/flows/provider-flow.runtime.ts b/src/flows/provider-flow.runtime.ts index 2d4dd6d664dc..649e7c29dcd4 100644 --- a/src/flows/provider-flow.runtime.ts +++ b/src/flows/provider-flow.runtime.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Provider flow runtime helpers load provider setup behavior behind runtime imports. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -60,8 +61,12 @@ export function resolveProviderModelPickerFlowContributions(params?: { return sortFlowContributionsByLabel( providerWizard.resolveProviderModelPickerEntries(params ?? {}).map((entry) => { const providerId = entry.value.startsWith("provider-plugin:") - ? entry.value.slice("provider-plugin:".length).split(":")[0] + ? expectDefined( + entry.value.slice("provider-plugin:".length).split(":").at(0), + "provider id", + ) : entry.value; + const docsPath = docsByProvider.get(providerId); // Provider-plugin values encode plugin/provider in the option value; docs attach by provider id. return { id: `provider:model-picker:${entry.value}`, @@ -72,9 +77,7 @@ export function resolveProviderModelPickerFlowContributions(params?: { value: entry.value, label: entry.label, ...(entry.hint ? { hint: entry.hint } : {}), - ...(docsByProvider.get(providerId) - ? { docs: { path: docsByProvider.get(providerId)! } } - : {}), + ...(docsPath ? { docs: { path: docsPath } } : {}), }, source: "runtime" as const, }; diff --git a/src/gateway/chat-attachments.ts b/src/gateway/chat-attachments.ts index 45c6d8fd87e6..62fe39436c3d 100644 --- a/src/gateway/chat-attachments.ts +++ b/src/gateway/chat-attachments.ts @@ -3,6 +3,7 @@ import { estimateBase64DecodedBytes } from "@openclaw/media-core/base64"; import { MAX_IMAGE_BYTES } from "@openclaw/media-core/constants"; import { extensionForMime, mimeTypeFromFilePath } from "@openclaw/media-core/mime"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; @@ -295,7 +296,7 @@ function normalizeAttachment( if (opts.stripDataUrlPrefix) { const dataUrlMatch = /^data:[^;]+;base64,(.*)$/.exec(base64); if (dataUrlMatch) { - base64 = dataUrlMatch[1]; + base64 = expectDefined(dataUrlMatch[1], "data url match capture group 1"); } } return { label, mime, base64 }; diff --git a/src/gateway/chat-display-projection.ts b/src/gateway/chat-display-projection.ts index cdc2c071a4f0..3dea7e9de512 100644 --- a/src/gateway/chat-display-projection.ts +++ b/src/gateway/chat-display-projection.ts @@ -1,6 +1,7 @@ // Gateway chat display projection. // Converts raw transcript messages into bounded Control UI/history display records. import { createHash } from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { asFiniteNumber, asPositiveSafeInteger, @@ -1407,13 +1408,17 @@ function mergeTtsSupplementMessages( if (marker && isAssistantTtsSupplementMessage(message)) { let targetIndex = -1; for (let i = merged.length - 1; i >= 0; i--) { - if (ttsSupplementMatchesAssistant(marker, merged[i])) { + const candidate = merged[i]; + if (candidate && ttsSupplementMatchesAssistant(marker, candidate)) { targetIndex = i; break; } } if (targetIndex >= 0) { - merged[targetIndex] = mergeTtsSupplementContent(merged[targetIndex], message); + merged[targetIndex] = mergeTtsSupplementContent( + expectDefined(merged[targetIndex], "merged entry at target index"), + message, + ); changed = true; continue; } @@ -1643,6 +1648,7 @@ function filterVisibleProjectedHistoryMessages( const nextRoleContent = next ? asRoleContentMessage(next) : null; if ( currentRoleContent && + next && nextRoleContent && isHeartbeatUserMessage(currentRoleContent, HEARTBEAT_PROMPT) && isHeartbeatOkResponse(nextRoleContent) && diff --git a/src/gateway/cli-session-history.claude.ts b/src/gateway/cli-session-history.claude.ts index edcb775db6fe..1afd04c7794b 100644 --- a/src/gateway/cli-session-history.claude.ts +++ b/src/gateway/cli-session-history.claude.ts @@ -191,7 +191,10 @@ function isUserToolResultMessage(message: unknown): boolean { function coalesceClaudeCliToolMessages(messages: TranscriptLikeMessage[]): TranscriptLikeMessage[] { const coalesced: TranscriptLikeMessage[] = []; for (let index = 0; index < messages.length; index += 1) { - const current = messages[index]; + const current = messages.at(index); + if (current === undefined) { + break; + } const next = messages[index + 1]; if (!isAssistantToolCallMessage(current) || !isUserToolResultMessage(next)) { coalesced.push(current); diff --git a/src/gateway/exec-approval-manager.ts b/src/gateway/exec-approval-manager.ts index 5417e56ce008..dbd8044d7c2a 100644 --- a/src/gateway/exec-approval-manager.ts +++ b/src/gateway/exec-approval-manager.ts @@ -1,6 +1,7 @@ // Gateway exec approval manager. // Tracks pending operator decisions and short-lived resolved approval records. import { randomUUID } from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveExpiresAtMsFromDurationMs } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { buildApprovalPresentation } from "../infra/approval-presentation.js"; @@ -1086,7 +1087,10 @@ export class ExecApprovalManager { } if (matches.length === 1) { - return { kind: "prefix", id: matches[0] }; + return { + kind: "prefix", + id: expectDefined(matches[0], "matches capture group 0"), + }; } if (matches.length > 1) { return { kind: "ambiguous", ids: matches }; diff --git a/src/gateway/gateway-config-prompts.shared.ts b/src/gateway/gateway-config-prompts.shared.ts index 3326eb2287a4..7338423080e4 100644 --- a/src/gateway/gateway-config-prompts.shared.ts +++ b/src/gateway/gateway-config-prompts.shared.ts @@ -1,6 +1,7 @@ // Gateway setup prompt shared constants. // Provides Tailscale copy and Control UI origin updates for CLI setup flows. import { isIpv6Address, parseCanonicalIpAddress } from "@openclaw/net-policy/ip"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { getTailnetHostname } from "../infra/tailscale.js"; @@ -75,7 +76,9 @@ export async function maybeAddTailnetOriginToControlUiAllowedOrigins(params: { return params.config; } const tsOrigin = await getTailnetHostname(undefined, params.tailscaleBin ?? undefined) - .then((host) => buildTailnetHttpsOrigin(host)) + .then((host) => + buildTailnetHttpsOrigin(expectDefined(host, "gateway config prompts.shared host")), + ) .catch(() => null); if (!tsOrigin) { return params.config; diff --git a/src/gateway/managed-image-attachments.ts b/src/gateway/managed-image-attachments.ts index 5a7b59c55861..ab4484621b59 100644 --- a/src/gateway/managed-image-attachments.ts +++ b/src/gateway/managed-image-attachments.ts @@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveDefaultAgentId } from "../agents/agent-scope-config.js"; import { getRuntimeConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; @@ -575,11 +576,17 @@ function parseManagedOutgoingRoute(value: string) { if (!match) { return null; } - if (!MANAGED_OUTGOING_ATTACHMENT_ID_RE.test(match[2])) { + if ( + !MANAGED_OUTGOING_ATTACHMENT_ID_RE.test( + expectDefined(match[2], "managed image attachments regex capture 2"), + ) + ) { return null; } return { - sessionKey: decodeURIComponent(match[1]), + sessionKey: decodeURIComponent( + expectDefined(match[1], "managed image attachments regex capture 1"), + ), attachmentId: match[2], }; } catch { @@ -607,8 +614,9 @@ function collectManagedOutgoingAttachmentRefs( if (expectedSessionKey && parsed.sessionKey !== expectedSessionKey) { continue; } - refs.set(parsed.attachmentId, { - attachmentId: parsed.attachmentId, + const attachmentId = expectDefined(parsed.attachmentId, "managed image attachment id"); + refs.set(attachmentId, { + attachmentId, sessionKey: parsed.sessionKey, }); } diff --git a/src/gateway/node-command-policy.ts b/src/gateway/node-command-policy.ts index f5f08f147b17..20d66baa5f4e 100644 --- a/src/gateway/node-command-policy.ts +++ b/src/gateway/node-command-policy.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Gateway node command policy. // Computes per-platform allowlists from built-in, plugin, runtime, and config inputs. import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; @@ -393,7 +394,9 @@ function resolveNodeCommandAllowlistInternal( const platformId = normalizePlatformId(node?.platform, node?.deviceFamily); const base = filterDesktopHostCommandDefaults({ platformId, - commands: PLATFORM_DEFAULTS[platformId] ?? PLATFORM_DEFAULTS.unknown, + commands: + expectDefined(PLATFORM_DEFAULTS[platformId], "platform defaults entry at platform id") ?? + PLATFORM_DEFAULTS.unknown, includeDesktopHostCommands: options?.includeDesktopHostCommands, }); const talkCommands = hasTalkSurface(node) ? TALK_PTT_COMMANDS : []; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 76fb5c2f714c..e2e30ec58ba5 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { performance } from "node:perf_hooks"; import { fileURLToPath } from "node:url"; import { isAudioFileName } from "@openclaw/media-core/mime"; +import { expectDefined } from "@openclaw/normalization-core"; import { isFutureDateTimestampMs } from "@openclaw/normalization-core/number-coercion"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import type { FastMode } from "@openclaw/normalization-core/string-coerce"; @@ -1086,7 +1087,12 @@ function replaceAssistantContentTextBlocks( typeof block.text === "string" && transcriptTextIndex < transcriptTextBlocks.length ) { - merged.push(transcriptTextBlocks[transcriptTextIndex++]); + merged.push( + expectDefined( + transcriptTextBlocks[transcriptTextIndex++], + "transcript text blocks entry at transcript text index++", + ), + ); continue; } merged.push(block); @@ -1535,9 +1541,9 @@ async function prestageMediaPathOffloads(params: { } const stagingCtx: MsgContext = { - MediaPath: refsToStage[0].path, + MediaPath: expectDefined(refsToStage[0], "refs to stage entry at 0").path, MediaPaths: refsToStage.map((ref) => ref.path), - MediaType: refsToStage[0].mimeType, + MediaType: expectDefined(refsToStage[0], "refs to stage entry at 0").mimeType, MediaTypes: refsToStage.map((ref) => ref.mimeType), }; let stageResult: StageSandboxMediaResult; @@ -5177,7 +5183,10 @@ export const chatHandlers: GatewayRequestHandlers = { runId: clientRunId, sessionKey, ...(sessionKey === "global" && agentId ? { agentId } : {}), - question: btwReplies[0].btw.question.trim(), + question: expectDefined( + btwReplies[0], + "btw replies entry at 0", + ).btw.question.trim(), text: btwText, isError: btwReplies.some((payload) => payload.isError), ts: Date.now(), diff --git a/src/gateway/server-methods/doctor.ts b/src/gateway/server-methods/doctor.ts index 252febbaeb42..ea7dd6f5d21c 100644 --- a/src/gateway/server-methods/doctor.ts +++ b/src/gateway/server-methods/doctor.ts @@ -2,6 +2,7 @@ // cron state, and REM harness previews for operator diagnostics. import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -263,7 +264,11 @@ function groundedMarkdownToDiaryLines(markdown: string): string[] { return markdown .split("\n") .map((line) => line.replace(/^##\s+/, "").trimEnd()) - .filter((line, index, lines) => line.length > 0 || (index > 0 && lines[index - 1]?.length > 0)); + .filter( + (line, index, lines) => + line.length > 0 || + (index > 0 && expectDefined(lines[index - 1], "lines entry at index 1")?.length > 0), + ); } async function listWorkspaceDailyFiles(memoryDir: string): Promise { @@ -435,7 +440,7 @@ function trimDreamingEntries( // Keep the public status payload bounded while preserving the comparator's best 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 entry at index")) < 0) { insertAt = index; break; } diff --git a/src/gateway/server-methods/models-auth-status.ts b/src/gateway/server-methods/models-auth-status.ts index 41dfcde46876..14e2c47d5031 100644 --- a/src/gateway/server-methods/models-auth-status.ts +++ b/src/gateway/server-methods/models-auth-status.ts @@ -29,7 +29,7 @@ import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js" import type { OpenClawConfig } from "../../config/config.js"; import { isSecretRef } from "../../config/types.secrets.js"; import { loadProviderUsageSummary } from "../../infra/provider-usage.load.js"; -import { PROVIDER_LABELS, resolveUsageProviderId } from "../../infra/provider-usage.shared.js"; +import { providerUsageLabel, resolveUsageProviderId } from "../../infra/provider-usage.shared.js"; import type { ProviderUsageBilling, ProviderUsageSnapshot, @@ -204,8 +204,9 @@ function buildExpiry( function providerDisplayName(provider: string): string { const usageId = resolveUsageProviderId(provider); - if (usageId && PROVIDER_LABELS[usageId]) { - return PROVIDER_LABELS[usageId]; + const usageLabel = usageId ? providerUsageLabel(usageId) : undefined; + if (usageLabel) { + return usageLabel; } return provider; } diff --git a/src/gateway/server-methods/sessions-diff.ts b/src/gateway/server-methods/sessions-diff.ts index 8baa956dd314..8a31efb3abb3 100644 --- a/src/gateway/server-methods/sessions-diff.ts +++ b/src/gateway/server-methods/sessions-diff.ts @@ -3,6 +3,7 @@ // Control UI diff panel can render without shelling out client-side. import fs from "node:fs/promises"; import nodePath from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { validateSessionsDiffParams, @@ -112,20 +113,20 @@ export function parseNumstatZ(text: string): Map { function chunkPath(chunk: string): string | null { const newFile = /^\+\+\+ b\/(.+)$/m.exec(chunk); if (newFile) { - return newFile[1]; + return expectDefined(newFile[1], "new file capture group 1"); } // Deleted files have `+++ /dev/null`; key the chunk by the old path. const oldFile = /^--- a\/(.+)$/m.exec(chunk); if (oldFile) { - return oldFile[1]; + return expectDefined(oldFile[1], "old file capture group 1"); } // Pure renames and binary chunks have neither marker line. const renameTo = /^rename to (.+)$/m.exec(chunk); if (renameTo) { - return renameTo[1]; + return expectDefined(renameTo[1], "rename to capture group 1"); } const header = /^diff --git a\/.+ b\/(.+)$/m.exec(chunk); - return header ? header[1] : null; + return header ? expectDefined(header[1], "header capture group 1") : null; } /** Splits a multi-file `git diff --patch` into per-file chunks keyed by path. */ diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 40276f9baa7d..2a58986e1145 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString, readStringValue, @@ -412,7 +413,10 @@ async function createAgentMainSessionForSend(params: { let createResult: | { ok: boolean; payload?: { key?: string }; error?: ReturnType } | undefined; - await sessionsHandlers["sessions.create"]({ + await expectDefined( + sessionsHandlers["sessions.create"], + "sessions.create handler", + )({ req: params.req, params: { key: params.canonicalKey, @@ -592,7 +596,10 @@ async function interruptSessionRunIfActive(params: { canonicalKey: params.canonicalKey, }); - await chatHandlers["chat.abort"]({ + await expectDefined( + chatHandlers["chat.abort"], + "chat.abort handler", + )({ req: params.req, params: { sessionKey: abortSessionKey, @@ -695,7 +702,10 @@ async function handleSessionSend(params: { : undefined; const idempotencyKey = explicitIdempotencyKey ?? randomUUID(); const dispatchChatSend = async (respond: RespondFn) => { - await chatHandlers["chat.send"]({ + await expectDefined( + chatHandlers["chat.send"], + "chat.send handler", + )({ req: params.req, params: { sessionKey: canonicalKey, @@ -1466,7 +1476,10 @@ export const sessionsHandlers: GatewayRequestHandlers = { sessionKey: key, storePath, })) + 1; - await chatHandlers["chat.send"]({ + await expectDefined( + chatHandlers["chat.send"], + "chat.send handler", + )({ req, params: { sessionKey: key, @@ -2084,7 +2097,10 @@ export const sessionsHandlers: GatewayRequestHandlers = { } } let abortedRunId: string | null = null; - await chatHandlers["chat.abort"]({ + await expectDefined( + chatHandlers["chat.abort"], + "chat.abort handler", + )({ req, params: { sessionKey: abortSessionKey, @@ -2590,7 +2606,11 @@ export const sessionsHandlers: GatewayRequestHandlers = { } | undefined; const abortSessionKey = target.canonicalKey ?? key; - await chatHandlers["chat.abort"]({ + const chatAbort = chatHandlers["chat.abort"]; + if (!chatAbort) { + throw new Error("chat.abort handler is not registered"); + } + await chatAbort({ req, params: { sessionKey: abortSessionKey, diff --git a/src/gateway/server-methods/usage.ts b/src/gateway/server-methods/usage.ts index 71d28644417f..6110824b310a 100644 --- a/src/gateway/server-methods/usage.ts +++ b/src/gateway/server-methods/usage.ts @@ -1,6 +1,7 @@ // Usage gateway methods aggregate provider and session cost/token metrics from // caches, logs, session stores, and discovered transcript files. import fs from "node:fs"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { ErrorCodes, @@ -1424,8 +1425,11 @@ export const usageHandlers: GatewayRequestHandlers = { if (!summary) { continue; } - const session = agentSessions[index]; - const merged = mergedEntries[session.entryIndex]; + const session = expectDefined(agentSessions[index], "agent sessions entry at index"); + const merged = expectDefined( + mergedEntries[session.entryIndex], + "merged entries entry at session.entry index", + ); const usage = usageByEntryIndex[session.entryIndex] ?? createEmptySessionCostSummary(); usage.sessionId = merged.sessionId; usage.sessionFile = merged.sessionFile; @@ -1590,7 +1594,7 @@ export const usageHandlers: GatewayRequestHandlers = { providerOverride: merged.storeEntry?.providerOverride, modelProvider: merged.storeEntry?.modelProvider, model: merged.storeEntry?.model, - usage, + usage: expectDefined(usage, "session usage summary"), contextWeight: includeContextWeight ? (merged.storeEntry?.systemPromptReport ?? null) : undefined, diff --git a/src/gateway/server-methods/web.ts b/src/gateway/server-methods/web.ts index 2f4a618d4160..e14eace4a0ac 100644 --- a/src/gateway/server-methods/web.ts +++ b/src/gateway/server-methods/web.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Web login methods delegate QR-login start/wait requests to the active channel // plugin that owns web login gateway methods. import { @@ -52,11 +53,11 @@ function resolveMissingWebLoginPluginHint(context: GatewayRequestContext): strin return null; } if (hints.length === 1) { - return hints[0].repairHint; + return expectDefined(hints[0], "hints entry at 0").repairHint; } const labels = [...new Set(hints.map((hint) => hint.label))]; const installCommands = [...new Set(hints.map((hint) => hint.installCommand))]; - const doctorFixCommand = hints[0].doctorFixCommand; + const doctorFixCommand = expectDefined(hints[0], "hints entry at 0").doctorFixCommand; return `Configured official external channel plugins are missing for ${labels.join(", ")}. Install them with: ${installCommands.join("; ")}, or run: ${doctorFixCommand}.`; } diff --git a/src/gateway/session-utils.fs.ts b/src/gateway/session-utils.fs.ts index c10a780f1853..707bdd6e0210 100644 --- a/src/gateway/session-utils.fs.ts +++ b/src/gateway/session-utils.fs.ts @@ -2,6 +2,7 @@ // Parses transcript JSONL files for messages, previews, counts, and usage metadata. import fs from "node:fs"; import { StringDecoder } from "node:string_decoder"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveIntegerOption, resolveNonNegativeIntegerOption, @@ -329,7 +330,7 @@ function isOversizedTranscriptLine(line: string): boolean { function isJsonObjectFieldToken(source: string, tokenIndex: number): boolean { for (let index = tokenIndex - 1; index >= 0; index--) { - const char = source[index]; + const char = source.charAt(index); if (/\s/.test(char)) { continue; } @@ -972,7 +973,7 @@ export function capArrayByJsonBytes( let bytes = 2 + parts.reduce((a, b) => a + b, 0) + (items.length - 1); let start = 0; while (bytes > maxBytes && start < items.length - 1) { - bytes -= parts[start] + 1; + bytes -= expectDefined(parts[start], "parts entry at start") + 1; start += 1; } const next = start > 0 ? items.slice(start) : items; @@ -2076,7 +2077,7 @@ function readRecentMessagesFromTranscript( const collected: TranscriptPreviewMessage[] = []; for (let i = tailLines.length - 1; i >= 0; i--) { - const line = tailLines[i]; + const line = expectDefined(tailLines[i], "tail lines entry at i"); try { const parsed = JSON.parse(line); const msg = parsed?.message as TranscriptPreviewMessage | undefined; diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index cfd8e22041e7..364eb0f33bf4 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Gateway session listing and projection helpers. // Normalizes persisted session stores into UI/RPC rows without mutating state. import { @@ -2881,7 +2882,7 @@ export async function listSessionsFromStoreAsync(params: { const sessions: GatewaySessionRow[] = []; for (let i = 0; i < entries.length; i++) { - const [key, entry] = entries[i]; + const [key, entry] = expectDefined(entries[i], "entries entry at i"); const includeTranscriptFields = i < sessionListTranscriptFieldRows; const rowAgentId = key === "global" && typeof opts.agentId === "string" diff --git a/src/gateway/sessions-resolve.ts b/src/gateway/sessions-resolve.ts index 4c94034d912a..2f7f099e3654 100644 --- a/src/gateway/sessions-resolve.ts +++ b/src/gateway/sessions-resolve.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Gateway sessions.resolve implementation helper. // Resolves key/sessionId/label selectors into one canonical session key. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; @@ -261,10 +262,13 @@ export async function resolveSessionKeyFromResolveParams(params: { }; } - const labelKey = list.sessions[0].key; + const labelKey = expectDefined(list.sessions[0], "sessions entry at 0").key; const agentCheckLabel = validateSessionAgentExists(cfg, labelKey, store[labelKey]); if (agentCheckLabel) { return agentCheckLabel; } - return { ok: true, key: list.sessions[0].key }; + return { + ok: true, + key: expectDefined(list.sessions[0], "sessions entry at 0").key, + }; } diff --git a/src/gateway/talk-agent-consult.ts b/src/gateway/talk-agent-consult.ts index 2dfe1da63e66..7f2a0bd8007c 100644 --- a/src/gateway/talk-agent-consult.ts +++ b/src/gateway/talk-agent-consult.ts @@ -1,6 +1,7 @@ // Gateway Talk realtime agent-consult bridge. // Starts chat.send runs that answer realtime Talk tool calls. import { randomUUID } from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { ErrorCodes, errorShape, @@ -80,7 +81,10 @@ export async function startTalkRealtimeAgentConsult(params: { { ok: true; result: unknown } | { ok: false; error: ErrorShape } | undefined >((resolve) => { let acknowledged = false; - const chatSendResult = chatHandlers["chat.send"]({ + const chatSendResult = expectDefined( + chatHandlers["chat.send"], + "chat.send handler", + )({ req: { type: "req", id: `${params.requestId}:talk-tool-call`, @@ -159,9 +163,9 @@ export async function startTalkRealtimeAgentConsult(params: { relaySessionId: params.relaySessionId, connId: params.connId, sessionKey: params.sessionKey, - runId, + runId: expectDefined(runId, "talk agent run id"), callId: params.callId, }); } - return { ok: true, runId, idempotencyKey }; + return { ok: true, runId: expectDefined(runId, "talk agent run id"), idempotencyKey }; } diff --git a/src/gateway/talk.test-helpers.ts b/src/gateway/talk.test-helpers.ts index 0b11d081f5b1..c1e2c6c77321 100644 --- a/src/gateway/talk.test-helpers.ts +++ b/src/gateway/talk.test-helpers.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** * Direct talk method invocation helpers for gateway speech tests. */ @@ -24,7 +25,10 @@ export async function invokeTalkSpeakDirect(params: Record) { error?: { code?: string; message?: string; details?: unknown }; } | undefined; - await talkHandlers["talk.speak"]({ + await expectDefined( + talkHandlers["talk.speak"], + "talk.speak handler", + )({ req: { type: "req", id: "test", method: "talk.speak", params }, params, client: null, diff --git a/src/gateway/test/server-sessions.test-helpers.ts b/src/gateway/test/server-sessions.test-helpers.ts index 9af2a2c8089f..1e82e70e839b 100644 --- a/src/gateway/test/server-sessions.test-helpers.ts +++ b/src/gateway/test/server-sessions.test-helpers.ts @@ -5,6 +5,7 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import type { AssistantMessage, UserMessage } from "openclaw/plugin-sdk/llm"; import { afterAll, beforeAll, beforeEach, expect, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; @@ -689,7 +690,10 @@ export async function directSessionReq( let result: | { ok: boolean; payload?: TPayload; error?: { code?: string; message?: string } } | undefined; - await sessionsHandlers[method]({ + await expectDefined( + sessionsHandlers[method], + "sessions handlers entry at method", + )({ req: {} as never, params, respond: (ok, payload, error) => { diff --git a/src/gateway/worker-environments/service.ts b/src/gateway/worker-environments/service.ts index 2944f8cc44ac..a342037c43da 100644 --- a/src/gateway/worker-environments/service.ts +++ b/src/gateway/worker-environments/service.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { type WorkerAdmissionHandshake, @@ -706,7 +707,10 @@ export function createWorkerEnvironmentService(options: WorkerEnvironmentService if (!profiles || !Object.hasOwn(profiles, normalizedProfileId)) { throw serviceError("profile_not_found", `Unknown worker profile: ${normalizedProfileId}`); } - const profile = profiles[normalizedProfileId]; + const profile = expectDefined( + profiles[normalizedProfileId], + "profiles entry at normalized profile id", + ); const provider = providerFor(profile.provider); const settings = requireWorkerProfile(profile.settings ?? {}); const intent = store.createIntent({ diff --git a/src/hooks/bundled/session-memory/transcript.ts b/src/hooks/bundled/session-memory/transcript.ts index f7f98621f88e..fe5850e52485 100644 --- a/src/hooks/bundled/session-memory/transcript.ts +++ b/src/hooks/bundled/session-memory/transcript.ts @@ -176,7 +176,11 @@ export async function getRecentSessionContentWithResetFallback( return primary; } - const latestResetPath = path.join(dir, resetCandidates[resetCandidates.length - 1]); + const latestReset = resetCandidates.at(-1); + if (latestReset === undefined) { + return primary; + } + const latestResetPath = path.join(dir, latestReset); return (await getRecentSessionContent(latestResetPath, messageCount)) || primary; } catch { return primary; @@ -219,8 +223,9 @@ export async function findPreviousSessionFile(params: { .filter((name) => name.startsWith(`${canonicalFile}.reset.`)) .toSorted() .toReversed(); - if (canonicalResetVariants.length > 0) { - return path.join(params.sessionsDir, canonicalResetVariants[0]); + const [canonicalResetVariant] = canonicalResetVariants; + if (canonicalResetVariant !== undefined) { + return path.join(params.sessionsDir, canonicalResetVariant); } const topicVariants = files @@ -232,8 +237,9 @@ export async function findPreviousSessionFile(params: { ) .toSorted() .toReversed(); - if (topicVariants.length > 0) { - return path.join(params.sessionsDir, topicVariants[0]); + const [topicVariant] = topicVariants; + if (topicVariant !== undefined) { + return path.join(params.sessionsDir, topicVariant); } const topicResetVariants = files @@ -242,8 +248,9 @@ export async function findPreviousSessionFile(params: { ) .toSorted() .toReversed(); - if (topicResetVariants.length > 0) { - return path.join(params.sessionsDir, topicResetVariants[0]); + const [topicResetVariant] = topicResetVariants; + if (topicResetVariant !== undefined) { + return path.join(params.sessionsDir, topicResetVariant); } } @@ -255,16 +262,18 @@ export async function findPreviousSessionFile(params: { .filter((name) => name.endsWith(".jsonl") && !name.includes(".reset.")) .toSorted() .toReversed(); - if (nonResetJsonl.length > 0) { - return path.join(params.sessionsDir, nonResetJsonl[0]); + const [nonResetFile] = nonResetJsonl; + if (nonResetFile !== undefined) { + return path.join(params.sessionsDir, nonResetFile); } const resetJsonl = files .filter((name) => name.includes(".jsonl.reset.")) .toSorted() .toReversed(); - if (resetJsonl.length > 0) { - return path.join(params.sessionsDir, resetJsonl[0]); + const [resetFile] = resetJsonl; + if (resetFile !== undefined) { + return path.join(params.sessionsDir, resetFile); } } catch { // Ignore directory read errors. diff --git a/src/hooks/installs.ts b/src/hooks/installs.ts index 9fbc926dadd4..a95dda3f9ba5 100644 --- a/src/hooks/installs.ts +++ b/src/hooks/installs.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Hook install record helpers read and write installed hook metadata. import type { HookInstallRecord } from "../config/types.hooks.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -25,7 +26,7 @@ export function recordHookInstall(cfg: OpenClawConfig, update: HookInstallUpdate ...cfg.hooks?.internal, installs: { ...installs, - [hookId]: installs[hookId], + [hookId]: expectDefined(installs[hookId], "installs entry at hook id"), }, }, }, diff --git a/src/infra/approval-native-route-coordinator.ts b/src/infra/approval-native-route-coordinator.ts index e2b6c58733f3..a6ba38c4fd5f 100644 --- a/src/infra/approval-native-route-coordinator.ts +++ b/src/infra/approval-native-route-coordinator.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Coordinates native approval delivery routing and notices. import { normalizeLowercaseStringOrEmpty, @@ -197,7 +198,7 @@ function resolveApprovalRouteNotice(params: { return { requestGateway: params.reports.find((report) => activeApprovalRouteRuntimes.has(report.runtimeId)) - ?.requestGateway ?? params.reports[0].requestGateway, + ?.requestGateway ?? expectDefined(params.reports[0], "reports entry at 0").requestGateway, target, text: resolveApprovalDeliveryFailedNoticeText({ approvalId: params.request.id, diff --git a/src/infra/bonjour-discovery.ts b/src/infra/bonjour-discovery.ts index b92abc92dbc3..4eec464d4fc4 100644 --- a/src/infra/bonjour-discovery.ts +++ b/src/infra/bonjour-discovery.ts @@ -1,4 +1,5 @@ // Discovers gateways over Bonjour and normalizes service records. +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries, @@ -526,7 +527,9 @@ function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] { } if (trimmed.startsWith("txt =")) { - const tokens = Array.from(trimmed.matchAll(/"([^"]*)"/g), (m) => m[1]); + const tokens = Array.from(trimmed.matchAll(/"([^"]*)"/g), (match) => + expectDefined(match.at(1), "Bonjour TXT token"), + ); const txt = parseTxtTokens(tokens); current.txt = Object.keys(txt).length ? txt : undefined; if (txt.displayName) { diff --git a/src/infra/command-explainer/extract.ts b/src/infra/command-explainer/extract.ts index 5e9c8eb11876..8d03a7c27c16 100644 --- a/src/infra/command-explainer/extract.ts +++ b/src/infra/command-explainer/extract.ts @@ -421,7 +421,7 @@ function hasUnescapedDynamicPattern(text: string): boolean { function decodeUnquotedShellTextWithOffsets(text: string): DecodedShellText { const decoded: DecodedShellText = { value: "", sourceOffsets: [0] }; for (let index = 0; index < text.length; index += 1) { - const ch = text[index]; + const ch = text.charAt(index); const next = text[index + 1]; if (ch === "\\" && next !== undefined) { if (next === "\r" && text[index + 2] === "\n") { @@ -453,7 +453,7 @@ function decodeDoubleQuotedTextWithOffsets(text: string): DecodedShellText { const body = hasQuotes ? text.slice(1, -1) : text; const decoded: DecodedShellText = { value: "", sourceOffsets: [bodyStart] }; for (let index = 0; index < body.length; index += 1) { - const ch = body[index]; + const ch = body.charAt(index); const next = body[index + 1]; const sourceOffset = bodyStart + index; if (ch === "\\" && next !== undefined) { @@ -503,7 +503,7 @@ function decodeAnsiCStringWithOffsets(text: string): DecodedShellText { const body = hasQuotes ? text.slice(2, -1) : text; const decoded: DecodedShellText = { value: "", sourceOffsets: [bodyStart] }; for (let index = 0; index < body.length; index += 1) { - const ch = body[index]; + const ch = body.charAt(index); const sourceOffset = bodyStart + index; if (ch !== "\\") { appendDecodedText(decoded, ch, sourceOffset + 1); diff --git a/src/infra/device-pairing.ts b/src/infra/device-pairing.ts index 7af2a05ffde9..1f3b2488721f 100644 --- a/src/infra/device-pairing.ts +++ b/src/infra/device-pairing.ts @@ -1,5 +1,6 @@ // Manages device pairing requests, approvals, and token issuance. import { randomUUID } from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeUniqueSingleOrTrimmedStringList } from "@openclaw/normalization-core/string-normalization"; import { normalizeDeviceAuthScopes } from "../shared/device-auth.js"; import { @@ -677,9 +678,15 @@ function reconcilePendingPairingRequests< }): PendingPairingRequestResult { if ( params.existing.length === 1 && - params.canRefreshSingle(params.existing[0], params.incoming) + params.canRefreshSingle( + expectDefined(params.existing[0], "existing entry at 0"), + params.incoming, + ) ) { - const refreshed = params.refreshSingle(params.existing[0], params.incoming); + const refreshed = params.refreshSingle( + expectDefined(params.existing[0], "existing entry at 0"), + params.incoming, + ); params.pendingById[refreshed.requestId] = refreshed; params.persist(); return { status: "pending", request: refreshed, created: false }; diff --git a/src/infra/diagnostic-trace-context.ts b/src/infra/diagnostic-trace-context.ts index c2a607689ed1..d725e6d6a4e6 100644 --- a/src/infra/diagnostic-trace-context.ts +++ b/src/infra/diagnostic-trace-context.ts @@ -1,6 +1,7 @@ // Creates and propagates lightweight W3C diagnostic trace contexts. import { AsyncLocalStorage } from "node:async_hooks"; import { randomBytes } from "node:crypto"; +import { expectDefined } from "@openclaw/normalization-core"; const TRACEPARENT_VERSION = "00"; const DEFAULT_TRACE_FLAGS = "01"; @@ -141,7 +142,7 @@ export function parseDiagnosticTraceparent( } const [version, traceId, spanId, traceFlags] = parts; if ( - !TRACEPARENT_VERSION_RE.test(version) || + !TRACEPARENT_VERSION_RE.test(expectDefined(version, "diagnostic trace context version")) || version === "ff" || (version === TRACEPARENT_VERSION && parts.length !== 4) ) { diff --git a/src/infra/exec-allowlist-pattern.ts b/src/infra/exec-allowlist-pattern.ts index 991ca9809482..43047734d774 100644 --- a/src/infra/exec-allowlist-pattern.ts +++ b/src/infra/exec-allowlist-pattern.ts @@ -59,7 +59,7 @@ function compileGlobRegex(pattern: string): RegExp { let regex = "^"; let i = 0; while (i < pattern.length) { - const ch = pattern[i]; + const ch = pattern.charAt(i); if (ch === "*") { const next = pattern[i + 1]; if (next === "*") { diff --git a/src/infra/exec-approval-command-display.ts b/src/infra/exec-approval-command-display.ts index b38f3091235d..b10742578e49 100644 --- a/src/infra/exec-approval-command-display.ts +++ b/src/infra/exec-approval-command-display.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; // Sanitizes command text before it is displayed in approval prompts. import { @@ -119,7 +120,10 @@ function sanitizeExecApprovalDisplayTextInternal( const strippedMask = computeSensitiveRedactionBitmap(stripped, redaction); let bypassDetected = false; for (let i = 0; i < strippedMask.length; i++) { - if (strippedMask[i] && !rawMask[strippedToOrig[i]]) { + if ( + strippedMask[i] && + !rawMask[expectDefined(strippedToOrig[i], "stripped to orig entry at i")] + ) { bypassDetected = true; break; } @@ -137,7 +141,7 @@ function sanitizeExecApprovalDisplayTextInternal( const unionMask = rawMask.slice(); for (let i = 0; i < strippedMask.length; i++) { if (strippedMask[i]) { - unionMask[strippedToOrig[i]] = true; + unionMask[expectDefined(strippedToOrig[i], "stripped to orig entry at i")] = true; } } let out = ""; diff --git a/src/infra/exec-approval-reply.ts b/src/infra/exec-approval-reply.ts index e836fc63d237..f9bfb3d9949f 100644 --- a/src/infra/exec-approval-reply.ts +++ b/src/infra/exec-approval-reply.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -354,7 +355,7 @@ export function parseExecApprovalCommandText( } const rawDecision = normalizeOptionalLowercaseString(match[2]) ?? ""; return { - approvalId: match[1], + approvalId: expectDefined(match[1], "exec approval reply regex capture 1"), decision: rawDecision === "always" ? "allow-always" : (rawDecision as ExecApprovalReplyDecision), }; diff --git a/src/infra/exec-control-command-guard.ts b/src/infra/exec-control-command-guard.ts index b5c8471b52e0..2038de736e7f 100644 --- a/src/infra/exec-control-command-guard.ts +++ b/src/infra/exec-control-command-guard.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { splitShellArgs } from "../utils/shell-argv.js"; @@ -20,7 +21,7 @@ function parseExecApprovalShellCommand(raw: string): ParsedExecApprovalCommand | return null; } return { - approvalId: match[1], + approvalId: expectDefined(match[1], "exec control command guard regex capture 1"), decision: normalizeLowercaseStringOrEmpty(match[2]) === "always" ? "allow-always" @@ -57,7 +58,7 @@ function stripOpenClawPackageRunner(argv: string[]): string[] { if (commandName === "npx" || commandName === "bunx") { let idx = 1; while (idx < argv.length) { - const token = argv[idx]; + const token = expectDefined(argv[idx], "argv entry at idx"); if (token === "--") { idx += 1; break; diff --git a/src/infra/exec-safe-bin-policy-profiles.ts b/src/infra/exec-safe-bin-policy-profiles.ts index 6cfcd34d3a24..abf164f162bc 100644 --- a/src/infra/exec-safe-bin-policy-profiles.ts +++ b/src/infra/exec-safe-bin-policy-profiles.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Defines safe-bin policy profile fixtures and metadata. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization"; @@ -392,7 +393,12 @@ export function renderSafeBinDeniedFlagsDocBullets( const deniedByBin = resolveSafeBinDeniedFlags(fixtures); const bins = Object.keys(deniedByBin).toSorted(); return bins - .map((bin) => `- \`${bin}\`: ${deniedByBin[bin].map((flag) => `\`${flag}\``).join(", ")}`) + .map( + (bin) => + `- \`${bin}\`: ${expectDefined(deniedByBin[bin], "denied by bin entry at bin") + .map((flag) => `\`${flag}\``) + .join(", ")}`, + ) .join("\n"); } diff --git a/src/infra/exec-safe-bin-policy-validator.ts b/src/infra/exec-safe-bin-policy-validator.ts index 51ca1bedaa14..2ad93302c34f 100644 --- a/src/infra/exec-safe-bin-policy-validator.ts +++ b/src/infra/exec-safe-bin-policy-validator.ts @@ -104,8 +104,7 @@ function consumeShortOptionClusterToken(params: { allowedBooleanFlags: ReadonlySet; deniedFlags: ReadonlySet; }): number { - for (let j = 0; j < params.flags.length; j += 1) { - const flag = params.flags[j]; + for (const [j, flag] of params.flags.entries()) { if (params.deniedFlags.has(flag)) { return -1; } diff --git a/src/infra/heartbeat-events-filter.ts b/src/infra/heartbeat-events-filter.ts index 86b128822b09..75d17b27bdee 100644 --- a/src/infra/heartbeat-events-filter.ts +++ b/src/infra/heartbeat-events-filter.ts @@ -186,7 +186,7 @@ function isHeartbeatAckEvent(evt: string): boolean { if (suffix.length === 0) { return true; } - return !/[a-z0-9_]/.test(suffix[0]); + return !/[a-z0-9_]/.test(suffix.charAt(0)); } function isHeartbeatNoiseEvent(evt: string): boolean { diff --git a/src/infra/net/proxy-env.ts b/src/infra/net/proxy-env.ts index 7dd14088f9f6..05ac01db13b4 100644 --- a/src/infra/net/proxy-env.ts +++ b/src/infra/net/proxy-env.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Proxy environment helpers mirror undici EnvHttpProxyAgent selection while // adding OpenClaw NO_PROXY CIDR/wildcard bypass checks. import { readTrimmedStringAlias } from "../../utils/string-readers.js"; @@ -191,7 +192,7 @@ export function matchesNoProxy(targetUrl: string, env: NodeJS.ProcessEnv = proce if (!m) { continue; } - entryHost = m[1]; + entryHost = expectDefined(m[1], "m capture group 1"); entryPort = m[2]; } else { const firstColonIdx = entry.indexOf(":"); @@ -261,7 +262,7 @@ function matchesIpv4NoProxyPattern(targetHost: string, entryHost: string): boole const cidrMatch = entryHost.match(/^(\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})$/); if (cidrMatch) { - const network = parseIpv4Address(cidrMatch[1]); + const network = parseIpv4Address(expectDefined(cidrMatch[1], "cidr match capture group 1")); const prefixLength = Number(cidrMatch[2]); if (network === undefined || prefixLength < 0 || prefixLength > 32) { return false; @@ -278,8 +279,7 @@ function matchesIpv4NoProxyPattern(targetHost: string, entryHost: string): boole if (patternParts.length > 4 || patternParts.length === 0) { return false; } - for (let index = 0; index < patternParts.length; index += 1) { - const part = patternParts[index]; + for (const [index, part] of patternParts.entries()) { if (part === "*") { if (index === patternParts.length - 1) { return true; diff --git a/src/infra/net/ssrf.ts b/src/infra/net/ssrf.ts index 6a7ec6ec01f8..b1d8f6633d4b 100644 --- a/src/infra/net/ssrf.ts +++ b/src/infra/net/ssrf.ts @@ -17,6 +17,7 @@ import { parseCanonicalIpAddress, parseLooseIpAddress, } from "@openclaw/net-policy/ip"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import type { Dispatcher } from "undici"; import { normalizeHostname } from "./hostname.js"; @@ -532,7 +533,10 @@ export function createPinnedLookup(params: { cb(null, usable as LookupAddress[]); return; } - const chosen = usable[index % usable.length]; + const chosen = expectDefined( + usable[index % usable.length], + "usable entry at index % usable.length", + ); index += 1; cb(null, chosen.address, chosen.family); }) as typeof dnsLookupCb; diff --git a/src/infra/outbound/channel-selection.ts b/src/infra/outbound/channel-selection.ts index 77f61d8b4cac..1e0e75e68cd2 100644 --- a/src/infra/outbound/channel-selection.ts +++ b/src/infra/outbound/channel-selection.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Channel selection chooses a deliverable message channel from explicit input, // tool context fallback, or configured plugin accounts. import { listChannelPlugins } from "../../channels/plugins/index.js"; @@ -279,7 +280,11 @@ export async function resolveMessageChannelSelection(params: { const configured = await listConfiguredMessageChannels(params.cfg); if (configured.length === 1) { - return { channel: configured[0], configured, source: "single-configured" }; + return { + channel: expectDefined(configured[0], "configured entry at 0"), + configured, + source: "single-configured", + }; } if (configured.length === 0) { const repairHints = listConfiguredOfficialExternalRepairHints(params.cfg); diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index c055295dea14..4e617ac3487b 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Outbound delivery core runs plugin hooks, queue durability, channel adapter // sends, commit hooks, diagnostics, transcript mirroring, and payload outcomes. import { hasTrustedMessageAuditListeners } from "../../audit/message-audit-events.js"; @@ -1878,7 +1879,12 @@ async function deliverOutboundPayloadsCore( availableReportedIndices.has(reported.resultIndex) && !coveredIndices.includes(reported.resultIndex) && results[reported.resultIndex]?.channel === delivery.channel && - resultPlatformIds(results[reported.resultIndex]).has(receiptId), + resultPlatformIds( + expectDefined( + results[reported.resultIndex], + "results entry at reported.result index", + ), + ).has(receiptId), ) .map((reported) => reported.resultIndex); // One receipt part covers one progress result. Repeated parts preserve diff --git a/src/infra/outbound/envelope.ts b/src/infra/outbound/envelope.ts index 08bc603c525c..457ae2a44536 100644 --- a/src/infra/outbound/envelope.ts +++ b/src/infra/outbound/envelope.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Outbound envelopes wrap payload projections, metadata, and delivery JSON for // tool responses while flattening simple delivery-only results. import type { ReplyPayload } from "../../auto-reply/types.js"; @@ -32,7 +33,7 @@ export function buildOutboundResultEnvelope( ? undefined : params.payloads.length === 0 ? [] - : isOutboundPayloadJson(params.payloads[0]) + : isOutboundPayloadJson(expectDefined(params.payloads[0], "payloads entry at 0")) ? [...(params.payloads as readonly OutboundPayloadJson[])] : normalizeOutboundPayloadsForJson(params.payloads as readonly ReplyPayload[]); diff --git a/src/infra/outbound/format.ts b/src/infra/outbound/format.ts index c97d04895b38..681f82a74236 100644 --- a/src/infra/outbound/format.ts +++ b/src/infra/outbound/format.ts @@ -1,6 +1,6 @@ // Outbound delivery formatting produces human CLI summaries for direct and // gateway send results. -import { getChatChannelMeta } from "../../channels/chat-meta.js"; +import { findChatChannelMeta } from "../../channels/chat-meta.js"; import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { ChannelId } from "../../channels/plugins/types.public.js"; import { normalizeChatChannelId } from "../../channels/registry.js"; @@ -32,7 +32,7 @@ const resolveChannelLabel = (channel: string) => { // Some legacy chat channels are not plugins; keep their human labels for CLI output. const normalized = normalizeChatChannelId(channel); if (normalized) { - return getChatChannelMeta(normalized).label; + return findChatChannelMeta(normalized)?.label ?? channel; } return channel; }; diff --git a/src/infra/outbound/outbound-session.test-helpers.ts b/src/infra/outbound/outbound-session.test-helpers.ts index 3fcfa1e7a9dc..c382a7868195 100644 --- a/src/infra/outbound/outbound-session.test-helpers.ts +++ b/src/infra/outbound/outbound-session.test-helpers.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Test helpers build minimal plugin registries for outbound session-route // scenarios without importing real channel implementations. import { @@ -91,10 +92,13 @@ function parseForumTargetForTest(raw: string): { .replace(/^group:/i, ""); const prefixedTopic = /^([^:]+):topic:(\d+)$/i.exec(trimmed); if (prefixedTopic) { - const chatId = prefixedTopic[1]; + const chatId = expectDefined(prefixedTopic[1], "prefixed topic capture group 1"); return { chatId, - messageThreadId: Number.parseInt(prefixedTopic[2], 10), + messageThreadId: Number.parseInt( + expectDefined(prefixedTopic[2], "prefixed topic capture group 2"), + 10, + ), chatType: chatId.startsWith("-") ? "group" : "direct", }; } @@ -113,7 +117,7 @@ function parseForumThreadIdForTest(threadId?: string | number | null): number | if (!topicMatch) { return undefined; } - return Number.parseInt(topicMatch[1], 10); + return Number.parseInt(expectDefined(topicMatch[1], "topic match capture group 1"), 10); } function buildForumGroupPeerIdForTest(chatId: string, messageThreadId?: number): string { @@ -398,7 +402,9 @@ function resolveLocalChatOutboundSessionRouteForTest(params: ChannelOutboundSess return null; } const match = /^(chat_guid|chat_identifier|chat_id):(.+)$/i.exec(stripped); - const rawId = match ? match[2].trim() : stripped.trim(); + const rawId = match + ? expectDefined(match[2], "outbound session.test helpers regex capture 2").trim() + : stripped.trim(); if (!rawId) { return null; } diff --git a/src/infra/outbound/target-resolver.ts b/src/infra/outbound/target-resolver.ts index 29b86eb0088c..b6ecf586bef6 100644 --- a/src/infra/outbound/target-resolver.ts +++ b/src/infra/outbound/target-resolver.ts @@ -479,6 +479,9 @@ export async function resolveMessagingTarget(params: { }); if (match.kind === "single") { const entry = match.entry; + if (!entry) { + throw new Error("Single directory match is missing its entry"); + } return { ok: true, target: { diff --git a/src/infra/outbound/targets.ts b/src/infra/outbound/targets.ts index 80bd13d31279..e9a67444f7a9 100644 --- a/src/infra/outbound/targets.ts +++ b/src/infra/outbound/targets.ts @@ -61,6 +61,7 @@ export type HeartbeatSenderContext = { export type { OutboundTargetResolution } from "./targets-resolve-shared.js"; export { resolveSessionDeliveryTarget, type SessionDeliveryTarget } from "./targets-session.js"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveSessionDeliveryTarget, type SessionDeliveryTarget } from "./targets-session.js"; /** Resolves a user-supplied outbound destination through the channel plugin. */ @@ -549,5 +550,5 @@ export function resolveHeartbeatSenderContext(params: { provider, }); - return { sender, provider, allowFrom }; + return { sender: expectDefined(sender, "resolved sender"), provider, allowFrom }; } diff --git a/src/infra/ports-format.ts b/src/infra/ports-format.ts index c27d545db1cd..002db61d0bba 100644 --- a/src/infra/ports-format.ts +++ b/src/infra/ports-format.ts @@ -1,5 +1,6 @@ // Formats port probe results for diagnostics and CLI output. import net from "node:net"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { formatCliCommand } from "../cli/command-format.js"; import type { PortListener, PortListenerKind, PortUsage } from "./ports-types.js"; @@ -49,7 +50,10 @@ function parseListenerAddress(address: string): { host: string; port: number } | const normalized = trimmed.replace(/^tcp6?\s+/i, "").replace(/\s*\(listen\)\s*$/i, ""); const bracketMatch = normalized.match(/^\[([^\]]+)\]:(\d+)$/); if (bracketMatch) { - const port = Number.parseInt(bracketMatch[2], 10); + const port = Number.parseInt( + expectDefined(bracketMatch[2], "bracket match capture group 2"), + 10, + ); return Number.isFinite(port) ? { host: normalizeLowercaseStringOrEmpty(bracketMatch[1]), port } : null; diff --git a/src/infra/ports-inspect.ts b/src/infra/ports-inspect.ts index 1ca588e3d2a7..f4dbc4a1f6e1 100644 --- a/src/infra/ports-inspect.ts +++ b/src/infra/ports-inspect.ts @@ -1,5 +1,6 @@ // Inspects gateway port listeners and connection state. import os from "node:os"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { runCommandWithTimeout } from "../process/exec.js"; import { parseStrictPositiveInteger } from "./parse-finite-number.js"; @@ -182,7 +183,7 @@ function parseSsConnections(output: string, port: number): PortConnection[] { }; const pidMatch = line.match(/pid=(\d+)/); if (pidMatch) { - const pid = Number.parseInt(pidMatch[1], 10); + const pid = Number.parseInt(expectDefined(pidMatch[1], "pid match capture group 1"), 10); if (Number.isFinite(pid)) { connection.pid = pid; } @@ -330,7 +331,7 @@ function parseSsListeners(output: string, port: number): PortListener[] { }; const pidMatch = line.match(/pid=(\d+)/); if (pidMatch) { - const pid = Number.parseInt(pidMatch[1], 10); + const pid = Number.parseInt(expectDefined(pidMatch[1], "pid match capture group 1"), 10); if (Number.isFinite(pid)) { listener.pid = pid; } diff --git a/src/infra/ports-netstat.ts b/src/infra/ports-netstat.ts index 304765a61b91..44a81f9133b5 100644 --- a/src/infra/ports-netstat.ts +++ b/src/infra/ports-netstat.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { parseStrictPositiveInteger } from "./parse-finite-number.js"; import type { PortListener } from "./ports-types.js"; @@ -21,7 +22,12 @@ export function parseTcpEndpoint(raw: string): { host: string; port: number } | const bracketMatch = endpoint.match(/^\[([^\]]+)\]:(\d+)$/); if (bracketMatch) { const port = parseTcpPort(bracketMatch[2]); - return port === null ? null : { host: normalizeTcpHost(bracketMatch[1]), port }; + return port === null + ? null + : { + host: normalizeTcpHost(expectDefined(bracketMatch[1], "bracket match capture group 1")), + port, + }; } const lastColon = endpoint.lastIndexOf(":"); if (lastColon <= 0 || lastColon >= endpoint.length - 1) { diff --git a/src/infra/provider-usage.fetch.gemini.ts b/src/infra/provider-usage.fetch.gemini.ts index 30e308e7c02a..152046f1aac4 100644 --- a/src/infra/provider-usage.fetch.gemini.ts +++ b/src/infra/provider-usage.fetch.gemini.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Fetches Gemini provider usage windows. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { @@ -6,7 +7,7 @@ import { fetchJson, readUsageJson, } from "./provider-usage.fetch.shared.js"; -import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; +import { clampPercent, providerUsageLabel } from "./provider-usage.shared.js"; import type { ProviderUsageSnapshot, UsageProviderId, @@ -95,5 +96,9 @@ export async function fetchGeminiUsage( }); } - return { provider, displayName: PROVIDER_LABELS[provider], windows }; + return { + provider, + displayName: expectDefined(providerUsageLabel(provider), "gemini provider usage label"), + windows, + }; } diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 26556ee33ddc..99a07b9f419f 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -6,7 +6,7 @@ import type { UsageProviderId } from "./provider-usage.types.js"; /** Default timeout for provider usage collection. */ export const DEFAULT_TIMEOUT_MS = 5000; -export const PROVIDER_LABELS: Readonly> = { +export const PROVIDER_LABELS = { anthropic: "Claude", clawrouter: "ClawRouter", deepseek: "DeepSeek", @@ -19,10 +19,16 @@ export const PROVIDER_LABELS: Readonly> = { xiaomi: "Xiaomi", "xiaomi-token-plan": "Xiaomi Token Plan", zai: "z.ai", -}; +} as const satisfies Readonly>; + +/** Dynamic-key lookup view; closed-key reads should use PROVIDER_LABELS directly. */ +export function providerUsageLabel(provider: string): string | undefined { + const labels: Readonly> = PROVIDER_LABELS; + return labels[provider]; +} export function resolveProviderUsageDisplayName(provider: string): string { - return PROVIDER_LABELS[provider] ?? provider; + return providerUsageLabel(provider) ?? provider; } /** Returns true for providers whose usage endpoint is only meaningful with OAuth/token auth. */ diff --git a/src/infra/push-web.ts b/src/infra/push-web.ts index 255536d99796..087c13f1efe4 100644 --- a/src/infra/push-web.ts +++ b/src/infra/push-web.ts @@ -1,6 +1,7 @@ // Stores and verifies web push subscriptions and delivery payloads. import { randomUUID } from "node:crypto"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { resolveStateDir } from "../config/paths.js"; import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { sha256HexPrefix } from "./crypto-digest.js"; @@ -284,7 +285,8 @@ export async function broadcastWebPush( ? r.value : { ok: false, - subscriptionId: subscriptions[i].subscriptionId, + subscriptionId: expectDefined(subscriptions[i], "subscriptions entry at i") + .subscriptionId, error: r.reason instanceof Error ? r.reason.message : "unknown error", }, ); @@ -293,7 +295,7 @@ export async function broadcastWebPush( const expiredEndpoints = mapped .map((result, i) => ({ result, sub: subscriptions[i] })) .filter(({ result }) => !result.ok && (result.statusCode === 410 || result.statusCode === 404)) - .map(({ sub }) => sub.endpoint); + .map(({ sub }) => expectDefined(sub, "push web sub").endpoint); if (expiredEndpoints.length > 0) { await Promise.allSettled( diff --git a/src/infra/runtime-guard.ts b/src/infra/runtime-guard.ts index 6e6f957d4e5f..1d191f6d2c1b 100644 --- a/src/infra/runtime-guard.ts +++ b/src/infra/runtime-guard.ts @@ -1,5 +1,6 @@ // Validates the current runtime against OpenClaw's Node engine floor. import process from "node:process"; +import { expectDefined } from "@openclaw/normalization-core"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; type RuntimeKind = "node" | "unknown"; @@ -37,9 +38,9 @@ export function parseSemver(version: string | null): Semver | null { } const [, major, minor, patch] = match; return { - major: Number.parseInt(major, 10), - minor: Number.parseInt(minor, 10), - patch: Number.parseInt(patch, 10), + major: Number.parseInt(expectDefined(major, "runtime guard major"), 10), + minor: Number.parseInt(expectDefined(minor, "runtime guard minor"), 10), + patch: Number.parseInt(expectDefined(patch, "runtime guard patch"), 10), }; } diff --git a/src/infra/session-cost-usage.ts b/src/infra/session-cost-usage.ts index 2c5092ca1513..50810490d975 100644 --- a/src/infra/session-cost-usage.ts +++ b/src/infra/session-cost-usage.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; +import { expectDefined } from "@openclaw/normalization-core"; import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; @@ -1229,9 +1230,9 @@ const computeLatencyStats = (values: number[]): SessionLatencyStats | undefined return { count, avgMs: total / count, - p95Ms: sorted[p95Index] ?? sorted[count - 1], - minMs: sorted[0], - maxMs: sorted[count - 1], + p95Ms: sorted[p95Index] ?? expectDefined(sorted[count - 1], "last latency sample"), + minMs: expectDefined(sorted[0], "sorted entry at 0"), + maxMs: expectDefined(sorted[count - 1], "sorted entry at count 1"), }; }; diff --git a/src/infra/sqlite-wal.ts b/src/infra/sqlite-wal.ts index a1463bdd31f7..ce6a7ed22151 100644 --- a/src/infra/sqlite-wal.ts +++ b/src/infra/sqlite-wal.ts @@ -3,6 +3,7 @@ import childProcess from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; +import { expectDefined } from "@openclaw/normalization-core"; import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { isSqliteLockError } from "./sqlite-transaction.js"; @@ -149,12 +150,20 @@ function parseMountCommandEntries(contents: string): MountEntry[] { for (const line of contents.split("\n")) { const linuxMatch = /^(.+) on (.+) type ([^,\s)]+) \(/.exec(line); if (linuxMatch) { - entries.push({ source: linuxMatch[1], mountPoint: linuxMatch[2], fsType: linuxMatch[3] }); + entries.push({ + source: linuxMatch[1], + mountPoint: expectDefined(linuxMatch[2], "linux match capture group 2"), + fsType: expectDefined(linuxMatch[3], "linux match capture group 3"), + }); continue; } const bsdMatch = /^(.+) on (.+) \(([^,\s)]+)/.exec(line); if (bsdMatch) { - entries.push({ source: bsdMatch[1], mountPoint: bsdMatch[2], fsType: bsdMatch[3] }); + entries.push({ + source: bsdMatch[1], + mountPoint: expectDefined(bsdMatch[2], "bsd match capture group 2"), + fsType: expectDefined(bsdMatch[3], "bsd match capture group 3"), + }); } } return entries; diff --git a/src/infra/stable-node-path.ts b/src/infra/stable-node-path.ts index a3a73fa594ca..1cbba25df8c8 100644 --- a/src/infra/stable-node-path.ts +++ b/src/infra/stable-node-path.ts @@ -1,6 +1,7 @@ // Resolves Homebrew Node binary paths to stable symlink targets. import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; /** * Homebrew Cellar paths (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node) @@ -16,8 +17,8 @@ export async function resolveStableNodePath(nodePath: string): Promise { if (!cellarMatch) { return nodePath; } - const prefix = cellarMatch[1]; // e.g. /opt/homebrew - const formula = cellarMatch[2]; // e.g. "node" or "node@22" + const prefix = expectDefined(cellarMatch[1], "cellar match capture group 1"); // e.g. /opt/homebrew + const formula = expectDefined(cellarMatch[2], "cellar match capture group 2"); // e.g. "node" or "node@22" const pathModule = nodePath.includes("\\") ? path.win32 : path.posix; // Try the Homebrew opt symlink first — works for both default and versioned formulas. diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index 576bf13de18a..8c6b6868aa3d 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { DatabaseSync, SQLInputValue } from "node:sqlite"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { writeAcpSessionMetaForMigration } from "../acp/runtime/session-meta.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; @@ -1171,12 +1172,13 @@ async function migrateLegacyTaskRunsSidecar(params: { "terminal_outcome", ]; for (const row of taskRows) { + const taskId = legacyKeyValue(expectDefined(row.task_id, "task migration row key")); const existing = db .prepare(`SELECT ${taskColumns.join(", ")} FROM task_runs WHERE task_id = ?`) - .get(legacyKeyValue(row.task_id)); + .get(taskId); if (existing) { if (!legacyRowsMatch(existing as Record, row, taskColumns)) { - conflicts.push(legacyKeyValue(row.task_id)); + conflicts.push(taskId); } continue; } @@ -1185,20 +1187,19 @@ async function migrateLegacyTaskRunsSidecar(params: { } const deliveryColumns = ["requester_origin_json", "last_notified_event_at"]; for (const row of deliveryRows) { + const taskId = legacyKeyValue(expectDefined(row.task_id, "delivery migration row key")); const existing = db .prepare( `SELECT requester_origin_json, last_notified_event_at FROM task_delivery_state WHERE task_id = ?`, ) - .get(legacyKeyValue(row.task_id)); + .get(taskId); if (existing) { if (!legacyRowsMatch(existing as Record, row, deliveryColumns)) { - conflicts.push(`${legacyKeyValue(row.task_id)}/delivery`); + conflicts.push(`${taskId}/delivery`); } continue; } - const taskExists = db - .prepare("SELECT 1 FROM task_runs WHERE task_id = ?") - .get(legacyKeyValue(row.task_id)); + const taskExists = db.prepare("SELECT 1 FROM task_runs WHERE task_id = ?").get(taskId); if (!taskExists) { skippedOrphanDeliveryStates++; continue; @@ -1296,12 +1297,13 @@ async function migrateLegacyFlowRunsSidecar(params: { "ended_at", ]; for (const row of rows) { + const flowId = legacyKeyValue(expectDefined(row.flow_id, "flow migration row key")); const existing = db .prepare(`SELECT ${columns.join(", ")} FROM flow_runs WHERE flow_id = ?`) - .get(legacyKeyValue(row.flow_id)); + .get(flowId); if (existing) { if (!legacyRowsMatch(existing as Record, row, columns)) { - conflicts.push(legacyKeyValue(row.flow_id)); + conflicts.push(flowId); } continue; } diff --git a/src/infra/system-events.ts b/src/infra/system-events.ts index 1103c961bca0..f3580d6f415f 100644 --- a/src/infra/system-events.ts +++ b/src/infra/system-events.ts @@ -2,6 +2,7 @@ // prefixed to the next prompt. We intentionally avoid persistence to keep // events ephemeral. Events are session-scoped and require an explicit key. +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString, normalizeOptionalString, @@ -186,7 +187,7 @@ function resetQueueState(key: string, entry: SessionQueue) { return; } for (let index = entry.queue.length - 1; index >= 0; index -= 1) { - const contextKey = entry.queue[index].contextKey ?? null; + const contextKey = expectDefined(entry.queue[index], "queue entry at index").contextKey ?? null; if (contextKey !== null) { entry.lastContextKey = contextKey; return; @@ -206,7 +207,9 @@ export function consumeSystemEventEntries( } if ( consumedEntries.length > entry.queue.length || - !consumedEntries.every((event, index) => areSystemEventsEqual(entry.queue[index], event)) + !consumedEntries.every((event, index) => + areSystemEventsEqual(expectDefined(entry.queue[index], "queue entry at index"), event), + ) ) { return []; } diff --git a/src/infra/system-presence.ts b/src/infra/system-presence.ts index 25c904134e6b..cebab6406469 100644 --- a/src/infra/system-presence.ts +++ b/src/infra/system-presence.ts @@ -145,6 +145,16 @@ function parsePresence(text: string): SystemPresence { return { text: trimmed, ts: Date.now() }; } const [, host, ip, version, lastInputStr, mode, reasonRaw] = match; + if ( + host === undefined || + ip === undefined || + version === undefined || + lastInputStr === undefined || + mode === undefined || + reasonRaw === undefined + ) { + return { text: trimmed, ts: Date.now() }; + } const lastInputSeconds = Number.parseInt(lastInputStr, 10); const reason = reasonRaw.trim(); return { @@ -283,8 +293,8 @@ export function listSystemPresence(): SystemPresence[] { if (entries.size > MAX_ENTRIES) { const sorted = [...entries.entries()].toSorted((a, b) => a[1].ts - b[1].ts); const toDrop = entries.size - MAX_ENTRIES; - for (let i = 0; i < toDrop; i++) { - entries.delete(sorted[i][0]); + for (const [key] of sorted.slice(0, toDrop)) { + entries.delete(key); } } touchSelfPresence(); diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index fd0f6cc8b05d..aaf764048ae4 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -146,8 +146,9 @@ export async function getTailnetHostname(exec: typeof runExec = runExec, detecte if (dns && dns.length > 0) { return dns.replace(/\.$/, ""); } - if (ips.length > 0) { - return ips[0]; + const [firstIp] = ips; + if (firstIp !== undefined) { + return firstIp; } throw new Error("Could not determine Tailscale DNS or IP"); } catch (err) { diff --git a/src/infra/widearea-dns.ts b/src/infra/widearea-dns.ts index 07a958902459..3ce52b70c4a2 100644 --- a/src/infra/widearea-dns.ts +++ b/src/infra/widearea-dns.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { CONFIG_DIR, ensureDir } from "../utils.js"; @@ -102,7 +103,7 @@ function extractSerial(zoneText: string): number | null { if (!match) { return null; } - const parsed = Number.parseInt(match[1], 10); + const parsed = Number.parseInt(expectDefined(match[1], "widearea dns regex capture 1"), 10); return Number.isFinite(parsed) ? parsed : null; } diff --git a/src/infra/windows-encoding.ts b/src/infra/windows-encoding.ts index ed474d7f03b1..cc7d8a85b599 100644 --- a/src/infra/windows-encoding.ts +++ b/src/infra/windows-encoding.ts @@ -212,13 +212,11 @@ export function createWindowsOutputDecoder(params?: { function getTrailingIncompleteUtf8Bytes(buffer: Buffer): Buffer { let index = buffer.length - 1; let continuationBytes = 0; - while ( - index >= 0 && - buffer[index] !== undefined && - buffer[index] >= 0x80 && - buffer[index] <= 0xbf && - continuationBytes < 3 - ) { + while (index >= 0 && continuationBytes < 3) { + const byte = buffer.at(index); + if (byte === undefined || byte < 0x80 || byte > 0xbf) { + break; + } continuationBytes += 1; index -= 1; } @@ -226,7 +224,10 @@ function getTrailingIncompleteUtf8Bytes(buffer: Buffer): Buffer { return buffer; } - const leadByte = buffer[index]; + const leadByte = buffer.at(index); + if (leadByte === undefined) { + return Buffer.alloc(0); + } const sequenceLength = getUtf8SequenceLength(leadByte); if (sequenceLength <= 1) { return Buffer.alloc(0); diff --git a/src/infra/windows-shell-command.ts b/src/infra/windows-shell-command.ts index ff499cc7d7f9..7641c9b6ad74 100644 --- a/src/infra/windows-shell-command.ts +++ b/src/infra/windows-shell-command.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { ExecCommandAnalysis } from "./exec-command-analysis-types.js"; import { resolveCommandResolutionFromArgv } from "./exec-command-resolution.js"; @@ -27,7 +28,7 @@ function findWindowsUnsupportedToken(command: string): string | null { // cmd.exe does not recognise single quotes, so they are not treated as safe // quoting for this cross-host safety check. for (let i = 0; i < command.length; i++) { - const ch = command[i]; + const ch = command.charAt(i); if (ch === '"') { inDouble = !inDouble; continue; @@ -68,7 +69,7 @@ export function tokenizeWindowsSegment(segment: string): string[] | null { }; for (let i = 0; i < segment.length; i += 1) { - const ch = segment[i]; + const ch = segment.charAt(i); if (ch === '"' && !inSingle) { if (!inDouble) { wasQuoted = true; @@ -118,7 +119,7 @@ function stripWindowsShellWrapper(command: string): string { function stripWindowsShellWrapperOnce(command: string): string { const psCallMatch = command.match(/^&\s+(.+)$/s); if (psCallMatch) { - return psCallMatch[1]; + return expectDefined(psCallMatch[1], "ps call match capture group 1"); } const psFlags = @@ -129,19 +130,22 @@ function stripWindowsShellWrapperOnce(command: string): string { new RegExp(`^(?:powershell|pwsh)(?:\\.exe)?\\s+${psFlags}${psCommandFlag}\\s+"(.+)"$`, "is"), ); if (psInvokeMatch) { - return psInvokeMatch[1].replace(/""/g, '"'); + return expectDefined(psInvokeMatch[1], "ps invoke match capture group 1").replace(/""/g, '"'); } const psInvokeSingleQuote = command.match( new RegExp(`^(?:powershell|pwsh)(?:\\.exe)?\\s+${psFlags}${psCommandFlag}\\s+'(.+)'$`, "is"), ); if (psInvokeSingleQuote) { - return psInvokeSingleQuote[1].replace(/''/g, "'"); + return expectDefined(psInvokeSingleQuote[1], "ps invoke single quote capture group 1").replace( + /''/g, + "'", + ); } const psInvokeNoQuote = command.match( new RegExp(`^(?:powershell|pwsh)(?:\\.exe)?\\s+${psFlags}${psCommandFlag}\\s+(.+)$`, "is"), ); if (psInvokeNoQuote) { - return psInvokeNoQuote[1]; + return expectDefined(psInvokeNoQuote[1], "ps invoke no quote capture group 1"); } // `cmd /c` stays intact because PowerShell execution would change cmd.exe diff --git a/src/llm/utils/oauth/github-copilot.ts b/src/llm/utils/oauth/github-copilot.ts index 9568f3be2533..870d30de9c8f 100644 --- a/src/llm/utils/oauth/github-copilot.ts +++ b/src/llm/utils/oauth/github-copilot.ts @@ -2,6 +2,7 @@ * GitHub Copilot OAuth flow */ +import { expectDefined } from "@openclaw/normalization-core"; import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; import { assertOkOrThrowProviderError, @@ -109,7 +110,7 @@ function getBaseUrlFromToken(token: string): string | null { if (!match) { return null; } - const proxyHost = match[1]; + const proxyHost = expectDefined(match[1], "github copilot regex capture 1"); // Convert proxy.xxx to api.xxx const apiHost = proxyHost.replace(/^proxy\./, "api."); return `https://${apiHost}`; diff --git a/src/logger.ts b/src/logger.ts index 20cd30f32001..fa2f5808a72d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Provides root logger helpers and themed terminal output. import { theme } from "../packages/terminal-core/src/theme.js"; import { isVerbose } from "./global-state.js"; @@ -12,7 +13,11 @@ function splitSubsystem(message: string) { if (!match) { return null; } - const [, subsystem, rest] = match; + const subsystem = match.at(1); + const rest = match.at(2); + if (subsystem === undefined || rest === undefined) { + return null; + } return { subsystem, rest }; } @@ -29,7 +34,11 @@ function logWithSubsystem(params: { }) { const parsed = params.runtime === defaultRuntime ? splitSubsystem(params.message) : null; if (parsed) { - createSubsystemLogger(parsed.subsystem)[params.subsystemMethod](parsed.rest); + const method = expectDefined( + createSubsystemLogger(parsed.subsystem)[params.subsystemMethod], + "subsystem logger method", + ); + method(parsed.rest); return; } params.runtime[params.runtimeMethod](params.runtimeFormatter(params.message)); diff --git a/src/logging/diagnostic-session-context.ts b/src/logging/diagnostic-session-context.ts index d87158e7a24f..0b34a200fbdb 100644 --- a/src/logging/diagnostic-session-context.ts +++ b/src/logging/diagnostic-session-context.ts @@ -1,6 +1,7 @@ // Diagnostic session context helpers capture session metadata for support bundles. import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { resolveStateDir } from "../config/paths.js"; import { loadCronJobsStoreSync, resolveCronJobsStorePath } from "../cron/store.js"; @@ -118,7 +119,7 @@ export function readLastAssistantFromSessionFile(filePath: string | undefined): } for (let index = lines.length - 1; index >= 0; index -= 1) { try { - const parsed = JSON.parse(lines[index]) as { + const parsed = JSON.parse(expectDefined(lines[index], "lines entry at index")) as { message?: { role?: unknown; content?: unknown }; }; if (parsed.message?.role !== "assistant") { diff --git a/src/logging/diagnostic-stability-bundle.ts b/src/logging/diagnostic-stability-bundle.ts index 33577b9a49b6..94b5c01ab4da 100644 --- a/src/logging/diagnostic-stability-bundle.ts +++ b/src/logging/diagnostic-stability-bundle.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import v8 from "node:v8"; +import { expectDefined } from "@openclaw/normalization-core"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { resolveStateDir } from "../config/paths.js"; import type { @@ -1027,10 +1028,10 @@ function collectActiveResources(): DiagnosticActiveResourceSummary | undefined { function sanitizeSessionEvidencePath(relativePath: string): string { const parts = relativePath.split("/"); if (parts.length === 4 && parts[0] === "agents" && parts[2] === "sessions") { - return `agents//sessions/${sanitizeSessionEvidenceFileName(parts[3])}`; + return `agents//sessions/${sanitizeSessionEvidenceFileName(expectDefined(parts[3], "parts entry at 3"))}`; } if (parts.length === 2 && parts[0] === "sessions") { - return `sessions/${sanitizeSessionEvidenceFileName(parts[1])}`; + return `sessions/${sanitizeSessionEvidenceFileName(expectDefined(parts[1], "parts entry at 1"))}`; } return redactSensitiveText(relativePath, { mode: "tools" }); } diff --git a/src/logging/logger.ts b/src/logging/logger.ts index 099105bc8acf..2be688fe7756 100644 --- a/src/logging/logger.ts +++ b/src/logging/logger.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { Logger as TsLogger } from "tslog"; import type { OpenClawConfig } from "../config/types.js"; @@ -607,7 +608,10 @@ function buildLogger(settings: ResolvedSettings): TsLogger { const structuredFields = buildStructuredFileLogFields(logObj as TsLogRecord); const record = { ...logObj, - _meta: withResolvedLogMetaHostname(logObj["_meta"], structuredFields.hostname), + _meta: withResolvedLogMetaHostname( + logObj["_meta"], + expectDefined(structuredFields.hostname, "structured log hostname"), + ), time, ...structuredFields, ...traceFields, diff --git a/src/logging/redact.ts b/src/logging/redact.ts index 93c7f5fbad42..b584753790c0 100644 --- a/src/logging/redact.ts +++ b/src/logging/redact.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Redaction helpers scrub secrets and sensitive identifiers from log output. import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -317,8 +318,11 @@ function parsePattern(raw: RedactPattern): RegExp | null { } else if (raw.trim()) { const match = raw.match(/^\/(.+)\/([gimsuy]*)$/); if (match) { - const flags = match[2].includes("g") ? match[2] : `${match[2]}g`; - pattern = compileConfigRegex(match[1], flags)?.regex ?? null; + const flags = expectDefined(match[2], "redact regex capture 2").includes("g") + ? match[2] + : `${match[2]}g`; + pattern = + compileConfigRegex(expectDefined(match[1], "redact regex capture 1"), flags)?.regex ?? null; } else { pattern = compileConfigRegex(raw, "gi")?.regex ?? null; } diff --git a/src/logging/secret-redaction-registry.ts b/src/logging/secret-redaction-registry.ts index 8aff64adb56f..dc95f67fb7b5 100644 --- a/src/logging/secret-redaction-registry.ts +++ b/src/logging/secret-redaction-registry.ts @@ -10,7 +10,7 @@ function escapeRegExp(value: string): string { } function rebuildProbe(): void { - firstChars = new Set([...registeredValues.keys()].map((value) => value[0])); + firstChars = new Set([...registeredValues.keys()].map((value) => value.charAt(0))); compiledMatcher = undefined; } diff --git a/src/logging/subsystem.ts b/src/logging/subsystem.ts index dd1f651ae68f..5cb2c1ba2c69 100644 --- a/src/logging/subsystem.ts +++ b/src/logging/subsystem.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Subsystem logger helpers create scoped loggers with subsystem-specific filters. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { Chalk } from "chalk"; @@ -161,24 +162,29 @@ function pickSubsystemColor(color: ChalkInstance, subsystem: string): ChalkInsta hash = (hash * 31 + subsystem.charCodeAt(i)) | 0; } const idx = Math.abs(hash) % SUBSYSTEM_COLORS.length; - const name = SUBSYSTEM_COLORS[idx]; + const name = expectDefined(SUBSYSTEM_COLORS[idx], "subsystem colors entry at idx"); return color[name]; } function formatSubsystemForConsole(subsystem: string): string { const parts = subsystem.split("/").filter(Boolean); const original = parts.join("/") || subsystem; - while ( - parts.length > 0 && - SUBSYSTEM_PREFIXES_TO_DROP.includes(parts[0] as (typeof SUBSYSTEM_PREFIXES_TO_DROP)[number]) - ) { + while (parts.length > 0) { + const first = parts.at(0); + if ( + first === undefined || + !SUBSYSTEM_PREFIXES_TO_DROP.includes(first as (typeof SUBSYSTEM_PREFIXES_TO_DROP)[number]) + ) { + break; + } parts.shift(); } - if (parts.length === 0) { + const first = parts.at(0); + if (first === undefined) { return original; } - if (isChannelSubsystemPrefix(parts[0])) { - return parts[0]; + if (isChannelSubsystemPrefix(first)) { + return first; } if (parts.length > SUBSYSTEM_MAX_SEGMENTS) { return parts.slice(-SUBSYSTEM_MAX_SEGMENTS).join("/"); diff --git a/src/media-understanding/apply.ts b/src/media-understanding/apply.ts index 11c10cd83f7e..4d5f1fd020c1 100644 --- a/src/media-understanding/apply.ts +++ b/src/media-understanding/apply.ts @@ -1,6 +1,7 @@ // Applies media-understanding outputs to inbound message context, including // attachment normalization, provider execution, file text extraction, and echoing. import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -267,7 +268,9 @@ function hasSuspiciousBinarySignal(buffer?: Buffer): boolean { if (sample.length < 4 || sample[0] !== 0x50 || sample[1] !== 0x4b) { return false; } - const signature = (sample[2] << 8) | sample[3]; + const signature = + (expectDefined(sample[2], "sample entry at 2") << 8) | + expectDefined(sample[3], "sample entry at 3"); // Cover the ZIP local-header, central-directory, and empty-archive markers // so archive payloads cannot slip past text coercion when MIME detection is weak. return signature === 0x0304 || signature === 0x0102 || signature === 0x0506; @@ -282,8 +285,8 @@ function decodeTextSample(buffer?: Buffer): string { if (utf16Charset === "utf-16be") { const swapped = Buffer.alloc(sample.length); for (let i = 0; i + 1 < sample.length; i += 2) { - swapped[i] = sample[i + 1]; - swapped[i + 1] = sample[i]; + swapped[i] = expectDefined(sample[i + 1], "UTF-16BE low byte"); + swapped[i + 1] = expectDefined(sample[i], "UTF-16BE high byte"); } return new TextDecoder("utf-16le").decode(swapped); } diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index c941897ac9e2..20b12a1d6438 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -2,6 +2,7 @@ // rotation, output extraction, and decision summaries. import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty, normalizeNullableString, @@ -1072,11 +1073,15 @@ export async function runCliEntry(params: { if (shouldLogVerbose()) { logVerbose(`Media understanding via CLI: ${argv.join(" ")}`); } - const { stdout, stderr } = await runExec(argv[0], argv.slice(1), { - timeoutMs, - maxBuffer: CLI_OUTPUT_MAX_BUFFER, - cwd: isAntigravityCliCommand(command) ? path.dirname(mediaPath) : undefined, - }); + const { stdout, stderr } = await runExec( + expectDefined(argv[0], "argv entry at 0"), + argv.slice(1), + { + timeoutMs, + maxBuffer: CLI_OUTPUT_MAX_BUFFER, + cwd: isAntigravityCliCommand(command) ? path.dirname(mediaPath) : undefined, + }, + ); const requestedBackend = capability === "audio" ? resolveRequestedLocalAudioBackend({ diff --git a/src/media/fetch.ts b/src/media/fetch.ts index 59bd47c94653..f592e6754b9e 100644 --- a/src/media/fetch.ts +++ b/src/media/fetch.ts @@ -3,6 +3,7 @@ import { MAX_DOCUMENT_BYTES } from "@openclaw/media-core/constants"; import { parseMediaContentLength } from "@openclaw/media-core/content-length"; import { basenameFromAnyPath, extnameFromAnyPath } from "@openclaw/media-core/file-name"; import { detectMime, extensionForMime } from "@openclaw/media-core/mime"; +import { expectDefined } from "@openclaw/normalization-core"; import { isAbortError, mergeAbortSignals } from "../infra/abort-signal.js"; import { formatErrorMessage } from "../infra/errors.js"; import { @@ -237,7 +238,7 @@ async function fetchGuardedMediaResponse( const attemptErrors: unknown[] = []; for (let i = 0; i < attempts.length; i += 1) { try { - result = await runGuardedFetch(attempts[i]); + result = await runGuardedFetch(expectDefined(attempts[i], "attempts entry at i")); break; } catch (err) { if ( diff --git a/src/media/parse.ts b/src/media/parse.ts index 4b0332f81078..9c320d6f75ce 100644 --- a/src/media/parse.ts +++ b/src/media/parse.ts @@ -10,6 +10,7 @@ import { parseLooseIpAddress, } from "@openclaw/net-policy/ip"; import { hasHttpUrlPrefix } from "@openclaw/net-policy/url-protocol"; +import { expectDefined } from "@openclaw/normalization-core"; import { parseFenceSpans } from "../../packages/markdown-core/src/fences.js"; import { parseAudioTag } from "./audio-tags.js"; @@ -346,7 +347,7 @@ function parseMarkdownImageDestination( let destinationEnd = index; let parenDepth = 0; while (index < input.length) { - const ch = input[index]; + const ch = input.charAt(index); if (ch === "\\") { index += 2; destinationEnd = index; @@ -583,7 +584,7 @@ export function splitMediaFromOutput( const start = match.index ?? 0; pieces.push(line.slice(cursor, start)); - const payload = match[1]; + const payload = expectDefined(match[1], "parse regex capture 1"); const unwrapped = unwrapQuoted(payload); const payloadValue = unwrapped ?? payload; const parts = unwrapped ? [unwrapped] : payload.split(/\s+/).filter(Boolean); diff --git a/src/media/png-encode.ts b/src/media/png-encode.ts index eff04189c8cb..a35ab5e46ef8 100644 --- a/src/media/png-encode.ts +++ b/src/media/png-encode.ts @@ -1,5 +1,6 @@ // PNG encode helpers build small PNG files without external image dependencies. import { deflateSync } from "node:zlib"; +import { expectDefined } from "@openclaw/normalization-core"; const CRC_TABLE = (() => { const table = new Uint32Array(256); @@ -17,7 +18,9 @@ const CRC_TABLE = (() => { function crc32(buf: Buffer): number { let crc = 0xffffffff; for (const byte of buf) { - crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); + crc = + expectDefined(CRC_TABLE[(crc ^ byte) & 0xff], "crc table entry at (crc ^ byte) & 0xff") ^ + (crc >>> 8); } return (crc ^ 0xffffffff) >>> 0; } diff --git a/src/node-host/invoke-system-run-allowlist.ts b/src/node-host/invoke-system-run-allowlist.ts index 3cccade374c2..e6ca0497272f 100644 --- a/src/node-host/invoke-system-run-allowlist.ts +++ b/src/node-host/invoke-system-run-allowlist.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Resolves system.run allowlist matches, argv plans, and truncated command output. */ import { analyzeArgvCommand, @@ -132,7 +133,9 @@ export function resolvePlannedAllowlistArgv(params: { ) { return undefined; } - const plannedAllowlistArgv = resolvePlannedSegmentArgv(params.segments[0]); + const plannedAllowlistArgv = resolvePlannedSegmentArgv( + expectDefined(params.segments[0], "segments entry at 0"), + ); return plannedAllowlistArgv && plannedAllowlistArgv.length > 0 ? plannedAllowlistArgv : null; } @@ -172,7 +175,9 @@ export async function resolveSystemRunExecArgv(params: { ) { // Exact-path matches stay bound to the resolved executable, while the bare // wildcard contract can still authorize unresolved Windows commands. - const plannedArgv = resolvePlannedSegmentArgv(params.segments[0]); + const plannedArgv = resolvePlannedSegmentArgv( + expectDefined(params.segments[0], "segments entry at 0"), + ); if (!plannedArgv) { return null; } diff --git a/src/node-host/invoke-system-run-plan.ts b/src/node-host/invoke-system-run-plan.ts index 9d3b0c87d4bf..c4c814a7758d 100644 --- a/src/node-host/invoke-system-run-plan.ts +++ b/src/node-host/invoke-system-run-plan.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty, normalizeNullableString, @@ -562,7 +563,7 @@ function resolveGenericInterpreterScriptOperandIndex(params: { if (collection.sawOptionValueFile) { return null; } - return collection.hits.length === 1 ? collection.hits[0] : null; + return collection.hits.length === 1 ? expectDefined(collection.hits[0], "hits entry at 0") : null; } function resolveBunScriptOperandIndex(params: { diff --git a/src/node-host/invoke.ts b/src/node-host/invoke.ts index 15140f6a0ce0..d4882e712f79 100644 --- a/src/node-host/invoke.ts +++ b/src/node-host/invoke.ts @@ -3,6 +3,7 @@ import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; @@ -355,7 +356,7 @@ async function runCommand( // node result because runner.ts intentionally dispatches invokes with `void`. let child: ReturnType; try { - child = spawn(argv[0], argv.slice(1), { + child = spawn(expectDefined(argv[0], "argv entry at 0"), argv.slice(1), { cwd, env, stdio: ["ignore", "pipe", "pipe"], diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index 72af4d2fd55c..56fb963d9a37 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -209,26 +209,26 @@ function pruneExcessRequestsByAccount(reqs: PairingRequest[], maxPending: number if (maxPending <= 0 || reqs.length <= maxPending) { return { requests: reqs, removed: false }; } - const grouped = new Map(); + const grouped = new Map>(); for (const [index, entry] of reqs.entries()) { const accountId = resolvePairingRequestAccountId(entry); const current = grouped.get(accountId); if (current) { - current.push(index); + current.push({ index, request: entry }); continue; } - grouped.set(accountId, [index]); + grouped.set(accountId, [{ index, request: entry }]); } const droppedIndexes = new Set(); - for (const indexes of grouped.values()) { - if (indexes.length <= maxPending) { + for (const entries of grouped.values()) { + if (entries.length <= maxPending) { continue; } - const sortedIndexes = indexes - .slice() - .toSorted((left, right) => resolveLastSeenAt(reqs[left]) - resolveLastSeenAt(reqs[right])); - for (const index of sortedIndexes.slice(0, sortedIndexes.length - maxPending)) { + const sortedEntries = entries.toSorted( + (left, right) => resolveLastSeenAt(left.request) - resolveLastSeenAt(right.request), + ); + for (const { index } of sortedEntries.slice(0, sortedEntries.length - maxPending)) { droppedIndexes.add(index); } } diff --git a/src/plugin-sdk/allowlist-config-edit.ts b/src/plugin-sdk/allowlist-config-edit.ts index d798d30843f9..d082c800284f 100644 --- a/src/plugin-sdk/allowlist-config-edit.ts +++ b/src/plugin-sdk/allowlist-config-edit.ts @@ -5,6 +5,7 @@ import type { ChannelId } from "../channels/plugins/types.public.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isBlockedObjectKey } from "../infra/prototype-keys.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +import { isRecord } from "../utils.js"; type AllowlistConfigPaths = { readPaths: string[][]; @@ -234,30 +235,32 @@ function ensureNestedObject( } function setNestedValue(root: Record, path: string[], value: unknown) { - if (path.length === 0) { + const leaf = path.at(-1); + if (leaf === undefined) { return; } if (path.length === 1) { - root[path[0]] = value; + root[leaf] = value; return; } const parent = ensureNestedObject(root, path.slice(0, -1)); - parent[path[path.length - 1]] = value; + parent[leaf] = value; } function deleteNestedValue(root: Record, path: string[]) { - if (path.length === 0) { + const leaf = path.at(-1); + if (leaf === undefined) { return; } if (path.length === 1) { - delete root[path[0]]; + delete root[leaf]; return; } const parent = getNestedValue(root, path.slice(0, -1)); - if (!parent || typeof parent !== "object") { + if (!isRecord(parent)) { return; } - delete (parent as Record)[path[path.length - 1]]; + delete parent[leaf]; } function applyAccountScopedAllowlistConfigEdit(params: { diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 79c754a6ba57..024e2a5bac6e 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -1,4 +1,5 @@ // Core SDK contracts expose stable identifiers, manifests, and shared plugin metadata types. +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "../../packages/normalization-core/src/string-coerce.js"; import type { ResolvedConfiguredAcpBinding } from "../acp/persistent-bindings.types.js"; import { buildChatChannelMetaById } from "../channels/chat-meta-shared.js"; @@ -322,12 +323,14 @@ function resolveSdkChatChannelMeta(id: string) { metaById: buildChatChannelMetaById(), }; } + // Optional by design: createChannelPluginBase serves external plugin ids that + // are never in the bundled catalog; their meta comes entirely from params.meta. return cachedSdkChatChannelMeta.metaById[id]; } /** Resolve bundled chat channel metadata while respecting the active bundled-plugin directory. */ export function getChatChannelMeta(id: ChatChannelId): ChannelMeta { - return resolveSdkChatChannelMeta(id); + return expectDefined(resolveSdkChatChannelMeta(id), `chat channel metadata: ${id}`); } /** Remove one of the known provider prefixes from a free-form target string. */ diff --git a/src/plugin-sdk/extension-shared.ts b/src/plugin-sdk/extension-shared.ts index 7c852510dc6f..bb523e806066 100644 --- a/src/plugin-sdk/extension-shared.ts +++ b/src/plugin-sdk/extension-shared.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Extension shared helpers expose cross-plugin runtime utilities that remain SDK-safe. import { createAmbientNodeProxyAgent, hasAmbientNodeProxyConfigured } from "@openclaw/proxyline"; import type { z } from "zod"; @@ -183,7 +184,10 @@ export function formatPluginConfigIssue( return options?.invalidConfigMessage ?? "invalid config"; } if (issue.code === "unrecognized_keys" && issue.keys.length > 0) { - return options?.unknownKeyMessage?.(issue.keys[0]) ?? `unknown config key: ${issue.keys[0]}`; + return ( + options?.unknownKeyMessage?.(expectDefined(issue.keys[0], "keys entry at 0")) ?? + `unknown config key: ${issue.keys[0]}` + ); } if (issue.code === "invalid_type" && issue.path.length === 0) { return options?.rootInvalidTypeMessage ?? "expected config object"; diff --git a/src/plugin-sdk/reply-payload.ts b/src/plugin-sdk/reply-payload.ts index 024b39a626e3..df9f077196ac 100644 --- a/src/plugin-sdk/reply-payload.ts +++ b/src/plugin-sdk/reply-payload.ts @@ -235,20 +235,21 @@ export async function sendPayloadWithChunkedTextAndMedia< if (!text && urls.length === 0) { return params.emptyResult; } - if (urls.length > 0) { + const [firstUrl, ...remainingUrls] = urls; + if (firstUrl !== undefined) { // Caption-limited transports get text only on the first media item; the // final result still represents the last platform send. let lastResult = await params.sendMedia({ ...params.ctx, text, - mediaUrl: urls[0], + mediaUrl: firstUrl, }); await params.onResult?.(lastResult); - for (let i = 1; i < urls.length; i++) { + for (const mediaUrl of remainingUrls) { lastResult = await params.sendMedia({ ...params.ctx, text: "", - mediaUrl: urls[i], + mediaUrl, }); await params.onResult?.(lastResult); } @@ -256,12 +257,17 @@ export async function sendPayloadWithChunkedTextAndMedia< } const limit = params.textChunkLimit; const chunks = limit && params.chunker ? params.chunker(text, limit) : [text]; - let lastResult: TResult; - for (const chunk of chunks) { + const [firstChunk, ...remainingChunks] = chunks; + if (firstChunk === undefined) { + return params.emptyResult; + } + let lastResult = await params.sendText({ ...params.ctx, text: firstChunk }); + await params.onResult?.(lastResult); + for (const chunk of remainingChunks) { lastResult = await params.sendText({ ...params.ctx, text: chunk }); await params.onResult?.(lastResult); } - return lastResult!; + return lastResult; } /** Sends a media sequence with caption text on the first item and returns the last send result. */ diff --git a/src/plugin-sdk/session-transcript-hit.ts b/src/plugin-sdk/session-transcript-hit.ts index 3903ac824b8b..2b943cc51c6a 100644 --- a/src/plugin-sdk/session-transcript-hit.ts +++ b/src/plugin-sdk/session-transcript-hit.ts @@ -1,5 +1,6 @@ // Session transcript hit helpers describe and load matched transcript snippets for plugins. import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.js"; import { uniqueStrings } from "../../packages/normalization-core/src/string-normalization.js"; import { parseUsageCountedSessionIdFromFileName } from "../config/sessions/artifacts.js"; @@ -40,7 +41,9 @@ function restoreQmdNormalizedArchiveName(mdStem: string): string | null { return null; } const [, sessionId, reason, timestamp] = match; - const restoredTimestamp = restoreQmdNormalizedArchiveTimestamp(timestamp); + const restoredTimestamp = restoreQmdNormalizedArchiveTimestamp( + expectDefined(timestamp, "session transcript hit timestamp"), + ); return restoredTimestamp ? `${sessionId}.jsonl.${reason}.${restoredTimestamp}` : null; } diff --git a/src/plugin-sdk/test-helpers/image-fixtures.ts b/src/plugin-sdk/test-helpers/image-fixtures.ts index 4584a9f75e26..232fc94129c0 100644 --- a/src/plugin-sdk/test-helpers/image-fixtures.ts +++ b/src/plugin-sdk/test-helpers/image-fixtures.ts @@ -1,5 +1,6 @@ // Deterministic image buffers for plugin SDK media tests. import { deflateSync } from "node:zlib"; +import { expectDefined } from "@openclaw/normalization-core"; import { encodePngRgb, encodePngRgba } from "../../media/png-encode.js"; type Rgba = { @@ -30,7 +31,9 @@ const CRC_TABLE = (() => { function crc32(buffer: Buffer): number { let crc = 0xffffffff; for (const byte of buffer) { - crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); + crc = + expectDefined(CRC_TABLE[(crc ^ byte) & 0xff], "crc table entry at (crc ^ byte) & 0xff") ^ + (crc >>> 8); } return (crc ^ 0xffffffff) >>> 0; } diff --git a/src/plugin-sdk/test-helpers/temp-home.ts b/src/plugin-sdk/test-helpers/temp-home.ts index 06efeca3b969..e7918eab1f74 100644 --- a/src/plugin-sdk/test-helpers/temp-home.ts +++ b/src/plugin-sdk/test-helpers/temp-home.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js"; import { cleanupSessionStateForTest } from "../../test-utils/session-state-cleanup.js"; @@ -82,7 +83,7 @@ function setTempHome(base: string) { if (!match) { return; } - setTestEnvValue("HOMEDRIVE", match[1]); + setTestEnvValue("HOMEDRIVE", expectDefined(match[1], "temp home regex capture 1")); setTestEnvValue("HOMEPATH", match[2] || "\\"); } diff --git a/src/plugin-state/plugin-state-store.sqlite.ts b/src/plugin-state/plugin-state-store.sqlite.ts index e106d783c0ce..3923f81669a1 100644 --- a/src/plugin-state/plugin-state-store.sqlite.ts +++ b/src/plugin-state/plugin-state-store.sqlite.ts @@ -898,8 +898,7 @@ export function seedPluginStateDatabaseEntriesForTests( const now = Date.now(); runWriteTransaction("register", (store) => { - for (let index = 0; index < entries.length; index += 1) { - const entry = entries[index]; + for (const [index, entry] of entries.entries()) { upsertPluginStateEntry( store.db, bindPluginStateEntry({ diff --git a/src/plugins/contracts/inventory/bundled-capability-metadata.ts b/src/plugins/contracts/inventory/bundled-capability-metadata.ts index 930efa7e86c4..f4688b381f76 100644 --- a/src/plugins/contracts/inventory/bundled-capability-metadata.ts +++ b/src/plugins/contracts/inventory/bundled-capability-metadata.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { expectDefined } from "@openclaw/normalization-core"; import { tryReadJsonSync } from "../../../infra/json-files.js"; import { normalizeBundledPluginStringList, @@ -224,7 +225,11 @@ export const BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS = Object.fromEntries( providerId, manifest.id, ]), - ).toSorted(([left], [right]) => left.localeCompare(right)), + ).toSorted(([left], [right]) => + expectDefined(left, "bundled capability metadata left").localeCompare( + expectDefined(right, "bundled capability metadata right"), + ), + ), ) as Readonly>; type BundledContractIdSnapshotKey = Exclude< diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index 69624accda99..d7006eee9c53 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -1,5 +1,6 @@ // Plugin contract registry assembles bundled plugin fixtures for shared contract tests. import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js"; import { discoverOpenClawPlugins } from "../discovery.js"; @@ -383,7 +384,7 @@ export function requireProviderContractProvider(providerId: string): ProviderPlu ...new Map(entries.map((entry) => [entry.provider.id, entry.provider])).values(), ]; if (pluginIds.length === 1 && pluginScopedProviders.length === 1) { - return pluginScopedProviders[0]; + return expectDefined(pluginScopedProviders[0], "plugin scoped providers entry at 0"); } if (providerContractLoadError) { throw new Error( diff --git a/src/plugins/dependency-denylist.ts b/src/plugins/dependency-denylist.ts index 9026c28cd37a..6a8b5f919890 100644 --- a/src/plugins/dependency-denylist.ts +++ b/src/plugins/dependency-denylist.ts @@ -172,7 +172,9 @@ function collectBlockedOverrideFindings( } const findings: BlockedManifestDependencyFinding[] = []; - for (const overrideKey of Object.keys(value).toSorted()) { + for (const [overrideKey, overrideValue] of Object.entries(value).toSorted(([left], [right]) => + left.localeCompare(right), + )) { const overrideSelectorPackageName = parsePackageNameFromOverrideSelector(overrideKey); if ( overrideSelectorPackageName && @@ -184,7 +186,7 @@ function collectBlockedOverrideFindings( field: "overrides", }); } - findings.push(...collectBlockedOverrideFindings(value[overrideKey], [...path, overrideKey])); + findings.push(...collectBlockedOverrideFindings(overrideValue, [...path, overrideKey])); } return findings; } @@ -209,13 +211,15 @@ export function findBlockedManifestDependencies( if (!dependencyMap) { continue; } - for (const dependencyName of Object.keys(dependencyMap).toSorted()) { + for (const [dependencyName, dependencySpec] of Object.entries(dependencyMap).toSorted( + ([left], [right]) => left.localeCompare(right), + )) { if (isBlockedInstallDependencyPackageName(dependencyName)) { findings.push({ dependencyName, field }); continue; } - const aliasTargetPackageName = parseNpmAliasTargetPackageName(dependencyMap[dependencyName]); + const aliasTargetPackageName = parseNpmAliasTargetPackageName(dependencySpec); if (!aliasTargetPackageName) { continue; } diff --git a/src/plugins/hook-runner-global-state.ts b/src/plugins/hook-runner-global-state.ts index 403f67e7e9d9..5c61c5f302da 100644 --- a/src/plugins/hook-runner-global-state.ts +++ b/src/plugins/hook-runner-global-state.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Internal state and composed-registry view for the global hook runner. import { resolveGlobalSingleton } from "../shared/global-singleton.js"; import type { GlobalHookRunnerRegistry } from "./hook-registry.types.js"; @@ -89,7 +90,12 @@ function composeLiveHookRegistry( // registration that still carries a fail-closed tool-call gate. sources.forEach((registry, index) => { for (const plugin of registry.plugins) { - if (plugin.status === "loaded" && hookPluginIdsBySource[index].has(plugin.id)) { + if ( + plugin.status === "loaded" && + expectDefined(hookPluginIdsBySource[index], "hook plugin ids by source entry at index").has( + plugin.id, + ) + ) { claimOwner(plugin.id, index); } } @@ -133,7 +139,13 @@ function composeLiveHookRegistry( }); sources.forEach((registry, index) => { for (const plugin of registry.plugins) { - if (plugin.status === "loaded" && trustedPolicyPluginIdsBySource[index].has(plugin.id)) { + if ( + plugin.status === "loaded" && + expectDefined( + trustedPolicyPluginIdsBySource[index], + "trusted policy plugin ids by source entry at index", + ).has(plugin.id) + ) { claimPolicyOwner(plugin.id, index); } } diff --git a/src/plugins/installs.ts b/src/plugins/installs.ts index 73a4f7bb934a..89f88b5bde9c 100644 --- a/src/plugins/installs.ts +++ b/src/plugins/installs.ts @@ -52,22 +52,20 @@ export function recordPluginInstall( ): OpenClawConfig { const { pluginId, ...record } = update; const previous = clearStaleInstallRecordFields(cfg.plugins?.installs?.[pluginId]); - const installs = { - ...cfg.plugins?.installs, - [pluginId]: { - ...previous, - ...record, - installedAt: record.installedAt ?? new Date().toISOString(), - }, + const nextRecord = { + ...previous, + ...record, + installedAt: record.installedAt ?? new Date().toISOString(), }; return { ...cfg, plugins: { + // cfg.plugins may be absent on first install; spreading undefined is {}. ...cfg.plugins, installs: { - ...installs, - [pluginId]: installs[pluginId], + ...cfg.plugins?.installs, + [pluginId]: nextRecord, }, }, }; diff --git a/src/plugins/plugin-registry-snapshot.ts b/src/plugins/plugin-registry-snapshot.ts index eecff840a1d2..ed40a0d34a76 100644 --- a/src/plugins/plugin-registry-snapshot.ts +++ b/src/plugins/plugin-registry-snapshot.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { tryReadJsonSync } from "../infra/json-files.js"; import { resolveUserPath } from "../utils.js"; import { resolveCompatibilityHostVersion } from "../version.js"; @@ -203,7 +204,11 @@ function directoryChildFingerprint(directoryPath: string): unknown { return fs .readdirSync(directoryPath, { withFileTypes: true }) .map((entry) => [entry.name, entry.isDirectory() ? "dir" : entry.isFile() ? "file" : "other"]) - .toSorted(([left], [right]) => left.localeCompare(right)); + .toSorted(([left], [right]) => + expectDefined(left, "plugin registry snapshot left").localeCompare( + expectDefined(right, "plugin registry snapshot right"), + ), + ); } catch { return "unreadable"; } diff --git a/src/plugins/provider-auth-input.ts b/src/plugins/provider-auth-input.ts index 706d8d59d51e..8578c5363c8d 100644 --- a/src/plugins/provider-auth-input.ts +++ b/src/plugins/provider-auth-input.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Normalizes provider auth input metadata collected from plugin setup flows. */ import { normalizeOptionalLowercaseString, @@ -41,7 +42,9 @@ export function normalizeApiKeyInput(raw: string): string { const assignmentMatch = normalizedPaste.match( /^(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.+)$/, ); - const valuePart = assignmentMatch ? assignmentMatch[1].trim() : normalizedPaste; + const valuePart = assignmentMatch + ? expectDefined(assignmentMatch[1], "assignment match capture group 1").trim() + : normalizedPaste; const withoutSemicolon = valuePart.endsWith(";") ? valuePart.slice(0, -1).trim() : valuePart; const unquoted = diff --git a/src/plugins/provider-wizard.ts b/src/plugins/provider-wizard.ts index 1189a09e57e0..7383f5925866 100644 --- a/src/plugins/provider-wizard.ts +++ b/src/plugins/provider-wizard.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** Provider setup wizard helpers shared by provider plugins and CLI setup flows. */ import { normalizeOptionalLowercaseString, @@ -290,7 +291,10 @@ export function resolveProviderPluginChoice(params: { normalizeProviderId(provider.id) === normalizeProviderId(choice) && provider.auth.length > 0 ) { - return { provider, method: provider.auth[0] }; + return { + provider, + method: expectDefined(provider.auth[0], "auth entry at 0"), + }; } } diff --git a/src/plugins/runtime/runtime-plugin-boundary.ts b/src/plugins/runtime/runtime-plugin-boundary.ts index eee78779d318..4c6c74e7342a 100644 --- a/src/plugins/runtime/runtime-plugin-boundary.ts +++ b/src/plugins/runtime/runtime-plugin-boundary.ts @@ -1,6 +1,7 @@ // Runtime plugin boundary helpers enforce package and source boundaries for runtime loading. import fs from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { getRuntimeConfig } from "../../config/config.js"; import { loadPluginManifestRegistry } from "../manifest-registry.js"; import { @@ -79,7 +80,7 @@ export function resolvePluginRuntimeRecordByEntryBaseNames( `plugin runtime boundary is ambiguous for entries [${entryBaseNames.join(", ")}]: ${pluginIds}`, ); } - const record = matches[0]; + const record = expectDefined(matches[0], "matches capture group 0"); return { ...(record.origin ? { origin: record.origin } : {}), rootDir: record.rootDir, diff --git a/src/poll-params.ts b/src/poll-params.ts index 5b12f9fbb672..c3b315b20be8 100644 --- a/src/poll-params.ts +++ b/src/poll-params.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Parses poll command parameters into validated polling options. import { readSnakeCaseParamRaw } from "./param-key.js"; @@ -37,7 +38,10 @@ const CONTENT_BEARING_SHARED_POLL_PARAM_NAMES = ["pollQuestion", "pollOption"] a function hasContentBearingPollCreationParam(params: Record): boolean { for (const key of CONTENT_BEARING_SHARED_POLL_PARAM_NAMES) { - const def = POLL_CREATION_PARAM_DEFS[key]; + const def = expectDefined( + POLL_CREATION_PARAM_DEFS[key], + "poll creation param defs entry at key", + ); const value = readPollParamRaw(params, key); if (def.kind === "string" && typeof value === "string" && value.trim().length > 0) { return true; diff --git a/src/process/exec.ts b/src/process/exec.ts index 92bb371059be..10527732c3cf 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -5,6 +5,7 @@ import path from "node:path"; import process from "node:process"; import { StringDecoder } from "node:string_decoder"; import { promisify } from "node:util"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { danger, shouldLogVerbose } from "../globals.js"; import { markOpenClawExecEnv } from "../infra/openclaw-exec-env.js"; @@ -71,10 +72,9 @@ function resolveNpmArgvForWindows(argv: string[]): string[] | null { if (process.platform !== "win32" || argv.length === 0) { return null; } - const basename = normalizeLowercaseStringOrEmpty(path.basename(argv[0])).replace( - /\.(cmd|exe|bat)$/, - "", - ); + const basename = normalizeLowercaseStringOrEmpty( + path.basename(expectDefined(argv[0], "argv entry at 0")), + ).replace(/\.(cmd|exe|bat)$/, ""); const cliName = basename === "npx" ? "npx-cli.js" : basename === "npm" ? "npm-cli.js" : null; if (!cliName) { return null; @@ -278,7 +278,7 @@ function appendCapturedOutput( capture.chunks.push(buffer); capture.bytes += buffer.byteLength; while (capture.bytes > maxBytes && capture.chunks.length > 0) { - const first = capture.chunks[0]; + const first = expectDefined(capture.chunks[0], "chunks entry at 0"); const overflow = capture.bytes - maxBytes; if (first.byteLength <= overflow) { capture.chunks.shift(); diff --git a/src/process/spawn-utils.ts b/src/process/spawn-utils.ts index 6fa195fcf3bb..7d57d4c0bb57 100644 --- a/src/process/spawn-utils.ts +++ b/src/process/spawn-utils.ts @@ -1,6 +1,7 @@ // Spawn utilities configure child processes and normalize spawned process handles. import type { ChildProcess, SpawnOptions } from "node:child_process"; import { spawn } from "node:child_process"; +import { expectDefined } from "@openclaw/normalization-core"; import { toErrorObject } from "../infra/errors.js"; type SpawnFallback = { @@ -44,7 +45,7 @@ async function spawnAndWaitForSpawn( argv: string[], options: SpawnOptions, ): Promise { - const child = spawnImpl(argv[0], argv.slice(1), options); + const child = spawnImpl(expectDefined(argv[0], "argv entry at 0"), argv.slice(1), options); return await new Promise((resolve, reject) => { let settled = false; @@ -98,8 +99,7 @@ export async function spawnWithFallback( ]; let lastError: unknown; - for (let index = 0; index < attempts.length; index += 1) { - const attempt = attempts[index]; + for (const [index, attempt] of attempts.entries()) { try { const child = await spawnAndWaitForSpawn(spawnImpl, params.argv, attempt.options); return { diff --git a/src/process/supervisor/supervisor.ts b/src/process/supervisor/supervisor.ts index 984c3378b903..f8c57d165bec 100644 --- a/src/process/supervisor/supervisor.ts +++ b/src/process/supervisor/supervisor.ts @@ -1,6 +1,7 @@ // Process supervisor manages long-running child and PTY process lifecycles. import crypto from "node:crypto"; import { performance } from "node:perf_hooks"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { getShellConfig } from "../../agents/shell-utils.js"; @@ -87,7 +88,7 @@ function resolveElapsedTimeoutReason(params: { return null; } elapsedDeadlines.sort((a, b) => a.deadlineMs - b.deadlineMs); - return elapsedDeadlines[0].reason; + return expectDefined(elapsedDeadlines[0], "elapsed deadlines entry at 0").reason; } export function createProcessSupervisor(): ProcessSupervisor { diff --git a/src/routing/bindings.ts b/src/routing/bindings.ts index 2bec9e11f498..fb702ec040d8 100644 --- a/src/routing/bindings.ts +++ b/src/routing/bindings.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Routing binding helpers resolve configured channel and agent route bindings. import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { listRouteBindings } from "../config/bindings.js"; @@ -80,7 +81,7 @@ export function resolvePreferredAccountId(params: { boundAccounts: string[]; }): string { if (params.boundAccounts.length > 0) { - return params.boundAccounts[0]; + return expectDefined(params.boundAccounts[0], "bound accounts entry at 0"); } return params.defaultAccountId; } diff --git a/src/secrets/resolve.ts b/src/secrets/resolve.ts index c350366ffd23..6930f02548cd 100644 --- a/src/secrets/resolve.ts +++ b/src/secrets/resolve.ts @@ -2,6 +2,7 @@ import { spawn } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { @@ -624,7 +625,7 @@ function parseExecValues(params: { try { parsed = JSON.parse(trimmed) as unknown; } catch { - return { [params.ids[0]]: trimmed }; + return { [expectDefined(params.ids[0], "ids entry at 0")]: trimmed }; } } else { try { @@ -640,7 +641,7 @@ function parseExecValues(params: { if (!isRecord(parsed)) { if (!params.jsonOnly && params.ids.length === 1 && typeof parsed === "string") { - return { [params.ids[0]]: parsed }; + return { [expectDefined(params.ids[0], "ids entry at 0")]: parsed }; } throw providerResolutionError({ source: "exec", @@ -952,7 +953,7 @@ export async function resolveSecretRefValues( }); } const providerConfig = resolveConfiguredProvider({ - ref: group.refs[0], + ref: expectDefined(group.refs[0], "refs entry at 0"), config: options.config, env: options.env ?? process.env, manifestRegistry: options.manifestRegistry, diff --git a/src/secrets/runtime-web-tools.shared.ts b/src/secrets/runtime-web-tools.shared.ts index 37d1fe324060..6ca87c4b1a6c 100644 --- a/src/secrets/runtime-web-tools.shared.ts +++ b/src/secrets/runtime-web-tools.shared.ts @@ -12,6 +12,7 @@ import type { import { pushInactiveSurfaceWarning, pushWarning } from "./runtime-shared.js"; import type { RuntimeWebDiagnostic, RuntimeWebDiagnosticCode } from "./runtime-web-tools.types.js"; export { isRecord } from "./shared.js"; +import { expectDefined } from "@openclaw/normalization-core"; import { isRecord } from "./shared.js"; const loadResolveManifestContractOwnerPluginId = createLazyRuntimeNamedExport( @@ -598,7 +599,9 @@ export async function resolveRuntimeWebProviderSelection< } } else { if (!selectedProvider && unresolvedWithoutFallback.length > 0) { - failUnresolvedNoFallback(unresolvedWithoutFallback[0]); + failUnresolvedNoFallback( + expectDefined(unresolvedWithoutFallback[0], "unresolved without fallback entry at 0"), + ); } if (selectedProvider) { diff --git a/src/security/audit-extra.sync.ts b/src/security/audit-extra.sync.ts index 82dead75f5fc..24eaf7673ec5 100644 --- a/src/security/audit-extra.sync.ts +++ b/src/security/audit-extra.sync.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Runs synchronous extra security audit checks. import { normalizeOptionalLowercaseString, @@ -309,17 +310,21 @@ function editDistance(a: string, b: string): number { const dp: number[] = Array.from({ length: b.length + 1 }, (_, j) => j); for (let i = 1; i <= a.length; i++) { - let prev = dp[0]; + let prev = expectDefined(dp[0], "dp entry at 0"); dp[0] = i; for (let j = 1; j <= b.length; j++) { const temp = dp[j]; const cost = a[i - 1] === b[j - 1] ? 0 : 1; - dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + cost); - prev = temp; + dp[j] = Math.min( + expectDefined(dp[j], "dp entry at j") + 1, + expectDefined(dp[j - 1], "dp entry at j 1") + 1, + prev + cost, + ); + prev = expectDefined(temp, "audit extra.sync temp"); } } - return dp[b.length]; + return expectDefined(dp[b.length], "dp entry at b.length"); } function suggestKnownNodeCommands(unknown: string, known: Set): string[] { diff --git a/src/security/dm-policy-shared.ts b/src/security/dm-policy-shared.ts index 1730c9e28b31..87e0c592bdc2 100644 --- a/src/security/dm-policy-shared.ts +++ b/src/security/dm-policy-shared.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Shares direct-message policy normalization for channel audits. import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { resolveGroupAllowFromSources } from "../channels/allow-from.js"; @@ -32,7 +33,9 @@ export function resolvePinnedMainDmOwnerFromAllowlist(params: { .filter((entry): entry is string => Boolean(entry)), ), ); - return normalizedOwners.length === 1 ? normalizedOwners[0] : null; + return normalizedOwners.length === 1 + ? expectDefined(normalizedOwners[0], "normalized owners entry at 0") + : null; } /** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */ diff --git a/src/security/external-content.ts b/src/security/external-content.ts index a6e9cc8ab8d8..9bbd1cd0efe3 100644 --- a/src/security/external-content.ts +++ b/src/security/external-content.ts @@ -225,7 +225,7 @@ function foldMarkerTextWithIndexMap(input: string): FoldedMarkerMatch { const originalEndByFoldedIndex: number[] = []; for (let index = 0; index < input.length; index += 1) { - const char = input[index]; + const char = input.charAt(index); if (isMarkerIgnorableChar(char)) { continue; } diff --git a/src/security/safe-regex.ts b/src/security/safe-regex.ts index ee1d57f04261..9dd5500f61f4 100644 --- a/src/security/safe-regex.ts +++ b/src/security/safe-regex.ts @@ -1,4 +1,5 @@ // Performs lightweight safe-regex checks for user-supplied patterns. +import { expectDefined } from "@openclaw/normalization-core"; type QuantifierRead = { consumed: number; minRepeat: number; @@ -102,7 +103,7 @@ function readQuantifier(source: string, index: number): QuantifierRead | null { } let i = index + 1; - while (i < source.length && /\d/.test(source[i])) { + while (i < source.length && /\d/.test(source.charAt(i))) { i += 1; } if (i === index + 1) { @@ -114,7 +115,7 @@ function readQuantifier(source: string, index: number): QuantifierRead | null { if (source[i] === ",") { i += 1; const maxStart = i; - while (i < source.length && /\d/.test(source[i])) { + while (i < source.length && /\d/.test(source.charAt(i))) { i += 1; } maxRepeat = i === maxStart ? null : Number.parseInt(source.slice(maxStart, i), 10); @@ -196,7 +197,7 @@ function analyzeTokensForNestedRepetition(tokens: PatternToken[]): boolean { const frames: ParseFrame[] = [createParseFrame()]; const emitToken = (token: TokenState) => { - const frame = frames[frames.length - 1]; + const frame = expectDefined(frames[frames.length - 1], "frames entry at frames.length 1"); frame.lastToken = token; if (token.containsRepetition) { frame.containsRepetition = true; @@ -252,7 +253,7 @@ function analyzeTokensForNestedRepetition(tokens: PatternToken[]): boolean { } if (token.kind === "alternation") { - const frame = frames[frames.length - 1]; + const frame = expectDefined(frames[frames.length - 1], "frames entry at frames.length 1"); frame.hasAlternation = true; recordAlternative(frame); frame.branchMinLength = 0; @@ -261,7 +262,7 @@ function analyzeTokensForNestedRepetition(tokens: PatternToken[]): boolean { continue; } - const frame = frames[frames.length - 1]; + const frame = expectDefined(frames[frames.length - 1], "frames entry at frames.length 1"); const previousToken = frame.lastToken; if (!previousToken) { continue; diff --git a/src/sessions/session-id-resolution.ts b/src/sessions/session-id-resolution.ts index 88e8def7d4fe..c9cf64f8247e 100644 --- a/src/sessions/session-id-resolution.ts +++ b/src/sessions/session-id-resolution.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Session id resolution helpers resolve user-provided session references. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { SessionEntry } from "../config/sessions.js"; @@ -67,11 +68,11 @@ function collapseAliasMatches(matches: NormalizedSessionIdMatch[]): NormalizedSe return Array.from(grouped.values(), (group) => { if (group.length === 1) { - return group[0]; + return expectDefined(group[0], "normalized session id match"); } // Aliases that normalize to the same request key represent one session. // Prefer freshest canonical key so ambiguity only reports distinct sessions. - return [...group].toSorted((a, b) => { + const sorted = [...group].toSorted((a, b) => { const timeDiff = compareNormalizedUpdatedAtDescending(a, b); if (timeDiff !== 0) { return timeDiff; @@ -80,7 +81,8 @@ function collapseAliasMatches(matches: NormalizedSessionIdMatch[]): NormalizedSe return a.isCanonicalSessionKey ? -1 : 1; } return compareStoreKeys(a.normalizedSessionKey, b.normalizedSessionKey); - })[0]; + }); + return expectDefined(sorted[0], "freshest normalized session id match"); }); } @@ -112,7 +114,11 @@ export function resolveSessionIdMatchSelection( normalizeSessionIdMatches(matches, normalizeLowercaseStringOrEmpty(sessionId)), ); if (canonicalMatches.length === 1) { - return { kind: "selected", sessionKey: canonicalMatches[0].sessionKey }; + return { + kind: "selected", + sessionKey: expectDefined(canonicalMatches[0], "canonical matches capture group 0") + .sessionKey, + }; } const structuralMatches = canonicalMatches.filter((match) => match.isStructural); diff --git a/src/shared/assistant-error-format.ts b/src/shared/assistant-error-format.ts index 6e6659f16451..53ee03a30d54 100644 --- a/src/shared/assistant-error-format.ts +++ b/src/shared/assistant-error-format.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Assistant error formatting helpers normalize assistant-visible error payloads. import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; const ERROR_PAYLOAD_PREFIX_RE = @@ -160,7 +161,7 @@ export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null { const httpPrefixMatch = candidate.match(/^(\d{3})\s+(.+)$/s); if (httpPrefixMatch) { httpCode = httpPrefixMatch[1]; - candidate = httpPrefixMatch[2].trim(); + candidate = expectDefined(httpPrefixMatch[2], "http prefix match capture group 2").trim(); } const payload = parseApiErrorPayload(candidate); @@ -234,7 +235,7 @@ export function formatRawAssistantErrorForUi(raw?: string): string { const httpMatch = trimmed.match(HTTP_STATUS_PREFIX_RE); if (httpMatch) { - const rest = httpMatch[2].trim(); + const rest = expectDefined(httpMatch[2], "http match capture group 2").trim(); if (!rest.startsWith("{")) { return `HTTP ${httpMatch[1]}: ${rest}`; } diff --git a/src/shared/human-list.ts b/src/shared/human-list.ts index aeedf61a8f0a..dbd971c0649f 100644 --- a/src/shared/human-list.ts +++ b/src/shared/human-list.ts @@ -1,10 +1,10 @@ -/** Formats a short human-readable disjunction such as "A, B, or C". */ +import { expectDefined } from "@openclaw/normalization-core"; /** Formats a short human-readable disjunction such as "A, B, or C". */ export function formatHumanList(values: readonly string[]): string { if (values.length === 0) { return ""; } if (values.length === 1) { - return values[0]; + return expectDefined(values[0], "values entry at 0"); } if (values.length === 2) { return `${values[0]} or ${values[1]}`; diff --git a/src/shared/levenshtein-distance.ts b/src/shared/levenshtein-distance.ts index 7ca9f72b61e5..77ebf7f4df3a 100644 --- a/src/shared/levenshtein-distance.ts +++ b/src/shared/levenshtein-distance.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; export function levenshteinDistance(left: string, right: string): number { if (left === right) { return 0; @@ -19,14 +20,14 @@ export function levenshteinDistance(left: string, right: string): number { for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) { const cost = left[leftIndex] === right[rightIndex] ? 0 : 1; current[rightIndex + 1] = Math.min( - current[rightIndex] + 1, - previous[rightIndex + 1] + 1, - previous[rightIndex] + cost, + expectDefined(current[rightIndex], "current entry at right index") + 1, + expectDefined(previous[rightIndex + 1], "previous entry at right index + 1") + 1, + expectDefined(previous[rightIndex], "previous entry at right index") + cost, ); } const nextPrevious = current; current = previous; previous = nextPrevious; } - return previous[right.length]; + return expectDefined(previous[right.length], "previous entry at right.length"); } diff --git a/src/shared/text-chunking.ts b/src/shared/text-chunking.ts index 0f7800e520a3..76142b2aa19e 100644 --- a/src/shared/text-chunking.ts +++ b/src/shared/text-chunking.ts @@ -48,7 +48,8 @@ export function chunkTextByBreakResolver( } // Keep separator ownership with the boundary: one matched separator is // consumed here, and any adjacent whitespace is trimmed before the next window. - const brokeOnSeparator = safeBreakIdx < remaining.length && /\s/.test(remaining[safeBreakIdx]); + const brokeOnSeparator = + safeBreakIdx < remaining.length && /\s/.test(remaining.charAt(safeBreakIdx)); const nextStart = Math.min(remaining.length, safeBreakIdx + (brokeOnSeparator ? 1 : 0)); remaining = remaining.slice(nextStart).trimStart(); } diff --git a/src/shared/text/assistant-visible-text.ts b/src/shared/text/assistant-visible-text.ts index 55f1bc87e640..1ee52d29be94 100644 --- a/src/shared/text/assistant-visible-text.ts +++ b/src/shared/text/assistant-visible-text.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Assistant visible text helpers strip hidden reasoning and control marker text. import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { stripPlainTextToolCallBlocks } from "../../../packages/tool-call-repair/src/index.js"; @@ -96,7 +97,7 @@ function parseXmlTagAt(text: string, start: number): ParsedToolCallTag | null { } let cursor = start + 1; - while (cursor < text.length && /\s/.test(text[cursor])) { + while (cursor < text.length && /\s/.test(text.charAt(cursor))) { cursor += 1; } @@ -104,7 +105,7 @@ function parseXmlTagAt(text: string, start: number): ParsedToolCallTag | null { if (text[cursor] === "/") { isClose = true; cursor += 1; - while (cursor < text.length && /\s/.test(text[cursor])) { + while (cursor < text.length && /\s/.test(text.charAt(cursor))) { cursor += 1; } } @@ -114,7 +115,7 @@ function parseXmlTagAt(text: string, start: number): ParsedToolCallTag | null { return null; } cursor += 1; - while (cursor < text.length && /[A-Za-z0-9_.:-]/.test(text[cursor])) { + while (cursor < text.length && /[A-Za-z0-9_.:-]/.test(text.charAt(cursor))) { cursor += 1; } @@ -201,7 +202,7 @@ function startsWithNestedJsonToolCallPayload(text: string, start: number): boole return false; } let cursor = start; - while (cursor < text.length && /\s/.test(text[cursor])) { + while (cursor < text.length && /\s/.test(text.charAt(cursor))) { cursor += 1; } const nestedTag = parseToolCallTagAt(text, cursor); @@ -235,7 +236,7 @@ function isLikelyStandaloneFunctionToolCall( idx -= 1; } - return idx < 0 || text[idx] === "\n" || text[idx] === "\r" || /[.!?:]/.test(text[idx]); + return idx < 0 || text[idx] === "\n" || text[idx] === "\r" || /[.!?:]/.test(text.charAt(idx)); } function isStandaloneOpeningTagLine( @@ -320,7 +321,7 @@ function findAdjacentOpeningToolCallTag( tagName: string, ): ParsedToolCallTag | null { let idx = start; - while (idx < text.length && /\s/.test(text[idx])) { + while (idx < text.length && /\s/.test(text.charAt(idx))) { idx += 1; } if (text[idx] !== "<") { @@ -366,7 +367,7 @@ function isDanglingFunctionParameterParent(text: string, tag: ParsedToolCallTag) return false; } let cursor = tag.end; - while (cursor < text.length && /\s/.test(text[cursor])) { + while (cursor < text.length && /\s/.test(text.charAt(cursor))) { cursor += 1; } const nextTag = parseXmlTagAt(text, cursor); @@ -425,7 +426,7 @@ function unwrapStandaloneParameterTags(text: string): string { if (tag.isClose) { const openIndex = openTags.findLastIndex((entry) => entry.name === tag.tagName); if (openIndex !== -1) { - const opening = openTags[openIndex]; + const opening = expectDefined(openTags[openIndex], "open tags entry at open index"); if (opening.unwrap) { const contentEnd = opening.trimBoundaryLineBreaks && diff --git a/src/shared/text/code-regions.ts b/src/shared/text/code-regions.ts index ad99b2044ff8..5cec81dd03e5 100644 --- a/src/shared/text/code-regions.ts +++ b/src/shared/text/code-regions.ts @@ -1,4 +1,5 @@ // Code region helpers find fenced and inline code spans in Markdown text. +import { expectDefined } from "@openclaw/normalization-core"; export interface CodeRegion { start: number; end: number; @@ -10,8 +11,12 @@ export function findCodeRegions(text: string): CodeRegion[] { const fencedRe = /(^|\n)(```|~~~)[^\n]*\n[\s\S]*?(?:\n\2|$)/g; for (const match of text.matchAll(fencedRe)) { - const start = (match.index ?? 0) + match[1].length; - regions.push({ start, end: start + match[0].length - match[1].length }); + const start = + (match.index ?? 0) + expectDefined(match[1], "code regions regex capture 1").length; + regions.push({ + start, + end: start + match[0].length - expectDefined(match[1], "code regions regex capture 1").length, + }); } const inlineRe = /`+[^`]+`+/g; diff --git a/src/shared/text/reasoning-tags.ts b/src/shared/text/reasoning-tags.ts index b8c59f672f5e..2634aad9ddd9 100644 --- a/src/shared/text/reasoning-tags.ts +++ b/src/shared/text/reasoning-tags.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Reasoning tag helpers find and remove model reasoning tag blocks from text. import { findCodeRegions, isInsideCode } from "./code-regions.js"; import { findFinalTagMatches } from "./final-tags.js"; @@ -89,7 +90,7 @@ export function stripReasoningTagsFromText( } for (let i = finalMatches.length - 1; i >= 0; i--) { - const m = finalMatches[i]; + const m = expectDefined(finalMatches[i], "final matches capture group i"); if (!m.inCode) { cleaned = cleaned.slice(0, m.start) + cleaned.slice(m.start + m.length); } diff --git a/src/skills/security/scanner.ts b/src/skills/security/scanner.ts index 79b9b4e8a346..7c31cdf436f5 100644 --- a/src/skills/security/scanner.ts +++ b/src/skills/security/scanner.ts @@ -1,6 +1,7 @@ // Skill security scanner inspects skill files and manifests for unsafe patterns. import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { hasErrnoCode } from "../../infra/errors.js"; import { isPathInside } from "../../security/scan-paths.js"; @@ -409,8 +410,7 @@ export function scanSource(source: string, filePath: string): SkillScanFinding[] continue; } - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + for (const [i, line] of lines.entries()) { const match = rule.pattern.exec(line); if (!match) { continue; @@ -422,7 +422,7 @@ export function scanSource(source: string, filePath: string): SkillScanFinding[] // Special handling for suspicious-network: check port if (rule.ruleId === "suspicious-network") { - const port = Number.parseInt(match[1], 10); + const port = Number.parseInt(expectDefined(match[1], "scanner regex capture 1"), 10); if (STANDARD_PORTS.has(port)) { continue; } diff --git a/src/skills/workshop/service.ts b/src/skills/workshop/service.ts index 05cef237e3f4..aec794b851aa 100644 --- a/src/skills/workshop/service.ts +++ b/src/skills/workshop/service.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { readLocalFileSafely, root, walkDirectory } from "../../infra/fs-safe.js"; @@ -231,7 +232,10 @@ export async function resolvePendingSkillProposal(input: { .join(", "); throw new Error(`Multiple pending skill proposals matched ${name}: ${candidates}`); } - const matched = await readRequiredProposal(matches[0].id, input.workspaceDir); + const matched = await readRequiredProposal( + expectDefined(matches[0], "matches capture group 0").id, + input.workspaceDir, + ); if (matched.record.status !== "pending") { throw new Error( `Only pending proposals can be revised. Current status: ${matched.record.status}.`, diff --git a/src/status/agent-runtime-label.ts b/src/status/agent-runtime-label.ts index 2355da56e3c8..7096cdc19f5c 100644 --- a/src/status/agent-runtime-label.ts +++ b/src/status/agent-runtime-label.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Agent runtime label helpers format provider, model, and runtime labels. import { normalizeOptionalLowercaseString, @@ -55,5 +56,5 @@ export function resolveAgentRuntimeLabel(args: { ); } - return AGENT_RUNTIME_LABELS.openclaw; + return expectDefined(AGENT_RUNTIME_LABELS.openclaw, "OpenClaw runtime label"); } diff --git a/src/talk/audio-codec.ts b/src/talk/audio-codec.ts index 8470530288a3..5560f5a6b751 100644 --- a/src/talk/audio-codec.ts +++ b/src/talk/audio-codec.ts @@ -1,4 +1,4 @@ -/** +import { expectDefined } from "@openclaw/normalization-core"; /** * PCM resampling and G.711 mu-law conversion helpers for Talk audio bridges. * * Telephony providers generally expect 8 kHz mu-law frames, while local audio @@ -189,8 +189,10 @@ export function resamplePcm( ? sampleBandlimitedWithCoefficients( inputView, Math.floor((i * inputSampleRate) / outputSampleRate), - kernel.coefficients[(i * kernel.inputStep) % kernel.phaseCount] ?? - kernel.coefficients[0], + expectDefined( + kernel.coefficients[(i * kernel.inputStep) % kernel.phaseCount], + "coefficients entry at (i * kernel.input step) % kernel.phase count", + ) ?? kernel.coefficients[0], ) : sampleBandlimited(inputView, i * ratio, cutoffCyclesPerSample), ); diff --git a/src/test-utils/chunk-test-helpers.ts b/src/test-utils/chunk-test-helpers.ts index 43cc1f2a5f88..d394641ec528 100644 --- a/src/test-utils/chunk-test-helpers.ts +++ b/src/test-utils/chunk-test-helpers.ts @@ -1,4 +1,4 @@ -/** Markdown chunk helpers shared by prompt and streaming tests. */ +import { expectDefined } from "@openclaw/normalization-core"; /** Markdown chunk helpers shared by prompt and streaming tests. */ export function countLines(text: string): number { return text.split("\n").length; } @@ -10,9 +10,9 @@ export function hasBalancedFences(chunk: string): boolean { if (!match) { continue; } - const marker = match[2]; + const marker = expectDefined(match[2], "chunk test helpers regex capture 2"); if (!open) { - open = { markerChar: marker[0], markerLen: marker.length }; + open = { markerChar: marker.charAt(0), markerLen: marker.length }; continue; } if (open.markerChar === marker[0] && marker.length >= open.markerLen) { diff --git a/src/test-utils/temp-home.ts b/src/test-utils/temp-home.ts index 407c94fea5fd..0c225670ed8f 100644 --- a/src/test-utils/temp-home.ts +++ b/src/test-utils/temp-home.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { captureEnv, setTestEnvValue } from "./env.js"; import { cleanupSessionStateForTest } from "./session-state-cleanup.js"; @@ -59,7 +60,7 @@ export async function createTempHomeEnv(prefix: string): Promise { if (process.platform === "win32") { const match = home.match(/^([A-Za-z]:)(.*)$/); if (match) { - setTestEnvValue("HOMEDRIVE", match[1]); + setTestEnvValue("HOMEDRIVE", expectDefined(match[1], "temp home regex capture 1")); setTestEnvValue("HOMEPATH", match[2] || "\\"); } } diff --git a/src/test-utils/tracked-temp-dirs.ts b/src/test-utils/tracked-temp-dirs.ts index 98284430569f..e30dff15b0c3 100644 --- a/src/test-utils/tracked-temp-dirs.ts +++ b/src/test-utils/tracked-temp-dirs.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; /** Allocates temp directories under reusable roots with explicit cleanup control. */ export function createTrackedTempDirs() { @@ -58,7 +59,7 @@ export function createTrackedTempDirs() { ); await Promise.all( roots.flatMap((dir, i) => - dirlists[i].map((entry) => + expectDefined(dirlists[i], "dirlists entry at i").map((entry) => fs.rm(path.join(dir, entry), { recursive: true, force: true }), ), ), diff --git a/src/tui/osc8-hyperlinks.ts b/src/tui/osc8-hyperlinks.ts index 9f67f2b717d3..5e47b1d56ee8 100644 --- a/src/tui/osc8-hyperlinks.ts +++ b/src/tui/osc8-hyperlinks.ts @@ -1,4 +1,5 @@ // Regex patterns for ANSI escape sequences (constructed from strings to +import { expectDefined } from "@openclaw/normalization-core"; // satisfy the no-control-regex lint rule). const SGR_PATTERN = "\\x1b\\[[0-9;]*m"; const OSC8_PATTERN = "\\x1b\\]8;;.*?(?:\\x07|\\x1b\\\\)"; @@ -32,7 +33,10 @@ function trimUnbalancedTrailingParens(url: string): string { } function hasUrlContent(url: string): boolean { - const authority = url.slice(url.indexOf("://") + 3).split(/[/?#]/, 1)[0]; + const authority = expectDefined( + url.slice(url.indexOf("://") + 3).split(/[/?#]/, 1)[0], + 'url.slice(url.index of("://") + 3).split(/[/?#]/, 1) entry at 0', + ); return /[\p{L}\p{N}]/u.test(authority) || /^\[[0-9a-f:.]+\](?::\d+)?$/i.test(authority); } @@ -50,8 +54,8 @@ export function extractUrls(markdown: string): string[] { ); let m: RegExpExecArray | null; while ((m = mdLinkRe.exec(markdown)) !== null) { - if (hasUrlContent(m[1])) { - urls.add(m[1]); + if (hasUrlContent(expectDefined(m[1], "m capture group 1"))) { + urls.add(expectDefined(m[1], "m capture group 1")); } } @@ -284,7 +288,12 @@ export function addOsc8Hyperlinks(lines: string[], urls: string[]): string[] { const visibleLines = lines.map(stripAnsi); return lines.map((line, index) => { - const result = findUrlRanges(visibleLines[index], urls, pending, visibleLines[index + 1]); + const result = findUrlRanges( + expectDefined(visibleLines[index], "visible lines entry at index"), + urls, + pending, + visibleLines[index + 1], + ); pending = result.pending; return applyOsc8Ranges(line, result.ranges); }); diff --git a/src/tui/theme/theme.ts b/src/tui/theme/theme.ts index 37679d665d27..6396cc654daf 100644 --- a/src/tui/theme/theme.ts +++ b/src/tui/theme/theme.ts @@ -5,6 +5,7 @@ import type { SelectListTheme, SettingsListTheme, } from "@earendil-works/pi-tui"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import chalk from "chalk"; import type { SearchableSelectListTheme } from "../components/searchable-select-list.js"; @@ -66,9 +67,18 @@ function isLightBackground(): boolean { return bg >= 244; } const cubeIndex = bg - 16; - const bVal = XTERM_LEVELS[cubeIndex % 6]; - const gVal = XTERM_LEVELS[Math.floor(cubeIndex / 6) % 6]; - const rVal = XTERM_LEVELS[Math.floor(cubeIndex / 36)]; + const bVal = expectDefined( + XTERM_LEVELS[cubeIndex % 6], + "xterm levels entry at cube index % 6", + ); + const gVal = expectDefined( + XTERM_LEVELS[Math.floor(cubeIndex / 6) % 6], + "xterm levels entry at math.floor(cube index / 6) % 6", + ); + const rVal = expectDefined( + XTERM_LEVELS[Math.floor(cubeIndex / 36)], + "xterm levels entry at math.floor(cube index / 36)", + ); return pickHigherContrastText(rVal, gVal, bVal); } } diff --git a/src/tui/tui-waiting.ts b/src/tui/tui-waiting.ts index ba895c589f73..d397ba34b81f 100644 --- a/src/tui/tui-waiting.ts +++ b/src/tui/tui-waiting.ts @@ -36,7 +36,7 @@ function shimmerText(theme: MinimalTheme, text: string, tick: number) { let out = ""; for (let i = 0; i < text.length; i++) { - const ch = text[i]; + const ch = text.charAt(i); out += i >= start && i <= end ? hi(ch) : theme.dim(ch); } return out; diff --git a/src/utils/directive-tags.ts b/src/utils/directive-tags.ts index 1ee225652af1..95524953a922 100644 --- a/src/utils/directive-tags.ts +++ b/src/utils/directive-tags.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; // Directive tag helpers parse inline directive tags from user text. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; @@ -63,7 +64,9 @@ function normalizeDirectiveWhitespace(text: string): string { .replace(/\n{3,}/g, "\n\n") .trimEnd(); - return normalized.replace(blockPlaceholderRe, (_, i) => blocks[Number(i)]); + return normalized.replace(blockPlaceholderRe, (_, i) => + expectDefined(blocks[Number(i)], "blocks entry at number(i)"), + ); } type StripInlineDirectiveTagsResult = { diff --git a/src/utils/message-channel.ts b/src/utils/message-channel.ts index c925af60f374..718fd04aa24c 100644 --- a/src/utils/message-channel.ts +++ b/src/utils/message-channel.ts @@ -8,7 +8,7 @@ import { normalizeGatewayClientName, } from "../../packages/gateway-protocol/src/client-info.js"; import { listBundledChannelCatalogEntries } from "../channels/bundled-channel-catalog-read.js"; -import { getChatChannelMeta } from "../channels/chat-meta.js"; +import { findChatChannelMeta } from "../channels/chat-meta.js"; import { getRegisteredChannelPluginMeta, normalizeChatChannelId } from "../channels/registry.js"; export { isDeliverableMessageChannel, @@ -103,7 +103,7 @@ export function isMarkdownCapableMessageChannel(raw?: string | null): boolean { } const builtInChannel = normalizeChatChannelId(channel); if (builtInChannel) { - const builtInMeta = getChatChannelMeta(builtInChannel); + const builtInMeta = findChatChannelMeta(builtInChannel); if (builtInMeta) { return builtInMeta.markdownCapable === true; } diff --git a/src/utils/queue-helpers.ts b/src/utils/queue-helpers.ts index ec07cd9632d8..88fa3b7cac3f 100644 --- a/src/utils/queue-helpers.ts +++ b/src/utils/queue-helpers.ts @@ -1,3 +1,4 @@ +import { expectDefined } from "@openclaw/normalization-core"; /** * Shared queue overflow, debounce, and collection helpers. * @@ -135,23 +136,23 @@ export function applyQueueDropPolicy(params: { // Only mutate the queue when enough victims exist so a partial drop cannot // admit overflow when the queue is full of in-flight/protected work. const victimIndices: number[] = []; - for ( - let index = 0; - index < params.queue.items.length && victimIndices.length < dropCount; - index += 1 - ) { - const item = params.queue.items[index]; + for (const [index, item] of params.queue.items.entries()) { if (params.inFlight?.has(item) || params.isProtected?.(item) === true) { continue; } victimIndices.push(index); + if (victimIndices.length === dropCount) { + break; + } } if (victimIndices.length < dropCount) { return false; } const dropped: T[] = []; for (let i = victimIndices.length - 1; i >= 0; i -= 1) { - dropped.unshift(...params.queue.items.splice(victimIndices[i], 1)); + dropped.unshift( + ...params.queue.items.splice(expectDefined(victimIndices[i], "victim indices entry at i"), 1), + ); } params.onDrop?.(dropped); if (params.queue.dropPolicy === "summarize") { diff --git a/src/utils/run-with-concurrency.ts b/src/utils/run-with-concurrency.ts index 805d2cb9fe16..dad82129d1a3 100644 --- a/src/utils/run-with-concurrency.ts +++ b/src/utils/run-with-concurrency.ts @@ -1,4 +1,4 @@ -/** Controls whether the worker pool keeps scheduling after a task failure. */ +import { expectDefined } from "@openclaw/normalization-core"; /** Controls whether the worker pool keeps scheduling after a task failure. */ export type ConcurrencyErrorMode = "continue" | "stop"; /** Options for running a fixed list of promise factories through a bounded worker pool. */ @@ -52,7 +52,7 @@ export async function runTasksWithConcurrency( return; } try { - results[index] = await tasks[index](); + results[index] = await expectDefined(tasks[index], "tasks entry at index")(); } catch (error) { if (!hasError) { firstError = error; diff --git a/src/utils/shell-argv.ts b/src/utils/shell-argv.ts index 2eb6f4207aa1..a90bfcd5a808 100644 --- a/src/utils/shell-argv.ts +++ b/src/utils/shell-argv.ts @@ -14,7 +14,7 @@ export function hasTopLevelShellControlOperator(raw: string): boolean { let wordStart = true; for (let i = 0; i < raw.length; i += 1) { - const ch = raw[i]; + const ch = raw.charAt(i); if (escaped) { escaped = false; wordStart = false; @@ -69,7 +69,7 @@ export function splitShellArgs(raw: string): string[] | null { }; for (let i = 0; i < raw.length; i += 1) { - const ch = raw[i]; + const ch = raw.charAt(i); if (escaped) { buf += ch; escaped = false; diff --git a/src/utils/usage-format.ts b/src/utils/usage-format.ts index b338c11f1570..d94eb41b56c7 100644 --- a/src/utils/usage-format.ts +++ b/src/utils/usage-format.ts @@ -3,6 +3,7 @@ * Keep this module synchronous; request paths call it while rendering usage summaries. */ import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { resolveDefaultAgentDir } from "../agents/agent-scope-config.js"; import { modelKey, normalizeModelRef, normalizeProviderId } from "../agents/model-selection.js"; @@ -661,7 +662,7 @@ function selectPricingTier(tiers: PricingTier[], input: number): PricingTier | u } for (let index = sortedTiers.length - 1; index >= 0; index -= 1) { - const tier = sortedTiers[index]; + const tier = expectDefined(sortedTiers[index], "sorted tiers entry at index"); if (input >= tier.range[0]) { return tier; } diff --git a/src/wizard/clack-navigation-prompts.ts b/src/wizard/clack-navigation-prompts.ts index 453c064222b7..51903ae9f79c 100644 --- a/src/wizard/clack-navigation-prompts.ts +++ b/src/wizard/clack-navigation-prompts.ts @@ -31,6 +31,7 @@ import { type SelectOptions, type TextOptions, } from "@clack/prompts"; +import { expectDefined } from "@openclaw/normalization-core"; import type { WizardPromptNavigation } from "./prompts.js"; type PromptOption = { @@ -164,7 +165,10 @@ export function selectWithNavigationFooter( const submitPrefix = showGuide ? `${styleText("gray", S_BAR)} ` : ""; const wrappedLines = wrapTextWithPrefix( opts.output, - selectOptionRenderer(this.options[this.cursor], "selected"), + selectOptionRenderer( + expectDefined(this.options[this.cursor], "options entry at this.cursor"), + "selected", + ), submitPrefix, ); return `${title}${wrappedLines}`; @@ -173,7 +177,10 @@ export function selectWithNavigationFooter( const cancelPrefix = showGuide ? `${styleText("gray", S_BAR)} ` : ""; const wrappedLines = wrapTextWithPrefix( opts.output, - selectOptionRenderer(this.options[this.cursor], "cancelled"), + selectOptionRenderer( + expectDefined(this.options[this.cursor], "options entry at this.cursor"), + "cancelled", + ), cancelPrefix, ); return `${title}${wrappedLines}${showGuide ? `\n${styleText("gray", S_BAR)}` : ""}`;