diff --git a/CHANGELOG.md b/CHANGELOG.md index f4fea0bacab..9c60adbbfcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/CLI: let flag-driven `openclaw channels add` install the selected channel plugin from its default source without opening an interactive prompt, fixing published npm Telegram setup in stdin-closed automation. Thanks @codex. - Onboarding/setup: keep first-run config reads, plugin compatibility notices, and post-model sanity checks on cold metadata paths unless the user chooses to browse all models, avoiding full plugin/runtime catalog work between prompts. Thanks @shakkernerd. - Onboarding/auth: run manifest-owned provider auth choices through scoped setup providers so selecting OpenAI Codex browser/device auth no longer loads every provider runtime before OAuth starts. Thanks @shakkernerd. - Onboarding/auth: keep the post-auth default-model policy lookup on manifest/setup metadata so the next prompt appears without loading broad provider runtime. Thanks @shakkernerd. diff --git a/docs/cli/channels.md b/docs/cli/channels.md index 2eb834d542a..2b4aa1e07bb 100644 --- a/docs/cli/channels.md +++ b/docs/cli/channels.md @@ -59,6 +59,8 @@ Common non-interactive add surfaces include: - Tlon fields: `--ship`, `--url`, `--code`, `--group-channels`, `--dm-allowlist`, `--auto-discover-channels` - `--use-env` for default-account env-backed auth where supported +If a channel plugin needs to be installed during a flag-driven add command, OpenClaw uses the channel's default install source without opening the interactive plugin install prompt. + When you run `openclaw channels add` without flags, the interactive wizard can prompt: - account ids per selected channel diff --git a/src/cli/daemon-cli.coverage.test.ts b/src/cli/daemon-cli.coverage.test.ts index a7c837470c7..b50e91d6c1f 100644 --- a/src/cli/daemon-cli.coverage.test.ts +++ b/src/cli/daemon-cli.coverage.test.ts @@ -1,3 +1,6 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { Command } from "commander"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { captureEnv } from "../test-utils/env.js"; @@ -139,17 +142,19 @@ function parseFirstJsonRuntimeLine() { describe("daemon-cli coverage", () => { let envSnapshot: ReturnType; + let tmpDir: string; beforeEach(() => { daemonProgram = createDaemonProgram(); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-daemon-cli-")); envSnapshot = captureEnv([ "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH", "OPENCLAW_GATEWAY_PORT", "OPENCLAW_PROFILE", ]); - process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-cli-state"; - process.env.OPENCLAW_CONFIG_PATH = "/tmp/openclaw-cli-state/openclaw.json"; + process.env.OPENCLAW_STATE_DIR = tmpDir; + process.env.OPENCLAW_CONFIG_PATH = path.join(tmpDir, "openclaw.json"); delete process.env.OPENCLAW_GATEWAY_PORT; delete process.env.OPENCLAW_PROFILE; serviceReadCommand.mockResolvedValue(null); @@ -160,6 +165,7 @@ describe("daemon-cli coverage", () => { afterEach(() => { envSnapshot.restore(); + fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("probes gateway status by default", async () => { diff --git a/src/commands/channel-setup/plugin-install.test.ts b/src/commands/channel-setup/plugin-install.test.ts index ebee8943ff4..711486b7253 100644 --- a/src/commands/channel-setup/plugin-install.test.ts +++ b/src/commands/channel-setup/plugin-install.test.ts @@ -430,6 +430,27 @@ describe("ensureChannelSetupPluginInstalled", () => { ); }); + it("uses the bundled default install source without prompting in non-interactive mode", async () => { + const runtime = makeRuntime(); + const { prompter, select } = makeSkipInstallPrompter(); + const cfg: OpenClawConfig = { update: { channel: "beta" } }; + mockBundledChatSource(); + + const result = await ensureChannelSetupPluginInstalled({ + cfg, + entry: baseEntry, + prompter, + runtime, + promptInstall: false, + }); + + expect(select).not.toHaveBeenCalled(); + expect(result.installed).toBe(true); + expect(result.cfg.plugins?.load?.paths).toContain( + bundledPluginRootAt("/opt/openclaw", "bundled-chat"), + ); + }); + it("does not default to bundled local path when an external catalog overrides the npm spec", async () => { const runtime = makeRuntime(); const { prompter, select } = makeSkipInstallPrompter(); diff --git a/src/commands/channel-setup/plugin-install.ts b/src/commands/channel-setup/plugin-install.ts index 293441a4fee..b6ce164b47d 100644 --- a/src/commands/channel-setup/plugin-install.ts +++ b/src/commands/channel-setup/plugin-install.ts @@ -41,6 +41,7 @@ export async function ensureChannelSetupPluginInstalled(params: { prompter: WizardPrompter; runtime: RuntimeEnv; workspaceDir?: string; + promptInstall?: boolean; }): Promise { const result = await ensureOnboardingPluginInstalled({ cfg: params.cfg, @@ -48,6 +49,7 @@ export async function ensureChannelSetupPluginInstalled(params: { prompter: params.prompter, runtime: params.runtime, workspaceDir: params.workspaceDir, + ...(params.promptInstall !== undefined ? { promptInstall: params.promptInstall } : {}), }); return { cfg: result.cfg, diff --git a/src/commands/channels.add.test.ts b/src/commands/channels.add.test.ts index a2ea8912b4a..74775e9ef51 100644 --- a/src/commands/channels.add.test.ts +++ b/src/commands/channels.add.test.ts @@ -501,7 +501,7 @@ describe("channelsAddCommand", () => { ); expect(ensureChannelSetupPluginInstalled).toHaveBeenCalledWith( - expect.objectContaining({ entry: catalogEntry }), + expect.objectContaining({ entry: catalogEntry, promptInstall: false }), ); expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1); expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith( diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index baad702c9f3..653e26f2d57 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -311,6 +311,7 @@ export async function channelsAddCommand( prompter, runtime, workspaceDir, + promptInstall: false, }); nextConfig = result.cfg; if (!result.installed) { diff --git a/src/commands/onboarding-plugin-install.ts b/src/commands/onboarding-plugin-install.ts index d273fef3cf1..52e107c8891 100644 --- a/src/commands/onboarding-plugin-install.ts +++ b/src/commands/onboarding-plugin-install.ts @@ -422,6 +422,7 @@ export async function ensureOnboardingPluginInstalled(params: { prompter: WizardPrompter; runtime: RuntimeEnv; workspaceDir?: string; + promptInstall?: boolean; }): Promise { const { entry, prompter, runtime, workspaceDir } = params; let next = params.cfg; @@ -442,12 +443,15 @@ export async function ensureOnboardingPluginInstalled(params: { bundledLocalPath, hasNpmSpec: Boolean(npmSpec), }); - const choice = await promptInstallChoice({ - entry, - localPath, - defaultChoice, - prompter, - }); + const choice = + params.promptInstall === false + ? defaultChoice + : await promptInstallChoice({ + entry, + localPath, + defaultChoice, + prompter, + }); if (choice === "skip") { return {