diff --git a/src/agents/config.ts b/src/agents/config.ts index e46506adad6..c1a145d6c43 100644 --- a/src/agents/config.ts +++ b/src/agents/config.ts @@ -1,8 +1,7 @@ -import { accessSync, constants, existsSync, readFileSync, realpathSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; -import { basename, dirname, join, resolve, sep, win32 } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { spawnProcessSync } from "./utils/child-process.ts"; // ============================================================================= // Package Detection @@ -20,321 +19,6 @@ export const isBunBinary = import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN"); -/** Detect if Bun is the runtime (compiled binary or bun run) */ -export const isBunRuntime = !!process.versions.bun; - -// ============================================================================= -// Install Method Detection -// ============================================================================= - -export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown"; - -interface SelfUpdateCommandStep { - command: string; - args: string[]; - display: string; -} - -export interface SelfUpdateCommand extends SelfUpdateCommandStep { - steps?: SelfUpdateCommandStep[]; -} - -function makeSelfUpdateCommand( - installStep: SelfUpdateCommandStep, - uninstallStep?: SelfUpdateCommandStep, -): SelfUpdateCommand { - if (!uninstallStep) { - return installStep; - } - return { - ...installStep, - display: `${uninstallStep.display} && ${installStep.display}`, - steps: [uninstallStep, installStep], - }; -} - -function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep { - return { - command, - args, - display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "), - }; -} - -export function detectInstallMethod(): InstallMethod { - if (isBunBinary) { - return "bun-binary"; - } - - const resolvedPath = `${currentDir}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/"); - - if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) { - return "pnpm"; - } - if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) { - return "yarn"; - } - if (isBunRuntime || resolvedPath.includes("/install/global/node_modules/")) { - return "bun"; - } - if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) { - return "npm"; - } - - return "unknown"; -} - -function getInferredNpmInstall(): { root: string; prefix: string } | undefined { - const packageDir = getPackageDir(); - const path = - process.platform === "win32" || packageDir.includes("\\") ? win32 : { basename, dirname }; - const parent = path.dirname(packageDir); - let root: string | undefined; - if ( - path.basename(parent).startsWith("@") && - path.basename(path.dirname(parent)) === "node_modules" - ) { - root = path.dirname(parent); - } else if (path.basename(parent) === "node_modules") { - root = parent; - } - if (!root) { - return undefined; - } - const rootParent = path.dirname(root); - if (path.basename(rootParent) === "lib") { - return { root, prefix: path.dirname(rootParent) }; - } - // Windows global npm prefixes use `\\node_modules`, which is - // indistinguishable from local project installs by path shape alone. Do not - // infer unsupported Windows custom prefixes without `npm root -g` evidence. - return undefined; -} - -function getSelfUpdateCommandForMethod( - method: InstallMethod, - installedPackageName: string, - updatePackageName = installedPackageName, - npmCommand?: string[], -): SelfUpdateCommand | undefined { - switch (method) { - case "bun-binary": - return undefined; - case "pnpm": - return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("pnpm", ["install", "-g", "--ignore-scripts", updatePackageName]), - updatePackageName === installedPackageName - ? undefined - : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]), - ); - case "yarn": - return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), - updatePackageName === installedPackageName - ? undefined - : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), - ); - case "bun": - return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("bun", ["install", "-g", "--ignore-scripts", updatePackageName]), - updatePackageName === installedPackageName - ? undefined - : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), - ); - case "npm": { - const [command = "npm", ...npmArgs] = npmCommand ?? []; - const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(); - const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])]; - const installStep = makeSelfUpdateCommandStep(command, [ - ...prefixArgs, - "install", - "-g", - "--ignore-scripts", - updatePackageName, - ]); - const uninstallStep = - updatePackageName === installedPackageName - ? undefined - : makeSelfUpdateCommandStep(command, [ - ...prefixArgs, - "uninstall", - "-g", - installedPackageName, - ]); - return makeSelfUpdateCommand(installStep, uninstallStep); - } - case "unknown": - return undefined; - } - return undefined; -} - -function readCommandOutput( - command: string, - args: string[], - options: { requireSuccess?: boolean } = {}, -): string | undefined { - const result = spawnProcessSync(command, args, { - encoding: "utf-8", - stdio: ["ignore", "pipe", "pipe"], - }); - if (result.status === 0) { - return result.stdout.trim() || undefined; - } - if (options.requireSuccess) { - const reason = - result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`; - throw new Error(`Failed to run ${[command, ...args].join(" ")}: ${reason}`); - } - return undefined; -} - -function getGlobalPackageRoots( - method: InstallMethod, - _packageName: string, - npmCommand?: string[], -): string[] { - switch (method) { - case "npm": { - const configured = !!npmCommand?.length; - const [command = "npm", ...npmArgs] = npmCommand ?? []; - if (configured && command === "bun") { - const bunBin = readCommandOutput(command, [...npmArgs, "pm", "bin", "-g"], { - requireSuccess: true, - }); - const roots = [join(homedir(), ".bun", "install", "global", "node_modules")]; - if (bunBin) { - roots.push(join(dirname(bunBin), "install", "global", "node_modules")); - } - return roots; - } - const root = readCommandOutput(command, [...npmArgs, "root", "-g"], { - requireSuccess: configured, - }); - const inferred = configured ? undefined : getInferredNpmInstall(); - return [root, inferred?.root].filter((x): x is string => !!x); - } - case "pnpm": { - const root = readCommandOutput("pnpm", ["root", "-g"]); - return root ? [root, dirname(root)] : []; - } - case "yarn": { - const dir = readCommandOutput("yarn", ["global", "dir"]); - return dir ? [dir, join(dir, "node_modules")] : []; - } - case "bun": { - const bunBin = readCommandOutput("bun", ["pm", "bin", "-g"]); - const roots = [join(homedir(), ".bun", "install", "global", "node_modules")]; - if (bunBin) { - roots.push(join(dirname(bunBin), "install", "global", "node_modules")); - } - return roots; - } - case "bun-binary": - case "unknown": - return []; - } - return []; -} - -function normalizeExistingPathForComparison( - path: string, - resolveSymlinks: boolean, -): string | undefined { - const resolvedPath = resolve(path); - if (!existsSync(resolvedPath)) { - return undefined; - } - let normalizedPath = resolvedPath; - if (resolveSymlinks) { - try { - normalizedPath = realpathSync(resolvedPath); - } catch { - return undefined; - } - } - if (process.platform === "win32") { - normalizedPath = normalizedPath.toLowerCase(); - } - return normalizedPath; -} - -function getPathComparisonCandidates(path: string): string[] { - return Array.from( - new Set( - [ - normalizeExistingPathForComparison(path, false), - normalizeExistingPathForComparison(path, true), - ].filter((candidate): candidate is string => !!candidate), - ), - ); -} - -function getEntrypointPackageDir(): string | undefined { - const entrypoint = process.argv[1]; - if (!entrypoint) { - return undefined; - } - let dir = dirname(entrypoint); - while (dir !== dirname(dir)) { - if (existsSync(join(dir, "package.json"))) { - return dir; - } - dir = dirname(dir); - } - return undefined; -} - -function isSelfUpdatePathWritable(): boolean { - const packageDir = getPackageDir(); - try { - accessSync(packageDir, constants.W_OK); - accessSync(dirname(packageDir), constants.W_OK); - return true; - } catch { - return false; - } -} - -function isManagedByGlobalPackageManager( - method: InstallMethod, - packageName: string, - npmCommand?: string[], -): boolean { - const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter( - (dir): dir is string => !!dir, - ); - const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir)); - return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => { - return getPathComparisonCandidates(root).some((normalizedRoot) => { - const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; - return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix)); - }); - }); -} - -export function getSelfUpdateUnavailableInstruction( - packageName: string, - npmCommand?: string[], - updatePackageName = packageName, -): string { - const method = detectInstallMethod(); - if (method === "bun-binary") { - return `Download from: https://github.com/openclaw/openclaw/releases/latest`; - } - const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); - if (command) { - if ( - isManagedByGlobalPackageManager(method, packageName, npmCommand) && - !isSelfUpdatePathWritable() - ) { - return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`; - } - return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; - } - return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`; -} - // ============================================================================= // Package Asset Paths (shipped with executable) // ============================================================================= diff --git a/src/agents/sessions/messages.ts b/src/agents/sessions/messages.ts index 4035a72e4fe..1a78ff04f94 100644 --- a/src/agents/sessions/messages.ts +++ b/src/agents/sessions/messages.ts @@ -1,14 +1,4 @@ -export { - bashExecutionToText, - BRANCH_SUMMARY_PREFIX, - BRANCH_SUMMARY_SUFFIX, - COMPACTION_SUMMARY_PREFIX, - COMPACTION_SUMMARY_SUFFIX, - convertToLlm, - createBranchSummaryMessage, - createCompactionSummaryMessage, - createCustomMessage, -} from "../../../packages/agent-core/src/harness/messages.js"; +export { convertToLlm } from "../../../packages/agent-core/src/harness/messages.js"; export type { BashExecutionMessage, diff --git a/src/agents/sessions/resolve-config-value.ts b/src/agents/sessions/resolve-config-value.ts index fe91e32e12e..43e21cb76fc 100644 --- a/src/agents/sessions/resolve-config-value.ts +++ b/src/agents/sessions/resolve-config-value.ts @@ -114,25 +114,6 @@ export function resolveConfigValueOrThrow(config: string, description: string): throw new Error(`Failed to resolve ${description}`); } -/** - * Resolve all header values using the same resolution logic as API keys. - */ -export function resolveHeaders( - headers: Record | undefined, -): Record | undefined { - if (!headers) { - return undefined; - } - const resolved: Record = {}; - for (const [key, value] of Object.entries(headers)) { - const resolvedValue = resolveConfigValue(value); - if (resolvedValue) { - resolved[key] = resolvedValue; - } - } - return Object.keys(resolved).length > 0 ? resolved : undefined; -} - export function resolveHeadersOrThrow( headers: Record | undefined, description: string, diff --git a/src/agents/sessions/sdk.ts b/src/agents/sessions/sdk.ts index 7dccf6d850b..73b700449a5 100644 --- a/src/agents/sessions/sdk.ts +++ b/src/agents/sessions/sdk.ts @@ -22,7 +22,6 @@ import { DefaultResourceLoader } from "./resource-loader.js"; import { getDefaultSessionDir, SessionManager } from "./session-manager.js"; import { SettingsManager } from "./settings-manager.js"; import { isInstallTelemetryEnabled } from "./telemetry.js"; -import { time } from "./timings.js"; import { createBashTool, createCodingTools, @@ -218,7 +217,6 @@ export async function createAgentSession( if (!resourceLoader) { resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager }); await resourceLoader.reload(); - time("resourceLoader.reload"); } // Check if session has existing data to restore diff --git a/src/agents/sessions/timings.ts b/src/agents/sessions/timings.ts deleted file mode 100644 index 8696616a3fa..00000000000 --- a/src/agents/sessions/timings.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Central timing instrumentation for startup profiling. - * Enable with OPENCLAW_TIMING=1 environment variable. - */ - -const ENABLED = process.env.OPENCLAW_TIMING === "1"; -const timings: Array<{ label: string; ms: number }> = []; -let lastTime = Date.now(); - -export function resetTimings(): void { - if (!ENABLED) { - return; - } - timings.length = 0; - lastTime = Date.now(); -} - -export function time(label: string): void { - if (!ENABLED) { - return; - } - const now = Date.now(); - timings.push({ label, ms: now - lastTime }); - lastTime = now; -} - -export function printTimings(): void { - if (!ENABLED || timings.length === 0) { - return; - } - console.error("\n--- Startup Timings ---"); - for (const t of timings) { - console.error(` ${t.label}: ${t.ms}ms`); - } - console.error(` TOTAL: ${timings.reduce((a, b) => a + b.ms, 0)}ms`); - console.error("------------------------\n"); -} diff --git a/src/agents/utils/child-process.ts b/src/agents/utils/child-process.ts index eaeb0dac449..0f960296892 100644 --- a/src/agents/utils/child-process.ts +++ b/src/agents/utils/child-process.ts @@ -2,11 +2,8 @@ import { type ChildProcess, type ChildProcessByStdio, spawn as nodeSpawn, - spawnSync as nodeSpawnSync, type SpawnOptions, type SpawnOptionsWithStdioTuple, - type SpawnSyncOptionsWithStringEncoding, - type SpawnSyncReturns, type StdioNull, type StdioPipe, } from "node:child_process"; @@ -27,16 +24,6 @@ export function spawnProcess(command: string, args: string[], options: SpawnOpti : nodeSpawn(command, args, options); } -export function spawnProcessSync( - command: string, - args: string[], - options: SpawnSyncOptionsWithStringEncoding, -): SpawnSyncReturns { - return process.platform === "win32" - ? crossSpawn.sync(command, args, options) - : nodeSpawnSync(command, args, options); -} - /** * Wait for a child process to terminate without hanging on inherited stdio handles. * diff --git a/src/infra/json-file.ts b/src/infra/json-file.ts index 96c9f353b78..ad634536d64 100644 --- a/src/infra/json-file.ts +++ b/src/infra/json-file.ts @@ -1,10 +1,9 @@ import "./fs-safe-defaults.js"; import fs from "node:fs"; import path from "node:path"; -import { tryReadJsonSync, tryReadJson, writeJsonSync } from "@openclaw/fs-safe/json"; +import { tryReadJsonSync, writeJsonSync } from "@openclaw/fs-safe/json"; -export { tryReadJson, tryReadJsonSync, writeJsonSync }; -export const readJsonFile = tryReadJson; +export { tryReadJsonSync, writeJsonSync }; function resolveJsonSymlinkTarget(pathname: string): string | undefined { let stat: fs.Stats;