fix: optimize routed CLI path (#1195) (thanks @gumadeiras)

This commit is contained in:
Peter Steinberger
2026-01-18 23:28:09 +00:00
parent fac0110e49
commit d5c8172197
9 changed files with 153 additions and 16 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.clawd.bot
- macOS: load menu session previews asynchronously so items populate while the menu is open.
- macOS: use label colors for session preview text so previews render in menu subviews.
- Telegram: honor pairing allowlists for native slash commands.
- CLI: keep banners on routed commands, restore config guarding outside fast-path routing, and tighten fast-path flag parsing while skipping console capture for extra speed. (#1195) — thanks @gumadeiras.
## 2026.1.18-4

View File

@@ -2,9 +2,11 @@ import { describe, expect, it } from "vitest";
import {
buildParseArgv,
getFlagValue,
getCommandPath,
getPrimaryCommand,
hasHelpOrVersion,
hasFlag,
} from "./argv.js";
describe("argv helpers", () => {
@@ -32,6 +34,25 @@ describe("argv helpers", () => {
expect(getPrimaryCommand(["node", "clawdbot"])).toBeNull();
});
it("parses boolean flags and ignores terminator", () => {
expect(hasFlag(["node", "clawdbot", "status", "--json"], "--json")).toBe(true);
expect(hasFlag(["node", "clawdbot", "--", "--json"], "--json")).toBe(false);
});
it("extracts flag values with equals and missing values", () => {
expect(getFlagValue(["node", "clawdbot", "status", "--timeout", "5000"], "--timeout")).toBe(
"5000",
);
expect(getFlagValue(["node", "clawdbot", "status", "--timeout=2500"], "--timeout")).toBe(
"2500",
);
expect(getFlagValue(["node", "clawdbot", "status", "--timeout"], "--timeout")).toBeNull();
expect(getFlagValue(["node", "clawdbot", "status", "--timeout", "--json"], "--timeout")).toBe(
null,
);
expect(getFlagValue(["node", "clawdbot", "--", "--timeout=99"], "--timeout")).toBeUndefined();
});
it("builds parse argv from raw args", () => {
const nodeArgv = buildParseArgv({
programName: "clawdbot",

View File

@@ -1,10 +1,44 @@
const HELP_FLAGS = new Set(["-h", "--help"]);
const VERSION_FLAGS = new Set(["-v", "-V", "--version"]);
const FLAG_TERMINATOR = "--";
export function hasHelpOrVersion(argv: string[]): boolean {
return argv.some((arg) => HELP_FLAGS.has(arg) || VERSION_FLAGS.has(arg));
}
function isValueToken(arg: string | undefined): boolean {
if (!arg) return false;
if (arg === FLAG_TERMINATOR) return false;
if (!arg.startsWith("-")) return true;
return /^-\d+(?:\.\d+)?$/.test(arg);
}
export function hasFlag(argv: string[], name: string): boolean {
const args = argv.slice(2);
for (const arg of args) {
if (arg === FLAG_TERMINATOR) break;
if (arg === name) return true;
}
return false;
}
export function getFlagValue(argv: string[], name: string): string | null | undefined {
const args = argv.slice(2);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === FLAG_TERMINATOR) break;
if (arg === name) {
const next = args[i + 1];
return isValueToken(next) ? next : null;
}
if (arg.startsWith(`${name}=`)) {
const value = arg.slice(name.length + 1);
return value ? value : null;
}
}
return undefined;
}
export function getCommandPath(argv: string[], depth = 2): string[] {
const args = argv.slice(2);
const path: string[] = [];

View File

@@ -229,6 +229,12 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
lines.push(`${label("Cache cap")} ${info(String(status.cache.maxEntries))}`);
}
}
if (status.fallback?.reason) {
lines.push(muted(status.fallback.reason));
}
if (indexError) {
lines.push(`${label("Index error")} ${warn(indexError)}`);
}
defaultRuntime.log(lines.join("\n"));
defaultRuntime.log("");
}
@@ -302,7 +308,66 @@ export function registerMemoryCli(program: Command) {
defaultRuntime.log(lines.join("\n"));
defaultRuntime.log("");
}
await manager.sync({ reason: "cli", force: opts.force });
const startedAt = Date.now();
let lastLabel = "Indexing memory…";
let lastCompleted = 0;
let lastTotal = 0;
const formatElapsed = () => {
const elapsedMs = Math.max(0, Date.now() - startedAt);
const seconds = Math.floor(elapsedMs / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
};
const formatEta = () => {
if (lastTotal <= 0 || lastCompleted <= 0) return null;
const elapsedMs = Math.max(1, Date.now() - startedAt);
const rate = lastCompleted / elapsedMs;
if (!Number.isFinite(rate) || rate <= 0) return null;
const remainingMs = Math.max(0, (lastTotal - lastCompleted) / rate);
const seconds = Math.floor(remainingMs / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${String(remainingSeconds).padStart(2, "0")}`;
};
const buildLabel = () => {
const elapsed = formatElapsed();
const eta = formatEta();
return eta
? `${lastLabel} · elapsed ${elapsed} · eta ${eta}`
: `${lastLabel} · elapsed ${elapsed}`;
};
await withProgressTotals(
{
label: "Indexing memory…",
total: 0,
fallback: opts.verbose ? "line" : undefined,
},
async (update, progress) => {
const interval = setInterval(() => {
progress.setLabel(buildLabel());
}, 1000);
try {
await manager.sync({
reason: "cli",
force: opts.force,
progress: (syncUpdate) => {
if (syncUpdate.label) lastLabel = syncUpdate.label;
lastCompleted = syncUpdate.completed;
lastTotal = syncUpdate.total;
update({
completed: syncUpdate.completed,
total: syncUpdate.total,
label: buildLabel(),
});
progress.setLabel(buildLabel());
},
});
} finally {
clearInterval(interval);
}
},
);
defaultRuntime.log(`Memory index updated (${agentId}).`);
} catch (err) {
const message = formatErrorMessage(err);

View File

@@ -1,5 +1,8 @@
import type { Command } from "commander";
import { defaultRuntime } from "../../runtime.js";
import { emitCliBanner } from "../banner.js";
import { getCommandPath, hasHelpOrVersion, isReadOnlyCommand } from "../argv.js";
import { ensureConfigReady } from "./config-guard.js";
function setProcessTitleForCommand(actionCommand: Command) {
let current: Command = actionCommand;
@@ -15,5 +18,11 @@ export function registerPreActionHooks(program: Command, programVersion: string)
program.hook("preAction", async (_thisCommand, actionCommand) => {
setProcessTitleForCommand(actionCommand);
emitCliBanner(programVersion);
const argv = process.argv;
if (hasHelpOrVersion(argv)) return;
const [primary] = getCommandPath(argv, 1);
if (primary === "doctor") return;
const migrateState = !isReadOnlyCommand(argv);
await ensureConfigReady({ runtime: defaultRuntime, migrateState });
});
}

View File

@@ -6,19 +6,13 @@ import { sessionsCommand } from "../commands/sessions.js";
import { agentsListCommand } from "../commands/agents.js";
import { ensurePluginRegistryLoaded } from "./plugin-registry.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { hasHelpOrVersion, getCommandPath } from "./argv.js";
import { emitCliBanner } from "./banner.js";
import { VERSION } from "../version.js";
import { getCommandPath, getFlagValue, hasFlag, hasHelpOrVersion } from "./argv.js";
import { parsePositiveIntOrUndefined } from "./program/helpers.js";
import { ensureConfigReady } from "./program/config-guard.js";
import { runMemoryStatus } from "./memory-cli.js";
const getFlagValue = (argv: string[], name: string): string | undefined => {
const index = argv.indexOf(name);
if (index === -1) return undefined;
return argv[index + 1];
};
const hasFlag = (argv: string[], name: string): boolean => argv.includes(name);
export async function tryRouteCli(argv: string[]): Promise<boolean> {
if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_ROUTE_FIRST)) return false;
if (hasHelpOrVersion(argv)) return false;
@@ -28,11 +22,13 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
if (!primary) return false;
if (primary === "health") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
ensurePluginRegistryLoaded();
const json = hasFlag(argv, "--json");
const verbose = hasFlag(argv, "--verbose") || hasFlag(argv, "--debug");
const timeout = getFlagValue(argv, "--timeout");
if (timeout === null) return false;
const timeoutMs = parsePositiveIntOrUndefined(timeout);
setVerbose(verbose);
await healthCommand({ json, timeoutMs, verbose }, defaultRuntime);
@@ -40,6 +36,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
}
if (primary === "status") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
ensurePluginRegistryLoaded();
const json = hasFlag(argv, "--json");
@@ -48,6 +45,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
const usage = hasFlag(argv, "--usage");
const verbose = hasFlag(argv, "--verbose") || hasFlag(argv, "--debug");
const timeout = getFlagValue(argv, "--timeout");
if (timeout === null) return false;
const timeoutMs = parsePositiveIntOrUndefined(timeout);
setVerbose(verbose);
await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime);
@@ -55,17 +53,21 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
}
if (primary === "sessions") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
const json = hasFlag(argv, "--json");
const verbose = hasFlag(argv, "--verbose");
const store = getFlagValue(argv, "--store");
if (store === null) return false;
const active = getFlagValue(argv, "--active");
if (active === null) return false;
setVerbose(verbose);
await sessionsCommand({ json, store, active }, defaultRuntime);
return true;
}
if (primary === "agents" && secondary === "list") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
const json = hasFlag(argv, "--json");
const bindings = hasFlag(argv, "--bindings");
@@ -74,7 +76,9 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
}
if (primary === "memory" && secondary === "status") {
emitCliBanner(VERSION, { argv });
const agent = getFlagValue(argv, "--agent");
if (agent === null) return false;
const json = hasFlag(argv, "--json");
const deep = hasFlag(argv, "--deep");
const index = hasFlag(argv, "--index");

View File

@@ -24,14 +24,14 @@ export async function runCli(argv: string[] = process.argv) {
normalizeEnv();
ensureClawdbotCliOnPath();
// Capture all console output into structured logs while keeping stdout/stderr behavior.
enableConsoleCapture();
// Enforce the minimum supported runtime before doing any work.
assertSupportedRuntime();
if (await tryRouteCli(argv)) return;
// Capture all console output into structured logs while keeping stdout/stderr behavior.
enableConsoleCapture();
const { buildProgram } = await import("./program.js");
const program = buildProgram();

View File

@@ -528,6 +528,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
}
async function writeConfigFile(cfg: ClawdbotConfig) {
clearConfigCache();
const dir = path.dirname(configPath);
await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
const json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2)

View File

@@ -5,6 +5,8 @@ export type BooleanParseOptions = {
const DEFAULT_TRUTHY = ["true", "1", "yes", "on"] as const;
const DEFAULT_FALSY = ["false", "0", "no", "off"] as const;
const DEFAULT_TRUTHY_SET = new Set<string>(DEFAULT_TRUTHY);
const DEFAULT_FALSY_SET = new Set<string>(DEFAULT_FALSY);
export function parseBooleanValue(
value: unknown,
@@ -16,9 +18,9 @@ export function parseBooleanValue(
if (!normalized) return undefined;
const truthy = options.truthy ?? DEFAULT_TRUTHY;
const falsy = options.falsy ?? DEFAULT_FALSY;
const truthySet = new Set<string>(truthy);
const falsySet = new Set<string>(falsy);
const truthySet = truthy === DEFAULT_TRUTHY ? DEFAULT_TRUTHY_SET : new Set(truthy);
const falsySet = falsy === DEFAULT_FALSY ? DEFAULT_FALSY_SET : new Set(falsy);
if (truthySet.has(normalized)) return true;
if (falsySet.has(normalized)) return false;
return undefined;
}
}