Skip to content

Commit 3826cda

Browse files
committed
fix(gateway): run export helpers through cli entry
1 parent 771881d commit 3826cda

4 files changed

Lines changed: 198 additions & 5 deletions

File tree

src/auto-reply/reply/commands-diagnostics.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import type { InteractiveReply, MessagePresentationAction } from "../../interact
1111
import { executePluginCommand, matchPluginCommand } from "../../plugins/commands.js";
1212
import type { PluginCommandDiagnosticsSession, PluginCommandResult } from "../../plugins/types.js";
1313
import type { ReplyPayload } from "../types.js";
14-
import { buildCurrentOpenClawCliCommand } from "./commands-openclaw-cli.js";
14+
import {
15+
buildCurrentOpenClawCliCommand,
16+
buildCurrentOpenClawCliExecEnv,
17+
} from "./commands-openclaw-cli.js";
1518
import {
1619
deliverPrivateCommandReply,
1720
readCommandDeliveryTarget,
@@ -323,6 +326,7 @@ async function requestGatewayDiagnosticsExportApproval(
323326
});
324327
const result = await execTool.execute("chat-diagnostics-gateway-export", {
325328
command,
329+
env: buildCurrentOpenClawCliExecEnv(),
326330
security: "allowlist",
327331
ask: "always",
328332
background: true,

src/auto-reply/reply/commands-export-trajectory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import {
2121
buildCurrentOpenClawCliArgv,
2222
buildCurrentOpenClawCliCommand,
23+
buildCurrentOpenClawCliExecEnv,
2324
} from "./commands-openclaw-cli.js";
2425
import {
2526
deliverPrivateCommandReply,
@@ -268,6 +269,7 @@ async function requestTrajectoryExportApproval(
268269
});
269270
const result = await execTool.execute("chat-export-trajectory", {
270271
command: request.command,
272+
env: buildCurrentOpenClawCliExecEnv(),
271273
security: "allowlist",
272274
ask: "always",
273275
background: true,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { createRequire } from "node:module";
2+
// Verifies chat-facing CLI snippets execute the OpenClaw CLI even from harness-hosted gateways.
3+
import path from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import {
6+
buildCurrentOpenClawCliArgv,
7+
buildCurrentOpenClawCliExecEnv,
8+
} from "./commands-openclaw-cli.js";
9+
10+
const requireFromHere = createRequire(import.meta.url);
11+
const originalArgv = [...process.argv];
12+
const repoSourceEntry = path.join(process.cwd(), "src", "entry.ts");
13+
const trustedTsxLoader = requireFromHere.resolve("tsx", { paths: [process.cwd()] });
14+
15+
function setArgv1(value: string): void {
16+
process.argv.splice(0, process.argv.length, process.execPath, value);
17+
}
18+
19+
describe("buildCurrentOpenClawCliArgv", () => {
20+
afterEach(() => {
21+
process.argv.splice(0, process.argv.length, ...originalArgv);
22+
});
23+
24+
it("falls back to the package CLI entry when hosted by a test harness", () => {
25+
setArgv1(path.join(process.cwd(), "scripts", "test-live.mjs"));
26+
27+
expect(buildCurrentOpenClawCliArgv(["sessions", "export-trajectory"])).toEqual([
28+
process.execPath,
29+
"--import",
30+
trustedTsxLoader,
31+
repoSourceEntry,
32+
"sessions",
33+
"export-trajectory",
34+
]);
35+
});
36+
37+
it("preserves a real OpenClaw launcher entry", () => {
38+
setArgv1("/opt/openclaw/openclaw.mjs");
39+
40+
expect(buildCurrentOpenClawCliArgv(["sessions", "export-trajectory"])).toEqual([
41+
process.execPath,
42+
...process.execArgv,
43+
"/opt/openclaw/openclaw.mjs",
44+
"sessions",
45+
"export-trajectory",
46+
]);
47+
});
48+
49+
it("preserves OpenClaw dist entries from the package root", () => {
50+
const distEntry = path.join(process.cwd(), "dist", "entry.js");
51+
setArgv1(distEntry);
52+
53+
expect(buildCurrentOpenClawCliArgv(["sessions", "export-trajectory"])).toEqual([
54+
process.execPath,
55+
...process.execArgv,
56+
distEntry,
57+
"sessions",
58+
"export-trajectory",
59+
]);
60+
});
61+
62+
it("preserves OpenClaw source entries from the package root", () => {
63+
const sourceEntry = path.join(process.cwd(), "src", "entry.ts");
64+
setArgv1(sourceEntry);
65+
66+
expect(buildCurrentOpenClawCliArgv(["sessions", "export-trajectory"])).toEqual([
67+
process.execPath,
68+
...process.execArgv,
69+
sourceEntry,
70+
"sessions",
71+
"export-trajectory",
72+
]);
73+
});
74+
75+
it("does not treat foreign dist entries as OpenClaw launchers", () => {
76+
setArgv1("/app/dist/index.js");
77+
78+
expect(buildCurrentOpenClawCliArgv(["sessions", "export-trajectory"])).toEqual([
79+
process.execPath,
80+
"--import",
81+
trustedTsxLoader,
82+
repoSourceEntry,
83+
"sessions",
84+
"export-trajectory",
85+
]);
86+
});
87+
88+
it("clears inherited Vitest runner environment for CLI child processes", () => {
89+
expect(
90+
buildCurrentOpenClawCliExecEnv({
91+
PATH: "/usr/bin",
92+
VITEST: "true",
93+
VITEST_POOL_ID: "pool",
94+
OPENCLAW_VITEST_MAX_WORKERS: "1",
95+
}),
96+
).toEqual({
97+
VITEST: "",
98+
VITEST_POOL_ID: "",
99+
OPENCLAW_VITEST_MAX_WORKERS: "",
100+
});
101+
});
102+
});

src/auto-reply/reply/commands-openclaw-cli.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,102 @@
11
// Formats OpenClaw CLI command snippets for chat-facing command responses.
2+
import fs from "node:fs";
3+
import { createRequire } from "node:module";
4+
import path from "node:path";
5+
import { isBunRuntime } from "../../daemon/runtime-binary.js";
6+
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
7+
8+
const requireFromHere = createRequire(import.meta.url);
9+
const OPENCLAW_CLI_ENTRY_BASENAMES = new Set(["openclaw", "openclaw.mjs"]);
10+
const OPENCLAW_PACKAGE_ENTRY_PATHS = new Set([
11+
path.join("dist", "entry.js"),
12+
path.join("dist", "entry.mjs"),
13+
path.join("dist", "index.js"),
14+
path.join("dist", "index.mjs"),
15+
path.join("src", "entry.ts"),
16+
]);
17+
const TEST_RUNNER_ENV_PREFIXES = ["VITEST_", "OPENCLAW_VITEST_"];
18+
219
function quoteShellArg(value: string): string {
320
if (process.platform === "win32") {
421
return `'${value.replaceAll("'", "''")}'`;
522
}
623
return `'${value.replaceAll("'", "'\\''")}'`;
724
}
825

26+
function isOpenClawCliLauncherEntry(entry: string): boolean {
27+
return OPENCLAW_CLI_ENTRY_BASENAMES.has(path.basename(entry));
28+
}
29+
30+
function isOpenClawPackageEntry(entry: string, packageRoot: string): boolean {
31+
const relativeEntry = path.relative(path.resolve(packageRoot), path.resolve(entry));
32+
return OPENCLAW_PACKAGE_ENTRY_PATHS.has(relativeEntry);
33+
}
34+
35+
function safeCwd(): string | undefined {
36+
try {
37+
return process.cwd();
38+
} catch {
39+
return undefined;
40+
}
41+
}
42+
43+
function buildPackageRootCliArgvPrefix(packageRoot: string): string[] {
44+
const sourceEntry = path.join(packageRoot, "src", "entry.ts");
45+
if (fs.existsSync(sourceEntry)) {
46+
const tsxLoader = resolveTrustedTsxLoader(packageRoot);
47+
return isBunRuntime(process.execPath)
48+
? [process.execPath, sourceEntry]
49+
: tsxLoader
50+
? [process.execPath, "--import", tsxLoader, sourceEntry]
51+
: [process.execPath, path.join(packageRoot, "openclaw.mjs")];
52+
}
53+
return [process.execPath, path.join(packageRoot, "openclaw.mjs")];
54+
}
55+
56+
function resolveTrustedTsxLoader(packageRoot: string): string | null {
57+
try {
58+
return requireFromHere.resolve("tsx", { paths: [packageRoot] });
59+
} catch {
60+
return null;
61+
}
62+
}
63+
64+
function resolveCurrentOpenClawCliArgvPrefix(): string[] {
65+
const entry = process.argv[1]?.trim();
66+
if (entry && entry !== process.execPath && isOpenClawCliLauncherEntry(entry)) {
67+
return [process.execPath, ...process.execArgv, entry];
68+
}
69+
const entryPackageRoot = entry ? resolveOpenClawPackageRootSync({ argv1: entry }) : null;
70+
if (entry && entryPackageRoot && isOpenClawPackageEntry(entry, entryPackageRoot)) {
71+
return [process.execPath, ...process.execArgv, entry];
72+
}
73+
const packageRoot = resolveOpenClawPackageRootSync({
74+
argv1: entry,
75+
cwd: safeCwd(),
76+
moduleUrl: import.meta.url,
77+
});
78+
if (packageRoot) {
79+
return buildPackageRootCliArgvPrefix(packageRoot);
80+
}
81+
return entry && entry !== process.execPath ? [process.execPath, entry] : [process.execPath];
82+
}
83+
984
/** Reconstructs the current OpenClaw CLI invocation with extra args. */
1085
export function buildCurrentOpenClawCliArgv(args: string[]): string[] {
11-
const entry = process.argv[1]?.trim();
12-
return entry && entry !== process.execPath
13-
? [process.execPath, ...process.execArgv, entry, ...args]
14-
: [process.execPath, ...args];
86+
return [...resolveCurrentOpenClawCliArgvPrefix(), ...args];
87+
}
88+
89+
/** Clears test-runner env inherited by harness-hosted gateways before spawning the CLI. */
90+
export function buildCurrentOpenClawCliExecEnv(
91+
env: NodeJS.ProcessEnv = process.env,
92+
): Record<string, string> | undefined {
93+
const overrides: Record<string, string> = {};
94+
for (const key of Object.keys(env)) {
95+
if (key === "VITEST" || TEST_RUNNER_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
96+
overrides[key] = "";
97+
}
98+
}
99+
return Object.keys(overrides).length > 0 ? overrides : undefined;
15100
}
16101

17102
/** Builds a shell-quoted command string for rerunning the current OpenClaw CLI. */

0 commit comments

Comments
 (0)