mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
refactor: consolidate duplicate helper exports (#106016)
* refactor(normalization): consolidate string helpers * refactor(normalization): consolidate agent ids * refactor(paths): consolidate user path resolution * refactor(gateway): preserve transcript helper facade * fix(normalization): align helper build aliases * fix(memory): keep helper import line neutral * chore(plugin-sdk): align consolidated helper surface * refactor(normalization): reuse lowercase string helper * refactor(normalization): reuse record guard * refactor(normalization): share surrogate boundary helper * refactor(agents): share token estimate constant * refactor(memory): distinguish standalone helper contracts * refactor(normalization): share error cause formatter * refactor(config): distinguish eligibility predicates * refactor(plugins): share registry state symbol * refactor(cli): reuse outbound dependency adapter * refactor(cron): distinguish positive duration parser * fix(gateway): keep transcript helper private * fix(paths): preserve public helper status * refactor(channels): reuse registry normalizer * refactor(agents): reuse agent core runtime facade * refactor(auth): reuse locked profile upsert * refactor(records): distinguish trap-safe guard * refactor(migrations): distinguish sync directory helper * test(plugins): drop stale duration alias expectations * fix(channels): remove stale registry import * chore(plugin-sdk): refresh helper API baseline * fix(memory): keep renamed helpers private * fix(plugins): keep registry state key private * fix(plugin-sdk): tighten wildcard surface budget * chore(plugin-sdk): refresh rebased helper API baseline
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
682886cad249cb42b80d77f8420947202be260c1b94f5cc11a780e64e781fa16 plugin-sdk-api-baseline.json
|
||||
59ac0750a4d514753ce0051bc769a0d642ca6a004e64dec7e28ae7c46b00609f plugin-sdk-api-baseline.jsonl
|
||||
258b7a39727a8306374bfdca2300d489fcbec41ef5a2f499c6a4b7cc83603e5c plugin-sdk-api-baseline.json
|
||||
f3f0affc5cffbd83ffb84cbbc9be86fcaffb574f7022dd804212d0ceac04bd4a plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -57,25 +57,4 @@ export function redactSensitiveText(value: string): string {
|
||||
return redacted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a non-Error `cause` value without leaking `[object Object]` or throwing
|
||||
* while formatting nested ACP runtime failures.
|
||||
*/
|
||||
export function stringifyNonErrorCause(value: unknown): string {
|
||||
if (value === null) {
|
||||
return "null";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
try {
|
||||
// JSON.stringify returns undefined (not a string) for functions/symbols/undefined; fall back to
|
||||
// a tag string so this `string`-typed helper never leaks undefined (matches src/infra/errors.ts).
|
||||
return JSON.stringify(value) ?? Object.prototype.toString.call(value);
|
||||
} catch {
|
||||
return Object.prototype.toString.call(value);
|
||||
}
|
||||
}
|
||||
export { stringifyNonErrorCause } from "@openclaw/normalization-core/error-coercion";
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@openclaw/normalization-core": "workspace:*",
|
||||
"markdown-it": "14.3.0",
|
||||
"markdown-it-cjk-friendly": "2.0.2",
|
||||
"yaml": "2.9.0"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// Markdown Core module implements chunk text behavior.
|
||||
import { avoidTrailingHighSurrogateBreak } from "@openclaw/normalization-core/utf16-slice";
|
||||
|
||||
export { avoidTrailingHighSurrogateBreak };
|
||||
|
||||
function resolveChunkEarlyReturn(text: string, limit: number): string[] | undefined {
|
||||
if (!text) {
|
||||
return [];
|
||||
@@ -105,23 +109,6 @@ export function chunkTextRanges(text: string, options: ChunkTextRangesOptions):
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps UTF-16 chunk boundaries from separating a supplementary-plane character.
|
||||
* A one-unit positive limit still needs to emit an entire surrogate pair.
|
||||
*/
|
||||
export function avoidTrailingHighSurrogateBreak(text: string, start: number, end: number): number {
|
||||
if (
|
||||
end >= text.length ||
|
||||
text.charCodeAt(end - 1) < 0xd800 ||
|
||||
text.charCodeAt(end - 1) > 0xdbff ||
|
||||
text.charCodeAt(end) < 0xdc00 ||
|
||||
text.charCodeAt(end) > 0xdfff
|
||||
) {
|
||||
return end;
|
||||
}
|
||||
return end - 1 > start ? end - 1 : end + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits plain text into size-bounded chunks at readable boundaries.
|
||||
*
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"dependencies": {
|
||||
"@openclaw/normalization-core": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Media Generation Core module implements capability model ref behavior.
|
||||
import { normalizeOptionalString } from "./string.js";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
/** Provider catalog entry shape used when resolving capability-scoped model references. */
|
||||
export type CapabilityModelProviderCandidate = {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
// Shared string normalization helpers for media-generation packages.
|
||||
|
||||
/** Normalize optional strings, returning undefined for non-strings or empty values. */
|
||||
export function normalizeOptionalString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
/** Return unique trimmed strings while preserving first-seen order. */
|
||||
export function uniqueTrimmedStrings(values: readonly unknown[]): string[] {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// Memory Host SDK module implements backend config behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
CANONICAL_ROOT_MEMORY_FILENAME,
|
||||
normalizeStringEntries,
|
||||
uniqueStrings,
|
||||
} from "@openclaw/normalization-core/string-normalization";
|
||||
import {
|
||||
MEMORY_HOST_ROOT_FILENAME,
|
||||
type MemoryBackend,
|
||||
type MemoryCitationsMode,
|
||||
type MemoryQmdConfig,
|
||||
@@ -11,19 +16,14 @@ import {
|
||||
type MemoryQmdSearchMode,
|
||||
type MemoryQmdStartupMode,
|
||||
type OpenClawConfig,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveMemoryHostAgentWorkspaceDir,
|
||||
normalizeAgentId,
|
||||
resolveUserPath,
|
||||
resolveMemoryHostUserPath,
|
||||
type SessionSendPolicyConfig,
|
||||
splitShellArgs,
|
||||
} from "./config-utils.js";
|
||||
import { isPathInside } from "./fs-utils.js";
|
||||
import { parseDurationMs } from "./openclaw-runtime-config.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeStringEntries,
|
||||
uniqueStrings,
|
||||
} from "./string-utils.js";
|
||||
|
||||
function escapeQmdExactFilePattern(fileName: string): string {
|
||||
return fileName.replace(/[\\*?[\]{}()!+@]/g, "\\$&");
|
||||
@@ -209,7 +209,7 @@ function resolvePath(raw: string, workspaceDir: string): string {
|
||||
throw new Error("path required");
|
||||
}
|
||||
if (trimmed.startsWith("~") || path.isAbsolute(trimmed)) {
|
||||
return path.normalize(resolveUserPath(trimmed));
|
||||
return path.normalize(resolveMemoryHostUserPath(trimmed));
|
||||
}
|
||||
return path.normalize(path.resolve(workspaceDir, trimmed));
|
||||
}
|
||||
@@ -407,7 +407,7 @@ function resolveDefaultCollections(
|
||||
return [];
|
||||
}
|
||||
const entries: Array<{ path: string; pattern: string; base: string }> = [
|
||||
{ path: workspaceDir, pattern: CANONICAL_ROOT_MEMORY_FILENAME, base: "memory-root" },
|
||||
{ path: workspaceDir, pattern: MEMORY_HOST_ROOT_FILENAME, base: "memory-root" },
|
||||
{ path: path.join(workspaceDir, "memory"), pattern: "**/*.md", base: "memory-dir" },
|
||||
];
|
||||
return entries.map((entry) => ({
|
||||
@@ -429,7 +429,7 @@ export function resolveMemoryBackendConfig(params: {
|
||||
return { backend: "builtin", citations };
|
||||
}
|
||||
|
||||
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, normalizedAgentId);
|
||||
const workspaceDir = resolveMemoryHostAgentWorkspaceDir(params.cfg, normalizedAgentId);
|
||||
const qmdCfg = params.cfg.memory?.qmd;
|
||||
const includeDefaultMemory = qmdCfg?.includeDefaultMemory !== false;
|
||||
const nameSet = new Set<string>();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Memory Host SDK module implements batch runner behavior.
|
||||
import { resolveSafeTimeoutDelayMs } from "../../../gateway-client/src/timeouts.js";
|
||||
import { splitBatchRequestsByLimits } from "./batch-utils.js";
|
||||
import { runWithConcurrency } from "./internal.js";
|
||||
import { runMemoryHostTasksWithConcurrency } from "./internal.js";
|
||||
|
||||
// Shared runner for splitting and executing remote embedding batch groups.
|
||||
|
||||
@@ -117,7 +117,7 @@ export async function runEmbeddingBatchGroups<TRequest>(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
|
||||
await runWithConcurrency(tasks, params.concurrency);
|
||||
await runMemoryHostTasksWithConcurrency(tasks, params.concurrency);
|
||||
return byCustomId;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
} from "./batch-utils.js";
|
||||
import { hashText } from "./hash.js";
|
||||
import { withRemoteHttpResponse } from "./remote-http.js";
|
||||
import { readResponseJsonWithLimit, readResponseTextSnippet } from "./response-snippet.js";
|
||||
import {
|
||||
readMemoryHostResponseTextSnippet,
|
||||
readResponseJsonWithLimit,
|
||||
} from "./response-snippet.js";
|
||||
|
||||
// Uploads provider batch JSONL payloads through the shared remote HTTP guard.
|
||||
|
||||
@@ -40,7 +43,7 @@ export async function uploadBatchJsonlFile(params: {
|
||||
},
|
||||
onResponse: async (fileRes) => {
|
||||
if (!fileRes.ok) {
|
||||
const text = await readResponseTextSnippet(fileRes, { signal: params.signal });
|
||||
const text = await readMemoryHostResponseTextSnippet(fileRes, { signal: params.signal });
|
||||
throw new Error(`${params.errorPrefix}: ${fileRes.status} ${text}`);
|
||||
}
|
||||
return (await readResponseJsonWithLimit(fileRes, {
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeAgentId } from "@openclaw/normalization-core/agent-id";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
normalizeStringEntries,
|
||||
uniqueStrings,
|
||||
} from "./string-utils.js";
|
||||
} from "@openclaw/normalization-core/string-normalization";
|
||||
export { normalizeAgentId };
|
||||
export { splitShellArgs } from "./openclaw-runtime-io.js";
|
||||
|
||||
// Shared OpenClaw config helpers used by memory host, QMD, and agent context code.
|
||||
@@ -165,34 +169,11 @@ export type OpenClawConfig = {
|
||||
};
|
||||
|
||||
/** Root memory filename used in agent workspaces. */
|
||||
export const CANONICAL_ROOT_MEMORY_FILENAME = "MEMORY.md";
|
||||
export const MEMORY_HOST_ROOT_FILENAME = "MEMORY.md";
|
||||
|
||||
const DEFAULT_AGENT_ID = "main";
|
||||
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
||||
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
|
||||
const LEADING_DASH_RE = /^-+/;
|
||||
const TRAILING_DASH_RE = /-+$/;
|
||||
const LEGACY_STATE_DIRNAMES = [".clawdbot"] as const;
|
||||
const NEW_STATE_DIRNAME = ".openclaw";
|
||||
/** Normalize user or config agent ids to the filesystem-safe canonical form. */
|
||||
export function normalizeAgentId(value: string | undefined | null): string {
|
||||
const trimmed = (value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_AGENT_ID;
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
if (VALID_ID_RE.test(trimmed)) {
|
||||
return normalized;
|
||||
}
|
||||
return (
|
||||
normalized
|
||||
.replace(INVALID_CHARS_RE, "-")
|
||||
.replace(LEADING_DASH_RE, "")
|
||||
.replace(TRAILING_DASH_RE, "")
|
||||
.slice(0, 64) || DEFAULT_AGENT_ID
|
||||
);
|
||||
}
|
||||
|
||||
/** Treat shell-placeholder home values as absent. */
|
||||
function normalizeHomeValue(value: string | undefined): string | undefined {
|
||||
const trimmed = normalizeOptionalString(value);
|
||||
@@ -223,8 +204,8 @@ function resolveRequiredHomeDir(
|
||||
return rawHome ? path.resolve(rawHome) : path.resolve(process.cwd());
|
||||
}
|
||||
|
||||
/** Resolve absolute user paths, including "~" against the effective OpenClaw home. */
|
||||
export function resolveUserPath(
|
||||
/** Resolve standalone memory-host paths without importing core home-directory policy. */
|
||||
export function resolveMemoryHostUserPath(
|
||||
input: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homedir: () => string = os.homedir,
|
||||
@@ -251,7 +232,7 @@ function resolveStateDir(
|
||||
): string {
|
||||
const override = env.OPENCLAW_STATE_DIR?.trim();
|
||||
if (override) {
|
||||
return resolveUserPath(override, env, homedir);
|
||||
return resolveMemoryHostUserPath(override, env, homedir);
|
||||
}
|
||||
const effectiveHome = () => resolveRequiredHomeDir(env, homedir);
|
||||
const nextDir = path.join(effectiveHome(), NEW_STATE_DIRNAME);
|
||||
@@ -308,7 +289,7 @@ function stripNullBytes(value: string): string {
|
||||
}
|
||||
|
||||
/** Resolve the workspace directory for an agent id and config defaults. */
|
||||
export function resolveAgentWorkspaceDir(
|
||||
export function resolveMemoryHostAgentWorkspaceDir(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
@@ -316,22 +297,22 @@ export function resolveAgentWorkspaceDir(
|
||||
const id = normalizeAgentId(agentId);
|
||||
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
||||
if (configured) {
|
||||
return stripNullBytes(resolveUserPath(configured, env));
|
||||
return stripNullBytes(resolveMemoryHostUserPath(configured, env));
|
||||
}
|
||||
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
||||
if (id === resolveDefaultAgentId(cfg)) {
|
||||
return stripNullBytes(
|
||||
fallback ? resolveUserPath(fallback, env) : resolveDefaultAgentWorkspaceDir(env),
|
||||
fallback ? resolveMemoryHostUserPath(fallback, env) : resolveDefaultAgentWorkspaceDir(env),
|
||||
);
|
||||
}
|
||||
if (fallback) {
|
||||
return stripNullBytes(path.join(resolveUserPath(fallback, env), id));
|
||||
return stripNullBytes(path.join(resolveMemoryHostUserPath(fallback, env), id));
|
||||
}
|
||||
return stripNullBytes(path.join(resolveStateDir(env), `workspace-${id}`));
|
||||
}
|
||||
|
||||
/** Resolve context limits for an agent with defaults fallback. */
|
||||
export function resolveAgentContextLimits(
|
||||
export function resolveMemoryHostAgentContextLimits(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
agentId?: string | null,
|
||||
): AgentContextLimitsConfig | undefined {
|
||||
@@ -343,7 +324,7 @@ export function resolveAgentContextLimits(
|
||||
}
|
||||
|
||||
/** Resolve enabled memory search config plus deduplicated extra paths for an agent. */
|
||||
export function resolveMemorySearchConfig(
|
||||
export function resolveMemoryHostSearchPathConfig(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
): { enabled: boolean; extraPaths: string[] } | null {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Memory Host SDK helper module supports embedding provider adapter utils behavior.
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
// Adapter helpers shared by remote embedding provider implementations.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Memory Host SDK module implements embeddings debug behavior.
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
// Lightweight stderr debug logging for memory embedding internals.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Memory Host SDK module implements embeddings remote client behavior.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { EmbeddingProviderOptions } from "./embeddings.types.js";
|
||||
import { requireApiKey, resolveApiKeyForProvider } from "./openclaw-runtime-auth.js";
|
||||
import { buildRemoteBaseUrlPolicy } from "./remote-http.js";
|
||||
import { resolveMemorySecretInputString } from "./secret-input.js";
|
||||
import type { SsrFPolicy } from "./ssrf-policy.js";
|
||||
import { normalizeOptionalString } from "./string-utils.js";
|
||||
|
||||
// Builds authenticated remote embedding HTTP clients from agent memory config.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { fork, type ChildProcess } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { DEFAULT_LOCAL_MODEL } from "./embedding-defaults.js";
|
||||
import {
|
||||
createLocalEmbeddingWorkerFailureError,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
attachLocalEmbeddingRuntimeFacts,
|
||||
type LocalEmbeddingRuntimeFacts,
|
||||
} from "./local-embedding-runtime-facts.js";
|
||||
import { normalizeOptionalString } from "./string-utils.js";
|
||||
|
||||
// Parent-side local embedding worker client for isolating node-llama-cpp state.
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { toRetryError } from "@openclaw/retry";
|
||||
import { DEFAULT_LOCAL_MODEL } from "./embedding-defaults.js";
|
||||
import { sanitizeAndNormalizeEmbedding } from "./embedding-vectors.js";
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
type LlamaModel,
|
||||
} from "./node-llama.js";
|
||||
// Memory Host SDK module implements embeddings behavior.
|
||||
import { normalizeOptionalString } from "./string-utils.js";
|
||||
|
||||
type DisposableResource = {
|
||||
dispose?: () => Promise<void> | void;
|
||||
|
||||
@@ -4,8 +4,9 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeStringEntries, uniqueStrings } from "@openclaw/normalization-core";
|
||||
import { runWithConcurrency as runWithConcurrencyImpl } from "./concurrency.js";
|
||||
import { CANONICAL_ROOT_MEMORY_FILENAME } from "./config-utils.js";
|
||||
import { MEMORY_HOST_ROOT_FILENAME } from "./config-utils.js";
|
||||
import { estimateStructuredEmbeddingInputBytes } from "./embedding-input-limits.js";
|
||||
import { buildTextEmbeddingInput, type EmbeddingInput } from "./embedding-inputs.js";
|
||||
import {
|
||||
@@ -32,7 +33,6 @@ import {
|
||||
shouldSkipRootMemoryAuxiliaryPath,
|
||||
} from "./openclaw-runtime-memory.js";
|
||||
import { retryTransientMemoryRead } from "./read-retry.js";
|
||||
import { normalizeStringEntries, uniqueStrings } from "./string-utils.js";
|
||||
|
||||
export { hashText } from "./hash.js";
|
||||
import { hashText } from "./hash.js";
|
||||
@@ -69,11 +69,13 @@ const DISABLED_MULTIMODAL_SETTINGS: MemoryMultimodalSettings = {
|
||||
maxFileBytes: 0,
|
||||
};
|
||||
|
||||
export function ensureDir(dir: string): string {
|
||||
function ensureMemoryHostDir(dir: string): string {
|
||||
fsSync.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
export { ensureMemoryHostDir as ensureDir };
|
||||
|
||||
function normalizeRelPath(value: string): string {
|
||||
const trimmed = value.trim().replace(/^[./]+/, "");
|
||||
return trimmed.replace(/\\/g, "/");
|
||||
@@ -106,7 +108,7 @@ export function isMemoryPath(relPath: string): boolean {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (normalized === CANONICAL_ROOT_MEMORY_FILENAME || normalized.toLowerCase() === "dreams.md") {
|
||||
if (normalized === MEMORY_HOST_ROOT_FILENAME || normalized.toLowerCase() === "dreams.md") {
|
||||
return true;
|
||||
}
|
||||
return normalized.startsWith("memory/");
|
||||
@@ -539,6 +541,11 @@ export function cosineSimilarity(a: number[], b: number[]): number {
|
||||
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
||||
}
|
||||
|
||||
export function runWithConcurrency<T>(tasks: Array<() => Promise<T>>, limit: number): Promise<T[]> {
|
||||
export function runMemoryHostTasksWithConcurrency<T>(
|
||||
tasks: Array<() => Promise<T>>,
|
||||
limit: number,
|
||||
): Promise<T[]> {
|
||||
return runWithConcurrencyImpl(tasks, limit);
|
||||
}
|
||||
|
||||
export { runMemoryHostTasksWithConcurrency as runWithConcurrency };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Memory Host SDK module implements multimodal behavior.
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
// Multimodal memory settings and file classification helpers.
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Memory Host SDK module implements post json behavior.
|
||||
import { withRemoteHttpResponse } from "./remote-http.js";
|
||||
import { readResponseJsonWithLimit, readResponseTextSnippet } from "./response-snippet.js";
|
||||
import {
|
||||
readMemoryHostResponseTextSnippet,
|
||||
readResponseJsonWithLimit,
|
||||
} from "./response-snippet.js";
|
||||
import type { SsrFPolicy } from "./ssrf-policy.js";
|
||||
|
||||
// Shared JSON POST helper for guarded remote memory provider calls.
|
||||
@@ -30,7 +33,7 @@ export async function postJson<T>(params: {
|
||||
},
|
||||
onResponse: async (res) => {
|
||||
if (!res.ok) {
|
||||
const text = await readResponseTextSnippet(res, { signal: params.signal });
|
||||
const text = await readMemoryHostResponseTextSnippet(res, { signal: params.signal });
|
||||
const err = new Error(`${params.errorPrefix}: ${res.status} ${text}`) as Error & {
|
||||
status?: number;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Memory Host SDK module implements qmd query parser behavior.
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { formatErrorMessage } from "./error-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
|
||||
// Parser for qmd query JSON output, including noisy CLI wrapper output.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Memory Host SDK module implements qmd scope behavior.
|
||||
import type { ResolvedQmdConfig } from "./backend-config.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "./string-utils.js";
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import type { ResolvedQmdConfig } from "./backend-config.js";
|
||||
|
||||
type ParsedQmdSessionScope = {
|
||||
channel?: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Memory Host SDK module implements query expansion behavior.
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
/**
|
||||
* Query expansion for FTS-only search mode.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
resolveAgentContextLimits,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveMemorySearchConfig,
|
||||
resolveMemoryHostAgentContextLimits,
|
||||
resolveMemoryHostAgentWorkspaceDir,
|
||||
resolveMemoryHostSearchPathConfig,
|
||||
type OpenClawConfig,
|
||||
} from "./config-utils.js";
|
||||
import {
|
||||
@@ -165,13 +165,13 @@ export async function readAgentMemoryFile(params: {
|
||||
from?: number;
|
||||
lines?: number;
|
||||
}): Promise<MemoryReadResult> {
|
||||
const settings = resolveMemorySearchConfig(params.cfg, params.agentId);
|
||||
const settings = resolveMemoryHostSearchPathConfig(params.cfg, params.agentId);
|
||||
if (!settings) {
|
||||
throw new Error("memory search disabled");
|
||||
}
|
||||
const contextLimits = resolveAgentContextLimits(params.cfg, params.agentId);
|
||||
const contextLimits = resolveMemoryHostAgentContextLimits(params.cfg, params.agentId);
|
||||
return await readMemoryFile({
|
||||
workspaceDir: resolveAgentWorkspaceDir(params.cfg, params.agentId),
|
||||
workspaceDir: resolveMemoryHostAgentWorkspaceDir(params.cfg, params.agentId),
|
||||
extraPaths: settings.extraPaths,
|
||||
relPath: params.relPath,
|
||||
from: params.from,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Memory Host SDK tests cover response snippet behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readResponseJsonWithLimit, readResponseTextSnippet } from "./response-snippet.js";
|
||||
import {
|
||||
readMemoryHostResponseTextSnippet,
|
||||
readResponseJsonWithLimit,
|
||||
} from "./response-snippet.js";
|
||||
|
||||
describe("readResponseTextSnippet", () => {
|
||||
describe("readMemoryHostResponseTextSnippet", () => {
|
||||
function stallingResponse(onCancel: () => void): Response {
|
||||
const reader = {
|
||||
read: () => new Promise<ReadableStreamReadResult<Uint8Array>>(() => {}),
|
||||
@@ -30,15 +33,15 @@ describe("readResponseTextSnippet", () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
readResponseTextSnippet(new Response(stream), { maxBytes: 4, maxChars: 100 }),
|
||||
readMemoryHostResponseTextSnippet(new Response(stream), { maxBytes: 4, maxChars: 100 }),
|
||||
).resolves.toBe("abcd... [truncated]");
|
||||
expect(canceled).toBe(true);
|
||||
});
|
||||
|
||||
it("does not split surrogate pairs when truncating text snippets", async () => {
|
||||
await expect(readResponseTextSnippet(new Response("abc🤖tail"), { maxChars: 4 })).resolves.toBe(
|
||||
"abc... [truncated]",
|
||||
);
|
||||
await expect(
|
||||
readMemoryHostResponseTextSnippet(new Response("abc🤖tail"), { maxChars: 4 }),
|
||||
).resolves.toBe("abc... [truncated]");
|
||||
});
|
||||
|
||||
it("drops partial UTF-8 characters when byte-capped snippets truncate a stream", async () => {
|
||||
@@ -50,7 +53,7 @@ describe("readResponseTextSnippet", () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
readResponseTextSnippet(new Response(stream), { maxBytes: 3, maxChars: 100 }),
|
||||
readMemoryHostResponseTextSnippet(new Response(stream), { maxBytes: 3, maxChars: 100 }),
|
||||
).resolves.toBe("ab... [truncated]");
|
||||
});
|
||||
|
||||
@@ -60,7 +63,7 @@ describe("readResponseTextSnippet", () => {
|
||||
canceled = true;
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const read = readResponseTextSnippet(response, {
|
||||
const read = readMemoryHostResponseTextSnippet(response, {
|
||||
maxBytes: 1024,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ type ResponsePrefix = {
|
||||
};
|
||||
|
||||
/** Read a small collapsed text snippet from a response body. */
|
||||
export async function readResponseTextSnippet(
|
||||
export async function readMemoryHostResponseTextSnippet(
|
||||
res: Response,
|
||||
options: ResponseTextSnippetOptions = {},
|
||||
): Promise<string> {
|
||||
|
||||
@@ -118,7 +118,7 @@ function coerceSecretRef(value: unknown): SecretRef | null {
|
||||
}
|
||||
|
||||
/** Return true when a secret input has either a literal value or resolvable reference shape. */
|
||||
export function hasConfiguredSecretInput(value: unknown): boolean {
|
||||
export function hasConfiguredMemorySecretInputValue(value: unknown): boolean {
|
||||
if (normalizeSecretInputString(value)) {
|
||||
return true;
|
||||
}
|
||||
@@ -138,12 +138,12 @@ function createUnresolvedSecretInputError(params: { path: string; ref: SecretRef
|
||||
}
|
||||
|
||||
/** Return a canonical SecretRef when the input is a supported reference shape. */
|
||||
export function resolveSecretInputRef(value: unknown): SecretRef | null {
|
||||
export function resolveMemorySecretInputRef(value: unknown): SecretRef | null {
|
||||
return coerceSecretRef(value);
|
||||
}
|
||||
|
||||
/** Normalize literal secrets, or throw for refs that still require gateway resolution. */
|
||||
export function normalizeResolvedSecretInputString(params: {
|
||||
export function normalizeResolvedMemorySecretInputString(params: {
|
||||
value: unknown;
|
||||
path: string;
|
||||
}): string | undefined {
|
||||
@@ -151,7 +151,7 @@ export function normalizeResolvedSecretInputString(params: {
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
const ref = resolveSecretInputRef(params.value);
|
||||
const ref = resolveMemorySecretInputRef(params.value);
|
||||
if (!ref) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Memory Host SDK module implements secret input behavior.
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
hasConfiguredMemorySecretInputValue,
|
||||
normalizeEnvSecretInputString,
|
||||
normalizeResolvedSecretInputString,
|
||||
resolveSecretInputRef,
|
||||
normalizeResolvedMemorySecretInputString,
|
||||
resolveMemorySecretInputRef,
|
||||
} from "./secret-input-utils.js";
|
||||
|
||||
// Memory-specific facade for resolving provider secret input from config.
|
||||
|
||||
/** Return true when a configured memory secret contains a literal value or reference. */
|
||||
export function hasConfiguredMemorySecretInput(value: unknown): boolean {
|
||||
return hasConfiguredSecretInput(value);
|
||||
return hasConfiguredMemorySecretInputValue(value);
|
||||
}
|
||||
|
||||
/** Resolve memory secret input, reading env refs directly when available. */
|
||||
@@ -18,14 +18,14 @@ export function resolveMemorySecretInputString(params: {
|
||||
value: unknown;
|
||||
path: string;
|
||||
}): string | undefined {
|
||||
const ref = resolveSecretInputRef(params.value);
|
||||
const ref = resolveMemorySecretInputRef(params.value);
|
||||
if (ref?.source === "env") {
|
||||
const envValue = normalizeEnvSecretInputString(process.env[ref.id]);
|
||||
if (envValue) {
|
||||
return envValue;
|
||||
}
|
||||
}
|
||||
return normalizeResolvedSecretInputString({
|
||||
return normalizeResolvedMemorySecretInputString({
|
||||
value: params.value,
|
||||
path: params.path,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Memory Host SDK module implements sqlite vec behavior.
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { formatErrorMessage } from "./error-utils.js";
|
||||
import { resolveSqliteVecPlatformVariant } from "./sqlite-vec-platform-variant.js";
|
||||
import { normalizeOptionalString } from "./string-utils.js";
|
||||
|
||||
type SqliteVecModule = {
|
||||
getLoadablePath: () => string;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
const require = createRequire(import.meta.url);
|
||||
const sqliteWalMaintenanceByDb = new WeakMap<DatabaseSync, SqliteWalMaintenance>();
|
||||
|
||||
export function requireNodeSqlite(): typeof import("node:sqlite") {
|
||||
function requireMemoryHostNodeSqlite(): typeof import("node:sqlite") {
|
||||
installProcessWarningFilter();
|
||||
try {
|
||||
return require("node:sqlite") as typeof import("node:sqlite");
|
||||
@@ -29,6 +29,8 @@ export function requireNodeSqlite(): typeof import("node:sqlite") {
|
||||
}
|
||||
}
|
||||
|
||||
export { requireMemoryHostNodeSqlite as requireNodeSqlite };
|
||||
|
||||
export function configureMemorySqliteWalMaintenance(
|
||||
db: DatabaseSync,
|
||||
options?: SqliteWalMaintenanceOptions & Pick<SqliteConnectionPragmaOptions, "busyTimeoutMs">,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Small string normalization helpers kept local to memory-host-sdk for package
|
||||
// builds that should not depend on the full normalization package graph.
|
||||
/** Normalize a non-empty string or return null. */
|
||||
function normalizeNullableString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
/** Normalize a non-empty string or return undefined. */
|
||||
export function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return normalizeNullableString(value) ?? undefined;
|
||||
}
|
||||
|
||||
/** Normalize a non-empty string to lowercase or return undefined. */
|
||||
export function normalizeOptionalLowercaseString(value: unknown): string | undefined {
|
||||
return normalizeOptionalString(value)?.toLowerCase();
|
||||
}
|
||||
|
||||
/** Normalize a value to lowercase text, defaulting to an empty string. */
|
||||
export function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
||||
return normalizeOptionalLowercaseString(value) ?? "";
|
||||
}
|
||||
|
||||
/** Normalize an array-like list of values into non-empty strings. */
|
||||
export function normalizeStringEntries(values: ReadonlyArray<unknown>): string[] {
|
||||
return values.map((value) => normalizeOptionalString(String(value)) ?? "").filter(Boolean);
|
||||
}
|
||||
|
||||
/** Return unique strings preserving first-seen order. */
|
||||
export function uniqueStrings(values: Iterable<string>): string[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"dependencies": {
|
||||
"@openclaw/normalization-core": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Model Catalog Core module implements provider id behavior.
|
||||
export function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
export { normalizeLowercaseStringOrEmpty };
|
||||
|
||||
export function normalizeProviderId(provider: string): string {
|
||||
return normalizeLowercaseStringOrEmpty(provider);
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
"import": "./dist/index.mjs",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./agent-id": {
|
||||
"types": "./dist/agent-id.d.mts",
|
||||
"import": "./dist/agent-id.mjs",
|
||||
"default": "./dist/agent-id.mjs"
|
||||
},
|
||||
"./boolean-coercion": {
|
||||
"types": "./dist/boolean-coercion.d.mts",
|
||||
"import": "./dist/boolean-coercion.mjs",
|
||||
@@ -61,6 +66,6 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown src/index.ts src/boolean-coercion.ts src/error-coercion.ts src/expect.ts src/number-coercion.ts src/record-coerce.ts src/result.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||
"build": "tsdown src/index.ts src/agent-id.ts src/boolean-coercion.ts src/error-coercion.ts src/expect.ts src/number-coercion.ts src/record-coerce.ts src/result.ts src/string-coerce.ts src/string-normalization.ts src/utf16-slice.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isValidAgentId, normalizeAgentId } from "@openclaw/normalization-core/agent-id";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("normalization-core/agent-id", () => {
|
||||
it.each([
|
||||
[undefined, "main"],
|
||||
[" OPS ", "ops"],
|
||||
["Agent not found: xyz", "agent-not-found-xyz"],
|
||||
["../../../etc/passwd", "etc-passwd"],
|
||||
["_".repeat(80), "_".repeat(64)],
|
||||
])("normalizes %j", (input, expected) => {
|
||||
expect(normalizeAgentId(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["main", true],
|
||||
["my-research_agent01", true],
|
||||
["", false],
|
||||
["Agent not found: xyz", false],
|
||||
["a".repeat(65), false],
|
||||
])("validates %j", (input, expected) => {
|
||||
expect(isValidAgentId(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
|
||||
|
||||
const DEFAULT_AGENT_ID = "main";
|
||||
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
||||
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
|
||||
const LEADING_DASH_RE = /^-+/;
|
||||
const TRAILING_DASH_RE = /-+$/;
|
||||
|
||||
/** Normalizes an OpenClaw agent id to its filesystem-safe canonical form. */
|
||||
export function normalizeAgentId(value: string | undefined | null): string {
|
||||
const trimmed = (value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_AGENT_ID;
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
if (VALID_ID_RE.test(trimmed)) {
|
||||
return normalized;
|
||||
}
|
||||
return (
|
||||
normalized
|
||||
.replace(INVALID_CHARS_RE, "-")
|
||||
.replace(LEADING_DASH_RE, "")
|
||||
.replace(TRAILING_DASH_RE, "")
|
||||
.slice(0, 64) || DEFAULT_AGENT_ID
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns whether a value is already a canonical agent-id input. */
|
||||
export function isValidAgentId(value: string | undefined | null): boolean {
|
||||
const trimmed = (value ?? "").trim();
|
||||
return Boolean(trimmed) && VALID_ID_RE.test(trimmed);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Normalization core tests cover shared error coercion and formatting behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatErrorMessage, toErrorObject } from "./error-coercion.js";
|
||||
import { formatErrorMessage, stringifyNonErrorCause, toErrorObject } from "./error-coercion.js";
|
||||
|
||||
const keepText = (text: string): string => text;
|
||||
const format = (value: unknown): string => formatErrorMessage(value, { redact: keepText });
|
||||
@@ -57,3 +57,16 @@ describe("toErrorObject", () => {
|
||||
expect(error.cause).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stringifyNonErrorCause", () => {
|
||||
it("renders primitive and structured values", () => {
|
||||
expect(stringifyNonErrorCause(null)).toBe("null");
|
||||
expect(stringifyNonErrorCause(42)).toBe("42");
|
||||
expect(stringifyNonErrorCause({ ok: true })).toBe('{"ok":true}');
|
||||
});
|
||||
|
||||
it("falls back to object tags when JSON has no string result", () => {
|
||||
expect(stringifyNonErrorCause(undefined)).toBe("[object Undefined]");
|
||||
expect(stringifyNonErrorCause(Symbol("value"))).toBe("[object Symbol]");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,3 +122,21 @@ export function toErrorObject(value: unknown, fallbackMessage: string): Error {
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
/** Renders a non-Error cause as useful text without throwing. */
|
||||
export function stringifyNonErrorCause(value: unknown): string {
|
||||
if (value === null) {
|
||||
return "null";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value) ?? Object.prototype.toString.call(value);
|
||||
} catch {
|
||||
return Object.prototype.toString.call(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
// Tests for surrogate-safe UTF-16 string slicing helpers.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sliceUtf16Safe, truncateUtf16Safe } from "./utf16-slice.js";
|
||||
import {
|
||||
avoidTrailingHighSurrogateBreak,
|
||||
sliceUtf16Safe,
|
||||
truncateUtf16Safe,
|
||||
} from "./utf16-slice.js";
|
||||
|
||||
describe("avoidTrailingHighSurrogateBreak", () => {
|
||||
it("keeps ordinary and terminal boundaries unchanged", () => {
|
||||
expect(avoidTrailingHighSurrogateBreak("hello", 0, 3)).toBe(3);
|
||||
expect(avoidTrailingHighSurrogateBreak("hello", 0, 5)).toBe(5);
|
||||
});
|
||||
|
||||
it("moves a split before a surrogate pair when room remains", () => {
|
||||
expect(avoidTrailingHighSurrogateBreak("a🤖b", 0, 2)).toBe(1);
|
||||
});
|
||||
|
||||
it("includes the full pair when a one-unit chunk starts with it", () => {
|
||||
expect(avoidTrailingHighSurrogateBreak("🤖b", 0, 1)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sliceUtf16Safe", () => {
|
||||
it("slices ASCII string normally", () => {
|
||||
|
||||
@@ -12,6 +12,20 @@ function isLowSurrogate(codeUnit: number): boolean {
|
||||
return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
|
||||
}
|
||||
|
||||
/** Moves a chunk boundary away from the middle of a UTF-16 surrogate pair. */
|
||||
export function avoidTrailingHighSurrogateBreak(text: string, start: number, end: number): number {
|
||||
if (
|
||||
end <= start ||
|
||||
end >= text.length ||
|
||||
!isHighSurrogate(text.charCodeAt(end - 1)) ||
|
||||
!isLowSurrogate(text.charCodeAt(end))
|
||||
) {
|
||||
return end;
|
||||
}
|
||||
const adjusted = end - 1;
|
||||
return adjusted > start ? adjusted : end + 1;
|
||||
}
|
||||
|
||||
/** Slices a UTF-16 string without returning dangling surrogate halves at either edge. */
|
||||
export function sliceUtf16Safe(input: string, start: number, end?: number): string {
|
||||
const len = input.length;
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@openclaw/normalization-core": "workspace:*",
|
||||
"@clack/prompts": "1.6.0",
|
||||
"chalk": "5.6.2"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
// Shared terminal string normalization helpers.
|
||||
|
||||
/** Normalize string input to lowercase, returning empty string for non-strings. */
|
||||
export function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
export { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
Generated
+16
-2
@@ -2027,6 +2027,9 @@ importers:
|
||||
|
||||
packages/markdown-core:
|
||||
dependencies:
|
||||
'@openclaw/normalization-core':
|
||||
specifier: workspace:*
|
||||
version: link:../normalization-core
|
||||
markdown-it:
|
||||
specifier: 14.3.0
|
||||
version: 14.3.0
|
||||
@@ -2046,7 +2049,11 @@ importers:
|
||||
specifier: 22.0.1
|
||||
version: 22.0.1
|
||||
|
||||
packages/media-generation-core: {}
|
||||
packages/media-generation-core:
|
||||
dependencies:
|
||||
'@openclaw/normalization-core':
|
||||
specifier: workspace:*
|
||||
version: link:../normalization-core
|
||||
|
||||
packages/media-understanding-common: {}
|
||||
|
||||
@@ -2062,7 +2069,11 @@ importers:
|
||||
specifier: 7.0.5
|
||||
version: 7.0.5
|
||||
|
||||
packages/model-catalog-core: {}
|
||||
packages/model-catalog-core:
|
||||
dependencies:
|
||||
'@openclaw/normalization-core':
|
||||
specifier: workspace:*
|
||||
version: link:../normalization-core
|
||||
|
||||
packages/net-policy:
|
||||
dependencies:
|
||||
@@ -2092,6 +2103,9 @@ importers:
|
||||
|
||||
packages/terminal-core:
|
||||
dependencies:
|
||||
'@openclaw/normalization-core':
|
||||
specifier: workspace:*
|
||||
version: link:../normalization-core
|
||||
'@clack/prompts':
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
|
||||
@@ -23,6 +23,7 @@ export {
|
||||
listProfilesForProvider,
|
||||
resolveSubscriptionAuthModeForProfiles,
|
||||
} from "./profile-list.js";
|
||||
export { upsertAuthProfileWithLock } from "./upsert-with-lock.js";
|
||||
|
||||
const authProfileProfilesLog = createSubsystemLogger("agent/embedded");
|
||||
|
||||
@@ -175,26 +176,6 @@ export function upsertAuthProfile(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Upserts an auth profile under the auth store lock. */
|
||||
export async function upsertAuthProfileWithLock(params: {
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
agentDir?: string;
|
||||
}): Promise<AuthProfileStore | null> {
|
||||
const credential = normalizeAuthProfileCredential(params.credential);
|
||||
return await updateAuthProfileStoreWithLock({
|
||||
agentDir: params.agentDir,
|
||||
saveOptions: {
|
||||
filterExternalAuthProfiles: false,
|
||||
syncExternalCli: false,
|
||||
},
|
||||
updater: (store) => {
|
||||
store.profiles[params.profileId] = credential;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes all auth profiles and related state for a provider. */
|
||||
export async function removeProviderAuthProfilesWithLock(params: {
|
||||
provider: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CHARS_PER_TOKEN_ESTIMATE } from "../../utils/cjk-chars.js";
|
||||
/**
|
||||
* Estimates message and tool-result character costs for context guards.
|
||||
*/
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
bashExecutionToText,
|
||||
} from "../runtime/index.js";
|
||||
|
||||
export const CHARS_PER_TOKEN_ESTIMATE = 4;
|
||||
export { CHARS_PER_TOKEN_ESTIMATE };
|
||||
export const TOOL_RESULT_CHARS_PER_TOKEN_ESTIMATE = 2;
|
||||
const IMAGE_CHAR_ESTIMATE = 8_000;
|
||||
|
||||
|
||||
@@ -1,28 +1,2 @@
|
||||
/**
|
||||
* OpenClaw-owned agent runtime facade.
|
||||
*
|
||||
* Wires agent-core to the plugin SDK LLM runtime and re-exports reusable runtime helpers.
|
||||
*/
|
||||
import {
|
||||
Agent as CoreAgent,
|
||||
type AgentOptions as CoreAgentOptions,
|
||||
} from "../../../packages/agent-core/src/agent.js";
|
||||
import type { AgentCoreRuntimeDeps } from "../../../packages/agent-core/src/runtime-deps.js";
|
||||
import type { CompleteSimpleFn, StreamFn } from "../../../packages/llm-core/src/index.js";
|
||||
import { completeSimple, streamSimple } from "../../plugin-sdk/llm.js";
|
||||
|
||||
export const openClawAgentCoreRuntime = {
|
||||
completeSimple: completeSimple as unknown as CompleteSimpleFn,
|
||||
streamSimple: streamSimple as unknown as StreamFn,
|
||||
} satisfies AgentCoreRuntimeDeps;
|
||||
|
||||
export class Agent extends CoreAgent {
|
||||
constructor(options: CoreAgentOptions = {}) {
|
||||
super({ runtime: openClawAgentCoreRuntime, ...options });
|
||||
}
|
||||
}
|
||||
|
||||
// OpenClaw-owned reusable agent core
|
||||
export * from "../../../packages/agent-core/src/index.js";
|
||||
// Proxy utilities
|
||||
export * from "./proxy.js";
|
||||
/** OpenClaw-owned agent runtime facade; the plugin SDK module owns the adapter. */
|
||||
export * from "../../plugin-sdk/agent-core.js";
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
// Public channel registry facade for channel ids, metadata, and setup copy.
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { normalizeChatChannelId, type ChatChannelId } from "./ids.js";
|
||||
import type { ChannelId } from "./plugins/channel-id.types.js";
|
||||
import type { ChannelMeta } from "./plugins/types.core.js";
|
||||
import {
|
||||
findRegisteredChannelPluginEntry,
|
||||
findRegisteredChannelPluginEntryById,
|
||||
listRegisteredChannelPluginEntries,
|
||||
} from "./registry-lookup.js";
|
||||
export { findChatChannelMeta, getChatChannelMeta } from "./chat-meta.js";
|
||||
export { CHAT_CHANNEL_ORDER } from "./ids.js";
|
||||
export type { ChatChannelId } from "./ids.js";
|
||||
export { normalizeAnyChannelId } from "./registry-normalize.js";
|
||||
export { normalizeChatChannelId };
|
||||
|
||||
/**
|
||||
@@ -23,17 +20,6 @@ export function normalizeChannelId(raw?: string | null): ChatChannelId | null {
|
||||
return normalizeChatChannelId(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes any registered channel plugin id or alias after registry initialization.
|
||||
*/
|
||||
export function normalizeAnyChannelId(raw?: string | null): ChannelId | null {
|
||||
const key = normalizeOptionalLowercaseString(raw);
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
return findRegisteredChannelPluginEntry(key)?.plugin.id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists registered channel plugin ids without importing their runtime implementations.
|
||||
*/
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
parseCronCommandEnv,
|
||||
parseCronFallbacks,
|
||||
parseCronToolsAllow,
|
||||
parseDurationMs,
|
||||
parsePositiveCronDurationMs,
|
||||
warnIfCronSchedulerDisabled,
|
||||
} from "./shared.js";
|
||||
import { normalizeCronSessionTargetOption, parseCronThreadIdOption } from "./thread-id-shared.js";
|
||||
@@ -605,7 +605,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
failureAlert.to = to ? to : undefined;
|
||||
}
|
||||
if (hasFailureAlertCooldown) {
|
||||
const cooldownMs = parseDurationMs(String(opts.failureAlertCooldown));
|
||||
const cooldownMs = parsePositiveCronDurationMs(String(opts.failureAlertCooldown));
|
||||
if (!cooldownMs && cooldownMs !== 0) {
|
||||
throw new Error("Invalid --failure-alert-cooldown.");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Shared schedule option resolver for cron create/edit commands.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { CronSchedule } from "../../cron/types.js";
|
||||
import { parseAt, parseCronStaggerMs, parseDurationMs } from "./shared.js";
|
||||
import { parseAt, parseCronStaggerMs, parsePositiveCronDurationMs } from "./shared.js";
|
||||
|
||||
type ScheduleOptionInput = {
|
||||
at?: unknown;
|
||||
@@ -169,7 +169,7 @@ function resolveDirectSchedule(options: NormalizedScheduleOptions): CronSchedule
|
||||
return { kind: "at", at: atIso };
|
||||
}
|
||||
if (options.every) {
|
||||
const everyMs = parseDurationMs(options.every);
|
||||
const everyMs = parsePositiveCronDurationMs(options.every);
|
||||
if (!everyMs) {
|
||||
throw new Error("Invalid --every. Use a duration like 10m, 1h, or 1d.");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getCronChannelOptions,
|
||||
parseAt,
|
||||
parseCronToolsAllow,
|
||||
parseDurationMs,
|
||||
parsePositiveCronDurationMs,
|
||||
printCronList,
|
||||
printCronShow,
|
||||
} from "./shared.js";
|
||||
@@ -526,29 +526,29 @@ describe("coerceCronDeliveryPreviews", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDurationMs", () => {
|
||||
describe("parsePositiveCronDurationMs", () => {
|
||||
it("parses valid positive durations", () => {
|
||||
expect(parseDurationMs("500ms")).toBe(500);
|
||||
expect(parseDurationMs("30s")).toBe(30_000);
|
||||
expect(parseDurationMs("1.5h")).toBe(5_400_000);
|
||||
expect(parseDurationMs("1h30m")).toBe(5_400_000);
|
||||
expect(parseDurationMs("1d")).toBe(86_400_000);
|
||||
expect(parsePositiveCronDurationMs("500ms")).toBe(500);
|
||||
expect(parsePositiveCronDurationMs("30s")).toBe(30_000);
|
||||
expect(parsePositiveCronDurationMs("1.5h")).toBe(5_400_000);
|
||||
expect(parsePositiveCronDurationMs("1h30m")).toBe(5_400_000);
|
||||
expect(parsePositiveCronDurationMs("1d")).toBe(86_400_000);
|
||||
});
|
||||
|
||||
it("rejects non-positive and malformed durations", () => {
|
||||
expect(parseDurationMs("0s")).toBeNull();
|
||||
expect(parseDurationMs("0.5ms")).toBe(1);
|
||||
expect(parseDurationMs("0.001ms")).toBeNull();
|
||||
expect(parseDurationMs("-5s")).toBeNull();
|
||||
expect(parseDurationMs("abc")).toBeNull();
|
||||
expect(parseDurationMs("")).toBeNull();
|
||||
expect(parsePositiveCronDurationMs("0s")).toBeNull();
|
||||
expect(parsePositiveCronDurationMs("0.5ms")).toBe(1);
|
||||
expect(parsePositiveCronDurationMs("0.001ms")).toBeNull();
|
||||
expect(parsePositiveCronDurationMs("-5s")).toBeNull();
|
||||
expect(parsePositiveCronDurationMs("abc")).toBeNull();
|
||||
expect(parsePositiveCronDurationMs("")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects durations that overflow to a non-finite millisecond value (#83906)", () => {
|
||||
// A finite mantissa can still overflow once multiplied by a large unit factor.
|
||||
expect(parseDurationMs(`1${"0".repeat(302)}d`)).toBeNull();
|
||||
expect(parsePositiveCronDurationMs(`1${"0".repeat(302)}d`)).toBeNull();
|
||||
// A large-but-finite result is still accepted.
|
||||
expect(parseDurationMs(`9${"0".repeat(15)}ms`)).toBe(9_000_000_000_000_000);
|
||||
expect(parsePositiveCronDurationMs(`9${"0".repeat(15)}ms`)).toBe(9_000_000_000_000_000);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseDurationMs(input: string): number | null {
|
||||
export function parsePositiveCronDurationMs(input: string): number | null {
|
||||
try {
|
||||
const result = parseSharedDurationMs(input);
|
||||
if (result <= 0) {
|
||||
@@ -242,7 +242,7 @@ export function parseCronStaggerMs(params: {
|
||||
if (!params.staggerRaw) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseDurationMs(params.staggerRaw);
|
||||
const parsed = parsePositiveCronDurationMs(params.staggerRaw);
|
||||
if (!parsed) {
|
||||
throw new Error("Invalid --stagger; use e.g. 30s, 1m, 5m");
|
||||
}
|
||||
@@ -301,7 +301,7 @@ export function parseAt(input: string, tz?: string): string | null {
|
||||
return timestampMsToIsoString(absolute) ?? null;
|
||||
}
|
||||
const durationInput = raw.startsWith("+") ? raw.slice(1) : raw;
|
||||
const dur = parseDurationMs(durationInput);
|
||||
const dur = parsePositiveCronDurationMs(durationInput);
|
||||
if (dur !== null) {
|
||||
const expiresAt = resolveExpiresAtMsFromDurationMs(dur);
|
||||
return timestampMsToIsoString(expiresAt) ?? null;
|
||||
|
||||
+2
-8
@@ -1,12 +1,8 @@
|
||||
// Default CLI dependency surface with lazy outbound channel send adapters.
|
||||
import { normalizeChannelId } from "../channels/registry.js";
|
||||
import type { OutboundSendDeps } from "../infra/outbound/send-deps.js";
|
||||
import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js";
|
||||
import type { CliDeps } from "./deps.types.js";
|
||||
import {
|
||||
CLI_OUTBOUND_SEND_FACTORY,
|
||||
createOutboundSendDepsFromCliSource,
|
||||
} from "./outbound-send-mapping.js";
|
||||
import { CLI_OUTBOUND_SEND_FACTORY } from "./outbound-send-mapping.js";
|
||||
|
||||
/**
|
||||
* Lazy-loaded per-channel send functions, keyed by channel ID.
|
||||
@@ -118,6 +114,4 @@ export function createDefaultDeps(): CliDeps {
|
||||
});
|
||||
}
|
||||
|
||||
export function createOutboundSendDeps(deps: CliDeps): OutboundSendDeps {
|
||||
return createOutboundSendDepsFromCliSource(deps);
|
||||
}
|
||||
export { createOutboundSendDeps } from "./outbound-send-deps.js";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Shared api.github.com plumbing for Control UI GitHub surfaces (link
|
||||
// previews, session pull request chips): pinned origin, manual redirects,
|
||||
// bounded bodies, and normalized upstream error statuses.
|
||||
export { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { readResponseWithLimit } from "../infra/http-body.js";
|
||||
|
||||
export const GITHUB_API_ORIGIN = "https://api.github.com";
|
||||
@@ -19,10 +20,6 @@ export class ControlUiGitHubError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function requiredString(record: Record<string, unknown>, key: string): string {
|
||||
const value = record[key];
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
extractJsonNullableStringFieldPrefix,
|
||||
extractJsonNumberFieldPrefix,
|
||||
extractJsonStringFieldPrefix,
|
||||
normalizeOptionalString,
|
||||
readNonBlankStringPreservingWhitespace,
|
||||
} from "./session-transcript-json.js";
|
||||
|
||||
const TRANSCRIPT_INDEX_READ_CHUNK_BYTES = 64 * 1024;
|
||||
@@ -264,9 +264,11 @@ async function buildSessionTranscriptIndex(
|
||||
if (!isIndexableTranscriptRecord(parsed)) {
|
||||
return;
|
||||
}
|
||||
const id = normalizeOptionalString(parsed.id);
|
||||
const id = readNonBlankStringPreservingWhitespace(parsed.id);
|
||||
const parentId =
|
||||
parsed.parentId === null ? null : (normalizeOptionalString(parsed.parentId) ?? undefined);
|
||||
parsed.parentId === null
|
||||
? null
|
||||
: (readNonBlankStringPreservingWhitespace(parsed.parentId) ?? undefined);
|
||||
const treeEntry = parseSessionTranscriptTreeEntry(parsed);
|
||||
const rawEntry: IndexedRawEntry = {
|
||||
...(id ? { id } : {}),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Shared bounded JSONL metadata parsing for gateway transcript readers.
|
||||
import { escapeRegExp } from "../shared/regexp.js";
|
||||
|
||||
export function normalizeOptionalString(value: unknown): string | undefined {
|
||||
/** Reads a nonblank transcript field while preserving its original whitespace. */
|
||||
export function readNonBlankStringPreservingWhitespace(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ export function extractJsonStringFieldPrefix(prefix: string, field: string): str
|
||||
}
|
||||
try {
|
||||
const decoded = JSON.parse(`"${match[1]}"`) as unknown;
|
||||
return normalizeOptionalString(decoded);
|
||||
return readNonBlankStringPreservingWhitespace(decoded);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
extractJsonNullableStringFieldPrefix,
|
||||
extractJsonNumberFieldPrefix,
|
||||
extractJsonStringFieldPrefix,
|
||||
normalizeOptionalString,
|
||||
readNonBlankStringPreservingWhitespace,
|
||||
} from "./session-transcript-json.js";
|
||||
import type { SessionPreviewItem } from "./session-utils.types.js";
|
||||
|
||||
@@ -362,7 +362,7 @@ function extractJsonStringFieldWindow(
|
||||
}
|
||||
try {
|
||||
const decoded = JSON.parse(`"${match[1]}"`) as unknown;
|
||||
return normalizeOptionalString(decoded);
|
||||
return readNonBlankStringPreservingWhitespace(decoded);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
// Gateway test assertion helpers narrow unknown protocol payloads to records
|
||||
// and assert selected fields with useful labels.
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { expect } from "vitest";
|
||||
|
||||
/**
|
||||
* Record-shape assertion helpers for gateway tests.
|
||||
*/
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
export { isRecord };
|
||||
|
||||
/** Requires an unknown value to be a record and throws with a test label. */
|
||||
export function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
|
||||
+5
-2
@@ -17,7 +17,10 @@ const DEFAULT_CONFIG_VALUES: Record<string, boolean> = {
|
||||
export { hasBinary };
|
||||
|
||||
/** Evaluate a config path with hook-specific defaults for legacy runtime requirements. */
|
||||
export function isConfigPathTruthy(config: OpenClawConfig | undefined, pathStr: string): boolean {
|
||||
export function isHookConfigPathTruthy(
|
||||
config: OpenClawConfig | undefined,
|
||||
pathStr: string,
|
||||
): boolean {
|
||||
return isConfigPathTruthyWithDefaults(config, pathStr, DEFAULT_CONFIG_VALUES);
|
||||
}
|
||||
|
||||
@@ -45,7 +48,7 @@ function evaluateHookRuntimeEligibility(params: {
|
||||
...base,
|
||||
hasBin: hasBinary,
|
||||
hasEnv: (envName) => Boolean(process.env[envName] || hookConfig?.env?.[envName]),
|
||||
isConfigPathTruthy: (configPath) => isConfigPathTruthy(config, configPath),
|
||||
isConfigPathTruthy: (configPath) => isHookConfigPathTruthy(config, configPath),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { evaluateEntryRequirementsForCurrentPlatform } from "../shared/entry-status.js";
|
||||
import type { RequirementConfigCheck, Requirements } from "../shared/requirements.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
import { hasBinary, isConfigPathTruthy } from "./config.js";
|
||||
import { hasBinary, isHookConfigPathTruthy } from "./config.js";
|
||||
import { isKnownInternalHookEventKey } from "./internal-hook-types.js";
|
||||
import {
|
||||
resolveHookConfig,
|
||||
@@ -102,7 +102,7 @@ function buildHookStatus(
|
||||
const unknownEvents = events.filter((event) => !isKnownInternalHookEventKey(event));
|
||||
const isEnvSatisfied = (envName: string) =>
|
||||
Boolean(process.env[envName] || hookConfig?.env?.[envName]);
|
||||
const isConfigSatisfied = (pathStr: string) => isConfigPathTruthy(config, pathStr);
|
||||
const isConfigSatisfied = (pathStr: string) => isHookConfigPathTruthy(config, pathStr);
|
||||
|
||||
const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } =
|
||||
evaluateEntryRequirementsForCurrentPlatform({
|
||||
|
||||
+1
-22
@@ -71,28 +71,7 @@ export function formatErrorMessage(err: unknown): string {
|
||||
return formatSharedErrorMessage(err, { redact: redactSensitiveText });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a non-Error `cause` value (string, number, plain object, etc.) for inclusion in
|
||||
* a flattened error chain. Returns `[object Object]`-free text without throwing.
|
||||
*/
|
||||
export function stringifyNonErrorCause(value: unknown): string {
|
||||
if (value === null) {
|
||||
return "null";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value) ?? Object.prototype.toString.call(value);
|
||||
} catch {
|
||||
return Object.prototype.toString.call(value);
|
||||
}
|
||||
}
|
||||
|
||||
export { toErrorObject } from "@openclaw/normalization-core/error-coercion";
|
||||
export { stringifyNonErrorCause, toErrorObject } from "@openclaw/normalization-core/error-coercion";
|
||||
|
||||
export function formatUncaughtError(err: unknown): string {
|
||||
if (extractErrorCode(err) === "INVALID_CONFIG") {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
resolveOsHomeRelativePath,
|
||||
resolveRequiredHomeDir,
|
||||
resolveRequiredOsHomeDir,
|
||||
resolveUserPath,
|
||||
} from "./home-dir.js";
|
||||
|
||||
describe("resolveEffectiveHomeDir", () => {
|
||||
@@ -275,6 +276,13 @@ describe("resolveHomeRelativePath", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveUserPath", () => {
|
||||
it("preserves the historical falsy-input contract", () => {
|
||||
expect(resolveUserPath(undefined as unknown as string)).toBe("");
|
||||
expect(resolveUserPath(null as unknown as string)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveOsHomeRelativePath", () => {
|
||||
it("expands tilde paths using the OS home instead of OPENCLAW_HOME", () => {
|
||||
expect(
|
||||
|
||||
@@ -143,16 +143,15 @@ export function resolveHomeRelativePath(
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible alias for resolving user paths against the effective home.
|
||||
*
|
||||
* @deprecated Use resolveHomeRelativePath.
|
||||
*/
|
||||
/** Resolves a user path against the effective home, preserving an empty input. */
|
||||
export function resolveUserPath(
|
||||
input: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homedir: () => string = os.homedir,
|
||||
): string {
|
||||
if (!input) {
|
||||
return "";
|
||||
}
|
||||
return resolveHomeRelativePath(input, { env, homedir });
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
ensureDir,
|
||||
ensureMigrationDir,
|
||||
existsDir,
|
||||
fileExists,
|
||||
isLegacyWhatsAppAuthFile,
|
||||
@@ -18,7 +18,7 @@ describe("state migration fs helpers", () => {
|
||||
const nested = path.join(base, "nested");
|
||||
|
||||
expect(safeReadDir(nested)).toStrictEqual([]);
|
||||
ensureDir(nested);
|
||||
ensureMigrationDir(nested);
|
||||
fs.writeFileSync(path.join(nested, "file.txt"), "ok", "utf8");
|
||||
|
||||
expect(safeReadDir(nested).map((entry) => entry.name)).toEqual(["file.txt"]);
|
||||
|
||||
@@ -27,7 +27,7 @@ export function existsDir(dir: string): boolean {
|
||||
}
|
||||
|
||||
/** Creates a directory tree for migration targets. */
|
||||
export function ensureDir(dir: string) {
|
||||
export function ensureMigrationDir(dir: string) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ import {
|
||||
migrateLegacyDebugProxyCaptureSidecar,
|
||||
} from "./state-migrations.debug-proxy.js";
|
||||
import {
|
||||
ensureDir,
|
||||
ensureMigrationDir,
|
||||
existsDir,
|
||||
fileExists,
|
||||
parseSessionStoreJson5,
|
||||
@@ -3112,7 +3112,7 @@ async function runLegacyMigrationPlans(
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ensureDir(path.dirname(plan.targetPath));
|
||||
ensureMigrationDir(path.dirname(plan.targetPath));
|
||||
if (plan.kind === "move") {
|
||||
fs.renameSync(plan.sourcePath, plan.targetPath);
|
||||
changes.push(`Moved ${plan.label} → ${plan.targetPath}`);
|
||||
@@ -4628,7 +4628,7 @@ async function migrateLegacySessions(
|
||||
return { changes, warnings };
|
||||
}
|
||||
|
||||
ensureDir(detected.sessions.targetDir);
|
||||
ensureMigrationDir(detected.sessions.targetDir);
|
||||
|
||||
const legacyParsed = fileExists(detected.sessions.legacyStorePath)
|
||||
? readSessionStoreJson5(detected.sessions.legacyStorePath)
|
||||
@@ -4895,7 +4895,7 @@ export async function migrateLegacyAgentDir(
|
||||
return { changes, warnings };
|
||||
}
|
||||
|
||||
ensureDir(detected.agentDir.targetDir);
|
||||
ensureMigrationDir(detected.agentDir.targetDir);
|
||||
|
||||
const entries = safeReadDir(detected.agentDir.legacyDir);
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -15,7 +15,11 @@ import type {
|
||||
UnifiedModelCatalogProviderContext,
|
||||
ProviderPluginWizardSetup,
|
||||
} from "../plugins/types.js";
|
||||
import { copyArrayEntries, isRecord, readRecordValue } from "../shared/safe-record.js";
|
||||
import {
|
||||
copyArrayEntries,
|
||||
isRecordWithoutThrowing,
|
||||
readRecordValue,
|
||||
} from "../shared/safe-record.js";
|
||||
import { definePluginEntry } from "./plugin-entry.js";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
@@ -183,11 +187,13 @@ function resolveWizardSetup(params: {
|
||||
}
|
||||
|
||||
function copyProviderAuthOptions(value: unknown): SingleProviderPluginApiKeyAuthOptions[] {
|
||||
return copyArrayEntries(value).filter(isRecord) as SingleProviderPluginApiKeyAuthOptions[];
|
||||
return copyArrayEntries(value).filter(
|
||||
isRecordWithoutThrowing,
|
||||
) as SingleProviderPluginApiKeyAuthOptions[];
|
||||
}
|
||||
|
||||
function copyProviderAuthMethods(value: unknown): ProviderAuthMethod[] {
|
||||
return copyArrayEntries(value).filter(isRecord) as ProviderAuthMethod[];
|
||||
return copyArrayEntries(value).filter(isRecordWithoutThrowing) as ProviderAuthMethod[];
|
||||
}
|
||||
|
||||
function resolveEnvVars(params: {
|
||||
|
||||
@@ -382,6 +382,7 @@ describe("installOpenClawPluginSdkNativeResolver", () => {
|
||||
"boolean-coercion.ts",
|
||||
);
|
||||
const resultSource = writeInternalCorePackageSource(root, "normalization-core", "result.ts");
|
||||
const agentIdSource = writeInternalCorePackageSource(root, "normalization-core", "agent-id.ts");
|
||||
const mediaCoreSource = writeInternalCorePackageSource(root, "media-core", "mime.ts");
|
||||
const markdownCoreSource = writeInternalCorePackageSource(
|
||||
root,
|
||||
@@ -418,6 +419,7 @@ describe("installOpenClawPluginSdkNativeResolver", () => {
|
||||
expect(installedAliases).toContain("@openclaw/normalization-core/string-coerce");
|
||||
expect(installedAliases).toContain("@openclaw/normalization-core/boolean-coercion");
|
||||
expect(installedAliases).toContain("@openclaw/normalization-core/result");
|
||||
expect(installedAliases).toContain("@openclaw/normalization-core/agent-id");
|
||||
expect(installedAliases).toContain("@openclaw/media-core/mime");
|
||||
expect(installedAliases).toContain("@openclaw/markdown-core/code-spans");
|
||||
expect(installedAliases).toContain("@openclaw/ai/internal/retry-after");
|
||||
@@ -437,6 +439,9 @@ describe("installOpenClawPluginSdkNativeResolver", () => {
|
||||
expect(
|
||||
fs.realpathSync(requireFromCoreSource.resolve("@openclaw/normalization-core/result")),
|
||||
).toBe(fs.realpathSync(resultSource));
|
||||
expect(
|
||||
fs.realpathSync(requireFromCoreSource.resolve("@openclaw/normalization-core/agent-id")),
|
||||
).toBe(fs.realpathSync(agentIdSource));
|
||||
expect(fs.realpathSync(requireFromCoreSource.resolve("@openclaw/media-core/mime"))).toBe(
|
||||
fs.realpathSync(mediaCoreSource),
|
||||
);
|
||||
|
||||
@@ -67,21 +67,6 @@ const INTERNAL_CORE_PACKAGE_ALIASES = [
|
||||
["types", "types.ts"],
|
||||
],
|
||||
},
|
||||
{
|
||||
packageName: "@openclaw/normalization-core",
|
||||
packageDir: "normalization-core",
|
||||
subpaths: [
|
||||
["", "index.ts"],
|
||||
["boolean-coercion", "boolean-coercion.ts"],
|
||||
["error-coercion", "error-coercion.ts"],
|
||||
["number-coercion", "number-coercion.ts"],
|
||||
["record-coerce", "record-coerce.ts"],
|
||||
["result", "result.ts"],
|
||||
["string-coerce", "string-coerce.ts"],
|
||||
["string-normalization", "string-normalization.ts"],
|
||||
["utf16-slice", "utf16-slice.ts"],
|
||||
],
|
||||
},
|
||||
{
|
||||
// Mirrors packages/ai/package.json exports; dist file names do not follow
|
||||
// the src layout (dist/diagnostics.mjs <- src/utils/diagnostics.ts), so the
|
||||
@@ -331,15 +316,15 @@ function listInternalCorePackageNativeAliases(
|
||||
}> = [];
|
||||
const internalCorePackageAliases = [
|
||||
...INTERNAL_CORE_PACKAGE_ALIASES,
|
||||
{
|
||||
packageName: "@openclaw/acp-core",
|
||||
packageDir: "acp-core",
|
||||
...["normalization-core", "acp-core"].map((packageDir) => ({
|
||||
packageName: `@openclaw/${packageDir}`,
|
||||
packageDir,
|
||||
subpaths: listWorkspacePackageExportAliasEntries({
|
||||
packageRoot,
|
||||
packageName: "@openclaw/acp-core",
|
||||
packageDir: "acp-core",
|
||||
packageName: `@openclaw/${packageDir}`,
|
||||
packageDir,
|
||||
}).map((entry) => [entry.subpath, entry.srcFile] as const),
|
||||
},
|
||||
})),
|
||||
];
|
||||
for (const entry of internalCorePackageAliases) {
|
||||
for (const [subpath, srcFile] of entry.subpaths) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ModelDefinitionConfig, ModelProviderConfig } from "../config/types
|
||||
import {
|
||||
copyArrayEntries,
|
||||
copyRecordEntries,
|
||||
isRecord,
|
||||
isRecordWithoutThrowing,
|
||||
readRecordValue,
|
||||
} from "../shared/safe-record.js";
|
||||
import type { ProviderCatalogResult } from "./types.js";
|
||||
@@ -92,7 +92,7 @@ export function copyProviderCatalogModels(
|
||||
}
|
||||
|
||||
function copyProviderCatalogModel(model: unknown): ModelDefinitionConfig | undefined {
|
||||
if (!isRecord(model)) {
|
||||
if (!isRecordWithoutThrowing(model)) {
|
||||
return undefined;
|
||||
}
|
||||
const id = readRecordValue(model, "id");
|
||||
@@ -118,7 +118,7 @@ function copyProviderCatalogModel(model: unknown): ModelDefinitionConfig | undef
|
||||
function copyProviderCatalogProviderConfig(
|
||||
providerConfig: unknown,
|
||||
): ModelProviderConfig | undefined {
|
||||
if (!isRecord(providerConfig)) {
|
||||
if (!isRecordWithoutThrowing(providerConfig)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ProviderThinkingProfile,
|
||||
ProviderThinkingPolicyContext,
|
||||
} from "./provider-thinking.types.js";
|
||||
import { PLUGIN_REGISTRY_STATE } from "./runtime-state-key.js";
|
||||
|
||||
type ThinkingProviderPlugin = {
|
||||
id: string;
|
||||
@@ -22,8 +23,6 @@ type ThinkingProviderPlugin = {
|
||||
) => "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | null | undefined;
|
||||
};
|
||||
|
||||
const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
|
||||
|
||||
type ThinkingRegistryState = {
|
||||
activeRegistry?: {
|
||||
providers?: Array<{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Stores active plugin channel registry state for the current runtime.
|
||||
import type { ActivePluginChannelRegistry } from "./channel-registry-state.types.js";
|
||||
|
||||
/** Global symbol that stores process-current plugin registry state. */
|
||||
const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
|
||||
import { PLUGIN_REGISTRY_STATE } from "./runtime-state-key.js";
|
||||
|
||||
type GlobalChannelRegistryState = typeof globalThis & {
|
||||
[PLUGIN_REGISTRY_STATE]?: {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Process-global symbol shared by every plugin registry runtime projection. */
|
||||
export const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PLUGIN_REGISTRY_STATE } from "./runtime-state-key.js";
|
||||
// Stores plugin runtime registry state for the current process lifecycle.
|
||||
import { getActivePluginRegistryWorkspaceDirFromState as getPinnedWorkspaceDirFromState } from "./runtime-workspace-state.js";
|
||||
|
||||
export const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
|
||||
export { PLUGIN_REGISTRY_STATE };
|
||||
|
||||
type PluginRegistry = import("./registry-types.js").PluginRegistry;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Shares plugin runtime workspace state across module reloads.
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import { PLUGIN_REGISTRY_STATE } from "./runtime-state-key.js";
|
||||
|
||||
const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
|
||||
const PINNED_PLUGIN_REGISTRY_WORKSPACE_KEY = Symbol.for(
|
||||
"openclaw.pinnedPluginRegistryWorkspaceDir",
|
||||
);
|
||||
|
||||
@@ -1565,6 +1565,12 @@ describe("plugin sdk alias helpers", () => {
|
||||
srcFile: "result.ts",
|
||||
distFile: "result.mjs",
|
||||
});
|
||||
const normalizationAgentId = writeWorkspacePackageEntry({
|
||||
root: fixture.root,
|
||||
packageDir: "normalization-core",
|
||||
srcFile: "agent-id.ts",
|
||||
distFile: "agent-id.mjs",
|
||||
});
|
||||
const normalizationStringCoerce = writeWorkspacePackageEntry({
|
||||
root: fixture.root,
|
||||
packageDir: "normalization-core",
|
||||
@@ -1641,6 +1647,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
fs.rmSync(normalizationCore.distFile);
|
||||
fs.rmSync(normalizationBooleanCoercion.distFile);
|
||||
fs.rmSync(normalizationResult.distFile);
|
||||
fs.rmSync(normalizationAgentId.distFile);
|
||||
fs.rmSync(normalizationStringCoerce.distFile);
|
||||
fs.rmSync(retry.distFile);
|
||||
fs.rmSync(terminalCore.distFile);
|
||||
@@ -1706,6 +1713,9 @@ describe("plugin sdk alias helpers", () => {
|
||||
expect(fs.realpathSync(aliases["@openclaw/normalization-core/result"] ?? "")).toBe(
|
||||
fs.realpathSync(normalizationResult.srcFile),
|
||||
);
|
||||
expect(fs.realpathSync(aliases["@openclaw/normalization-core/agent-id"] ?? "")).toBe(
|
||||
fs.realpathSync(normalizationAgentId.srcFile),
|
||||
);
|
||||
expect(fs.realpathSync(aliases["@openclaw/normalization-core/string-coerce"] ?? "")).toBe(
|
||||
fs.realpathSync(normalizationStringCoerce.srcFile),
|
||||
);
|
||||
@@ -1860,7 +1870,7 @@ describe("plugin sdk alias helpers", () => {
|
||||
).toBe(fs.realpathSync(modelCatalogCore.distFile));
|
||||
});
|
||||
|
||||
it("derives acp-core aliases from packaged root dist when package metadata is absent", () => {
|
||||
it("derives workspace aliases from packaged root dist when package metadata is absent", () => {
|
||||
const fixture = createPluginSdkAliasFixture();
|
||||
const sourcePluginEntry = writePluginEntry(
|
||||
fixture.root,
|
||||
@@ -1869,6 +1879,14 @@ describe("plugin sdk alias helpers", () => {
|
||||
const acpRuntimeErrors = path.join(fixture.root, "dist", "acp-core", "runtime", "errors.js");
|
||||
mkdirSafeDir(path.dirname(acpRuntimeErrors));
|
||||
fs.writeFileSync(acpRuntimeErrors, "export {};\n", "utf-8");
|
||||
const normalizationAgentId = path.join(
|
||||
fixture.root,
|
||||
"dist",
|
||||
"normalization-core",
|
||||
"agent-id.js",
|
||||
);
|
||||
mkdirSafeDir(path.dirname(normalizationAgentId));
|
||||
fs.writeFileSync(normalizationAgentId, "export {};\n", "utf-8");
|
||||
const cwdWithoutOpenClawPackage = makeTempDir();
|
||||
|
||||
const aliases = withCwd(cwdWithoutOpenClawPackage, () =>
|
||||
@@ -1880,6 +1898,9 @@ describe("plugin sdk alias helpers", () => {
|
||||
expect(fs.realpathSync(aliases["@openclaw/acp-core/runtime/errors"] ?? "")).toBe(
|
||||
fs.realpathSync(acpRuntimeErrors),
|
||||
);
|
||||
expect(fs.realpathSync(aliases["@openclaw/normalization-core/agent-id"] ?? "")).toBe(
|
||||
fs.realpathSync(normalizationAgentId),
|
||||
);
|
||||
});
|
||||
|
||||
it("aliases bundled plugin package public surfaces for source plugin transforms", () => {
|
||||
|
||||
@@ -768,25 +768,6 @@ const WORKSPACE_PACKAGE_ALIAS_ENTRIES: WorkspacePackageAliasEntry[] = [
|
||||
srcFile: "read-byte-stream-with-limit.ts",
|
||||
distFile: "read-byte-stream-with-limit.mjs",
|
||||
},
|
||||
...(
|
||||
[
|
||||
["", "index"],
|
||||
["boolean-coercion", "boolean-coercion"],
|
||||
["error-coercion", "error-coercion"],
|
||||
["number-coercion", "number-coercion"],
|
||||
["record-coerce", "record-coerce"],
|
||||
["result", "result"],
|
||||
["string-coerce", "string-coerce"],
|
||||
["string-normalization", "string-normalization"],
|
||||
["utf16-slice", "utf16-slice"],
|
||||
] as const
|
||||
).map(([subpath, file]) => ({
|
||||
packageName: "@openclaw/normalization-core",
|
||||
packageDir: "normalization-core",
|
||||
subpath,
|
||||
srcFile: `${file}.ts`,
|
||||
distFile: `${file}.mjs`,
|
||||
})),
|
||||
{
|
||||
packageName: "@openclaw/retry",
|
||||
packageDir: "retry",
|
||||
@@ -1343,11 +1324,13 @@ function resolveWorkspacePackageAliasMap(params: {
|
||||
const aliasMap: Record<string, string> = {};
|
||||
const workspacePackageAliasEntries = [
|
||||
...WORKSPACE_PACKAGE_ALIAS_ENTRIES,
|
||||
...listWorkspacePackageExportAliasEntries({
|
||||
packageRoot,
|
||||
packageName: "@openclaw/acp-core",
|
||||
packageDir: "acp-core",
|
||||
}),
|
||||
...["normalization-core", "acp-core"].flatMap((packageDir) =>
|
||||
listWorkspacePackageExportAliasEntries({
|
||||
packageRoot,
|
||||
packageName: `@openclaw/${packageDir}`,
|
||||
packageDir,
|
||||
}),
|
||||
),
|
||||
];
|
||||
for (const entry of workspacePackageAliasEntries) {
|
||||
const alias = entry.subpath ? `${entry.packageName}/${entry.subpath}` : entry.packageName;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isValidAgentId, normalizeAgentId } from "@openclaw/normalization-core/agent-id";
|
||||
// Routing session key helpers build stable session keys from route targets.
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -26,17 +27,12 @@ export {
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "./account-id.js";
|
||||
export { isValidAgentId, normalizeAgentId };
|
||||
|
||||
export const DEFAULT_AGENT_ID = "main";
|
||||
export const DEFAULT_MAIN_KEY = "main";
|
||||
type SessionKeyShape = "missing" | "agent" | "legacy_or_alias" | "malformed_agent";
|
||||
|
||||
// Pre-compiled regex
|
||||
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
||||
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
|
||||
const LEADING_DASH_RE = /^-+/;
|
||||
const TRAILING_DASH_RE = /-+$/;
|
||||
|
||||
function normalizeToken(value: string | undefined | null): string {
|
||||
return normalizeLowercaseStringOrEmpty(value);
|
||||
}
|
||||
@@ -174,36 +170,11 @@ export function scopeLegacySessionKeyToAgent(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeAgentId(value: string | undefined | null): string {
|
||||
const trimmed = (value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_AGENT_ID;
|
||||
}
|
||||
const normalized = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
// Keep it path-safe + shell-friendly.
|
||||
if (VALID_ID_RE.test(trimmed)) {
|
||||
return normalized;
|
||||
}
|
||||
// Best-effort fallback: collapse invalid characters to "-"
|
||||
return (
|
||||
normalized
|
||||
.replace(INVALID_CHARS_RE, "-")
|
||||
.replace(LEADING_DASH_RE, "")
|
||||
.replace(TRAILING_DASH_RE, "")
|
||||
.slice(0, 64) || DEFAULT_AGENT_ID
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeOptionalAgentId(value: unknown): string | undefined {
|
||||
const trimmed = normalizeOptionalString(value);
|
||||
return trimmed ? normalizeAgentId(trimmed) : undefined;
|
||||
}
|
||||
|
||||
export function isValidAgentId(value: string | undefined | null): boolean {
|
||||
const trimmed = (value ?? "").trim();
|
||||
return Boolean(trimmed) && VALID_ID_RE.test(trimmed);
|
||||
}
|
||||
|
||||
export function sanitizeAgentId(value: string | undefined | null): string {
|
||||
return normalizeAgentId(value);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Defensive object guard for values that may have hostile traps. */
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
export function isRecordWithoutThrowing(value: unknown): value is Record<string, unknown> {
|
||||
try {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
} catch {
|
||||
@@ -9,7 +9,7 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
/** Read one property from a record-like value without letting traps escape. */
|
||||
export function readRecordValue(value: unknown, key: string): unknown {
|
||||
if (!isRecord(value)) {
|
||||
if (!isRecordWithoutThrowing(value)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
@@ -52,7 +52,7 @@ export function copyArrayEntries(value: unknown): unknown[] {
|
||||
|
||||
/** Copy record entries whose values are also record-shaped. */
|
||||
export function copyRecordEntries<T>(value: unknown): Array<[string, T]> {
|
||||
if (!isRecord(value)) {
|
||||
if (!isRecordWithoutThrowing(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function copyRecordEntries<T>(value: unknown): Array<[string, T]> {
|
||||
const entry = readRecordValue(value, key);
|
||||
// Callers use this for nested config maps; non-object leaves are ignored so
|
||||
// later code does not need repeated record guards.
|
||||
if (isRecord(entry)) {
|
||||
if (isRecordWithoutThrowing(entry)) {
|
||||
entries.push([key, entry as T]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import { avoidTrailingHighSurrogateBreak } from "@openclaw/normalization-core/utf16-slice";
|
||||
|
||||
export { avoidTrailingHighSurrogateBreak };
|
||||
|
||||
/**
|
||||
* Splits text into bounded chunks using caller-owned soft-break selection.
|
||||
*
|
||||
* The resolver sees each limit-sized window and returns an in-window break index;
|
||||
* invalid indexes fall back to the hard limit so chunking always makes progress.
|
||||
*/
|
||||
export function avoidTrailingHighSurrogateBreak(text: string, start: number, end: number): number {
|
||||
if (end <= start || end >= text.length) {
|
||||
return end;
|
||||
}
|
||||
const previous = text.charCodeAt(end - 1);
|
||||
const next = text.charCodeAt(end);
|
||||
const splitsSurrogatePair =
|
||||
previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff;
|
||||
if (!splitsSurrogatePair) {
|
||||
return end;
|
||||
}
|
||||
const adjusted = end - 1;
|
||||
return adjusted > start ? adjusted : end + 1;
|
||||
}
|
||||
|
||||
export function chunkTextByBreakResolver(
|
||||
text: string,
|
||||
limit: number,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { resolveBundledSkillsContext } from "../loading/bundled-context.js";
|
||||
import {
|
||||
hasBinary,
|
||||
isBundledSkillAllowed,
|
||||
isConfigPathTruthy,
|
||||
isSkillConfigPathTruthy,
|
||||
resolveBundledAllowlist,
|
||||
resolveSkillConfig,
|
||||
resolveSkillsInstallPreferences,
|
||||
@@ -272,7 +272,7 @@ function buildSkillStatus(
|
||||
skillConfig?.env?.[envName] ||
|
||||
(skillConfig?.apiKey && entry.metadata?.primaryEnv === envName),
|
||||
);
|
||||
const isConfigSatisfied = (pathStr: string) => isConfigPathTruthy(config, pathStr);
|
||||
const isConfigSatisfied = (pathStr: string) => isSkillConfigPathTruthy(config, pathStr);
|
||||
const skillSource = indexed.source;
|
||||
const bundled = indexed.bundled;
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ export function resolveSkillsInstallPreferences(config?: OpenClawConfig): Skills
|
||||
return { preferBrew, nodeManager };
|
||||
}
|
||||
|
||||
export function isConfigPathTruthy(config: OpenClawConfig | undefined, pathStr: string): boolean {
|
||||
export function isSkillConfigPathTruthy(
|
||||
config: OpenClawConfig | undefined,
|
||||
pathStr: string,
|
||||
): boolean {
|
||||
return isConfigPathTruthyWithDefaults(config, pathStr, DEFAULT_CONFIG_VALUES);
|
||||
}
|
||||
|
||||
@@ -115,6 +118,6 @@ export function shouldIncludeSkill(params: {
|
||||
skillConfig?.env?.[envName] ||
|
||||
(skillConfig?.apiKey && entry.metadata?.primaryEnv === envName),
|
||||
),
|
||||
isConfigPathTruthy: (configPath) => isConfigPathTruthy(config, configPath),
|
||||
isConfigPathTruthy: (configPath) => isSkillConfigPathTruthy(config, configPath),
|
||||
});
|
||||
}
|
||||
|
||||
+3
-21
@@ -5,12 +5,14 @@ import path from "node:path";
|
||||
import { pathExists as fsSafePathExists } from "./infra/fs-safe.js";
|
||||
import {
|
||||
resolveEffectiveHomeDir,
|
||||
resolveHomeRelativePath,
|
||||
resolveRequiredHomeDir,
|
||||
resolveUserPath,
|
||||
} from "./infra/home-dir.js";
|
||||
import { isPlainObject } from "./infra/plain-object.js";
|
||||
export { escapeRegExp } from "./shared/regexp.js";
|
||||
export { sleep } from "./utils/sleep.js";
|
||||
export { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
export { resolveUserPath };
|
||||
|
||||
/** Creates a directory tree if it does not already exist. */
|
||||
export async function ensureDir(dir: string) {
|
||||
@@ -44,14 +46,6 @@ export function safeParseJson<T>(raw: string): T | null {
|
||||
|
||||
export { isPlainObject };
|
||||
|
||||
/**
|
||||
* Type guard for Record<string, unknown> (less strict than isPlainObject).
|
||||
* Accepts any non-null object that isn't an array.
|
||||
*/
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Normalizes phone-like input into the loose E.164 shape used by channel helpers. */
|
||||
export function normalizeE164(number: string): string {
|
||||
const withoutPrefix = number.replace(/^[a-z][a-z0-9-]*:/i, "").trim();
|
||||
@@ -64,18 +58,6 @@ export function normalizeE164(number: string): string {
|
||||
// to preserve the historical `utils.ts` import surface.
|
||||
export { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
|
||||
/** Resolves `~` and OpenClaw home-relative paths with injectable env/home sources. */
|
||||
export function resolveUserPath(
|
||||
input: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homedir: () => string = os.homedir,
|
||||
): string {
|
||||
if (!input) {
|
||||
return "";
|
||||
}
|
||||
return resolveHomeRelativePath(input, { env, homedir });
|
||||
}
|
||||
|
||||
/** Resolves the OpenClaw config directory from state/config env overrides or home. */
|
||||
export function resolveConfigDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
|
||||
@@ -395,6 +395,10 @@ export const sharedVitestConfig = {
|
||||
find: "@openclaw/net-policy",
|
||||
replacement: path.join(repoRoot, "packages", "net-policy", "src", "index.ts"),
|
||||
},
|
||||
{
|
||||
find: "@openclaw/normalization-core/agent-id",
|
||||
replacement: path.join(repoRoot, "packages", "normalization-core", "src", "agent-id.ts"),
|
||||
},
|
||||
{
|
||||
find: "@openclaw/normalization-core/boolean-coercion",
|
||||
replacement: path.join(
|
||||
|
||||
@@ -133,6 +133,9 @@
|
||||
"@openclaw/markdown-core/types": ["./packages/markdown-core/src/types.ts"],
|
||||
"@openclaw/markdown-core/*": ["./packages/markdown-core/src/*"],
|
||||
"@openclaw/normalization-core": ["./packages/normalization-core/src/index.ts"],
|
||||
"@openclaw/normalization-core/agent-id": [
|
||||
"./packages/normalization-core/src/agent-id.ts"
|
||||
],
|
||||
"@openclaw/normalization-core/boolean-coercion": [
|
||||
"./packages/normalization-core/src/boolean-coercion.ts"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user