Compare commits
2 Commits
main
...
fix/messag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c01bfcbc12 | ||
|
|
47110e88c7 |
@ -31,6 +31,7 @@ Docs: https://docs.clawd.bot
|
||||
- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs.
|
||||
- TUI: align custom editor initialization with the latest pi-tui API. (#1298) — thanks @sibbl.
|
||||
- CLI: avoid duplicating --profile/--dev flags when formatting commands.
|
||||
- CLI: load channel plugins for commands that need registry-backed lookups. (#1338) — thanks @MaudeBot.
|
||||
- Status: route native `/status` to the active agent so model selection reflects the correct profile. (#1301)
|
||||
- Exec: prefer bash when fish is default shell, falling back to sh if bash is missing. (#1297) — thanks @ysqander.
|
||||
- Exec: merge login-shell PATH for host=gateway exec while keeping daemon PATH minimal. (#1304)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import {
|
||||
channelsAddCommand,
|
||||
channelsCapabilitiesCommand,
|
||||
@ -13,9 +12,11 @@ import { danger } from "../globals.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveCliChannelOptions } from "./channel-options.js";
|
||||
import { runChannelLogin, runChannelLogout } from "./channel-auth.js";
|
||||
import { runCommandWithRuntime } from "./cli-utils.js";
|
||||
import { hasExplicitOptions } from "./command-options.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./program/command-metadata.js";
|
||||
|
||||
const optionNamesAdd = [
|
||||
"channel",
|
||||
@ -58,9 +59,7 @@ function runChannelsCommandWithDanger(action: () => Promise<void>, label: string
|
||||
}
|
||||
|
||||
export function registerChannelsCli(program: Command) {
|
||||
const channelNames = listChannelPlugins()
|
||||
.map((plugin) => plugin.id)
|
||||
.join("|");
|
||||
const channelNames = resolveCliChannelOptions().join("|");
|
||||
const channels = program
|
||||
.command("channels")
|
||||
.description("Manage chat channel accounts")
|
||||
@ -72,6 +71,7 @@ export function registerChannelsCli(program: Command) {
|
||||
"docs.clawd.bot/cli/channels",
|
||||
)}\n`,
|
||||
);
|
||||
markCommandRequiresPluginRegistry(channels);
|
||||
|
||||
channels
|
||||
.command("list")
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { parseAbsoluteTimeMs } from "../../cron/parse.js";
|
||||
import type { CronJob, CronSchedule } from "../../cron/types.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
||||
import { resolveCliChannelOptions } from "../channel-options.js";
|
||||
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||
import { callGatewayFromCli } from "../gateway-rpc.js";
|
||||
|
||||
export const getCronChannelOptions = () =>
|
||||
["last", ...listChannelPlugins().map((plugin) => plugin.id)].join("|");
|
||||
export const getCronChannelOptions = () => ["last", ...resolveCliChannelOptions()].join("|");
|
||||
|
||||
export async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
|
||||
try {
|
||||
|
||||
@ -8,6 +8,7 @@ import { resolveMessageChannelSelection } from "../infra/outbound/channel-select
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./program/command-metadata.js";
|
||||
|
||||
function parseLimit(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
@ -42,6 +43,7 @@ export function registerDirectoryCli(program: Command) {
|
||||
.action(() => {
|
||||
directory.help({ error: true });
|
||||
});
|
||||
markCommandRequiresPluginRegistry(directory);
|
||||
|
||||
const withChannel = (cmd: Command) =>
|
||||
cmd
|
||||
|
||||
@ -34,12 +34,16 @@ vi.mock("../channels/plugins/index.js", () => ({
|
||||
normalizeChannelId,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: vi.fn().mockReturnValue({}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("pairing cli", () => {
|
||||
it("evaluates pairing channels when registering the CLI (not at import)", async () => {
|
||||
it("defers pairing channel lookup until command execution", async () => {
|
||||
listPairingChannels.mockClear();
|
||||
|
||||
const { registerPairingCli } = await import("./pairing-cli.js");
|
||||
@ -49,6 +53,10 @@ describe("pairing cli", () => {
|
||||
program.name("test");
|
||||
registerPairingCli(program);
|
||||
|
||||
expect(listPairingChannels).not.toHaveBeenCalled();
|
||||
|
||||
listChannelPairingRequests.mockResolvedValueOnce([]);
|
||||
await program.parseAsync(["pairing", "list", "telegram"], { from: "user" });
|
||||
expect(listPairingChannels).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@ -10,7 +10,9 @@ import {
|
||||
} from "../pairing/pairing-store.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveCliChannelOptions } from "./channel-options.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./program/command-metadata.js";
|
||||
|
||||
/** Parse channel, allowing extension channels not in core registry. */
|
||||
function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel {
|
||||
@ -44,7 +46,9 @@ async function notifyApproved(channel: PairingChannel, id: string) {
|
||||
}
|
||||
|
||||
export function registerPairingCli(program: Command) {
|
||||
const channels = listPairingChannels();
|
||||
const channelOptions = resolveCliChannelOptions();
|
||||
const channelHint =
|
||||
channelOptions.length > 0 ? `Channel (${channelOptions.join(", ")})` : "Channel";
|
||||
const pairing = program
|
||||
.command("pairing")
|
||||
.description("Secure DM pairing (approve inbound requests)")
|
||||
@ -53,14 +57,16 @@ export function registerPairingCli(program: Command) {
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/pairing", "docs.clawd.bot/cli/pairing")}\n`,
|
||||
);
|
||||
markCommandRequiresPluginRegistry(pairing);
|
||||
|
||||
pairing
|
||||
.command("list")
|
||||
.description("List pending pairing requests")
|
||||
.option("--channel <channel>", `Channel (${channels.join(", ")})`)
|
||||
.argument("[channel]", `Channel (${channels.join(", ")})`)
|
||||
.option("--channel <channel>", channelHint)
|
||||
.argument("[channel]", channelHint)
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (channelArg, opts) => {
|
||||
const channels = listPairingChannels();
|
||||
const channelRaw = opts.channel ?? channelArg;
|
||||
if (!channelRaw) {
|
||||
throw new Error(
|
||||
@ -87,11 +93,12 @@ export function registerPairingCli(program: Command) {
|
||||
pairing
|
||||
.command("approve")
|
||||
.description("Approve a pairing code and allow that sender")
|
||||
.option("--channel <channel>", `Channel (${channels.join(", ")})`)
|
||||
.option("--channel <channel>", channelHint)
|
||||
.argument("<codeOrChannel>", "Pairing code (or channel when using 2 args)")
|
||||
.argument("[code]", "Pairing code (when channel is passed as the 1st arg)")
|
||||
.option("--notify", "Notify the requester on the same channel", false)
|
||||
.action(async (codeOrChannel, code, opts) => {
|
||||
const channels = listPairingChannels();
|
||||
const channelRaw = opts.channel ?? codeOrChannel;
|
||||
const resolvedCode = opts.channel ? codeOrChannel : code;
|
||||
if (!opts.channel && !code) {
|
||||
|
||||
24
src/cli/program/command-metadata.test.ts
Normal file
24
src/cli/program/command-metadata.test.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Command } from "commander";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
commandRequiresPluginRegistry,
|
||||
markCommandRequiresPluginRegistry,
|
||||
} from "./command-metadata.js";
|
||||
|
||||
describe("commandRequiresPluginRegistry", () => {
|
||||
it("detects direct requirement", () => {
|
||||
const program = new Command();
|
||||
const cmd = program.command("message");
|
||||
markCommandRequiresPluginRegistry(cmd);
|
||||
expect(commandRequiresPluginRegistry(cmd)).toBe(true);
|
||||
});
|
||||
|
||||
it("walks parent chain", () => {
|
||||
const program = new Command();
|
||||
const parent = program.command("channels");
|
||||
const child = parent.command("list");
|
||||
markCommandRequiresPluginRegistry(parent);
|
||||
expect(commandRequiresPluginRegistry(child)).toBe(true);
|
||||
});
|
||||
});
|
||||
21
src/cli/program/command-metadata.ts
Normal file
21
src/cli/program/command-metadata.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Command } from "commander";
|
||||
|
||||
const REQUIRES_PLUGIN_REGISTRY = Symbol.for("clawdbot.requiresPluginRegistry");
|
||||
|
||||
type CommandWithPluginRequirement = Command & {
|
||||
[REQUIRES_PLUGIN_REGISTRY]?: boolean;
|
||||
};
|
||||
|
||||
export function markCommandRequiresPluginRegistry(command: Command): Command {
|
||||
(command as CommandWithPluginRequirement)[REQUIRES_PLUGIN_REGISTRY] = true;
|
||||
return command;
|
||||
}
|
||||
|
||||
export function commandRequiresPluginRegistry(command?: Command | null): boolean {
|
||||
let current: Command | null | undefined = command;
|
||||
while (current) {
|
||||
if ((current as CommandWithPluginRequirement)[REQUIRES_PLUGIN_REGISTRY]) return true;
|
||||
current = current.parent ?? undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -86,6 +86,7 @@ const routeSessions: RouteSpec = {
|
||||
|
||||
const routeAgentsList: RouteSpec = {
|
||||
match: (path) => path[0] === "agents" && path[1] === "list",
|
||||
loadPlugins: true,
|
||||
run: async (argv) => {
|
||||
const json = hasFlag(argv, "--json");
|
||||
const bindings = hasFlag(argv, "--bindings");
|
||||
|
||||
56
src/cli/program/preaction.test.ts
Normal file
56
src/cli/program/preaction.test.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { markCommandRequiresPluginRegistry } from "./command-metadata.js";
|
||||
|
||||
vi.mock("../plugin-registry.js", () => ({
|
||||
ensurePluginRegistryLoaded: vi.fn(),
|
||||
}));
|
||||
vi.mock("./config-guard.js", () => ({
|
||||
ensureConfigReady: vi.fn(async () => {}),
|
||||
}));
|
||||
vi.mock("../banner.js", () => ({
|
||||
emitCliBanner: vi.fn(),
|
||||
}));
|
||||
vi.mock("../argv.js", () => ({
|
||||
getCommandPath: vi.fn(() => ["message"]),
|
||||
hasHelpOrVersion: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
const loadRegisterPreActionHooks = async () => {
|
||||
const mod = await import("./preaction.js");
|
||||
return mod.registerPreActionHooks;
|
||||
};
|
||||
|
||||
const loadEnsurePluginRegistryLoaded = async () => {
|
||||
const mod = await import("../plugin-registry.js");
|
||||
return mod.ensurePluginRegistryLoaded;
|
||||
};
|
||||
|
||||
describe("registerPreActionHooks", () => {
|
||||
beforeEach(async () => {
|
||||
const ensurePluginRegistryLoaded = await loadEnsurePluginRegistryLoaded();
|
||||
vi.mocked(ensurePluginRegistryLoaded).mockClear();
|
||||
});
|
||||
|
||||
it("loads plugins for marked commands", async () => {
|
||||
const registerPreActionHooks = await loadRegisterPreActionHooks();
|
||||
const ensurePluginRegistryLoaded = await loadEnsurePluginRegistryLoaded();
|
||||
const program = new Command();
|
||||
registerPreActionHooks(program, "test");
|
||||
|
||||
const message = program.command("message").action(() => {});
|
||||
markCommandRequiresPluginRegistry(message);
|
||||
|
||||
const originalArgv = process.argv;
|
||||
const argv = ["node", "clawdbot", "message"];
|
||||
process.argv = argv;
|
||||
try {
|
||||
await program.parseAsync(argv);
|
||||
} finally {
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
|
||||
expect(vi.mocked(ensurePluginRegistryLoaded)).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -2,6 +2,8 @@ import type { Command } from "commander";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { emitCliBanner } from "../banner.js";
|
||||
import { getCommandPath, hasHelpOrVersion } from "../argv.js";
|
||||
import { ensurePluginRegistryLoaded } from "../plugin-registry.js";
|
||||
import { commandRequiresPluginRegistry } from "./command-metadata.js";
|
||||
import { ensureConfigReady } from "./config-guard.js";
|
||||
|
||||
function setProcessTitleForCommand(actionCommand: Command) {
|
||||
@ -20,8 +22,12 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
||||
emitCliBanner(programVersion);
|
||||
const argv = process.argv;
|
||||
if (hasHelpOrVersion(argv)) return;
|
||||
const needsPlugins = commandRequiresPluginRegistry(actionCommand);
|
||||
const commandPath = getCommandPath(argv, 2);
|
||||
if (commandPath[0] === "doctor") return;
|
||||
await ensureConfigReady({ runtime: defaultRuntime, commandPath });
|
||||
if (needsPlugins) {
|
||||
ensurePluginRegistryLoaded();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -14,10 +14,11 @@ import { theme } from "../../terminal/theme.js";
|
||||
import { hasExplicitOptions } from "../command-options.js";
|
||||
import { createDefaultDeps } from "../deps.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./command-metadata.js";
|
||||
import { collectOption } from "./helpers.js";
|
||||
|
||||
export function registerAgentCommands(program: Command, args: { agentChannelOptions: string }) {
|
||||
program
|
||||
const agent = program
|
||||
.command("agent")
|
||||
.description("Run an agent turn via the Gateway (use --local for embedded)")
|
||||
.requiredOption("-m, --message <text>", "Message body for the agent")
|
||||
@ -67,6 +68,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
|
||||
await agentCliCommand(opts, defaultRuntime, deps);
|
||||
});
|
||||
});
|
||||
markCommandRequiresPluginRegistry(agent);
|
||||
|
||||
const agents = program
|
||||
.command("agents")
|
||||
@ -76,6 +78,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/agents", "docs.clawd.bot/cli/agents")}\n`,
|
||||
);
|
||||
markCommandRequiresPluginRegistry(agents);
|
||||
|
||||
agents
|
||||
.command("list")
|
||||
|
||||
@ -8,9 +8,10 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./command-metadata.js";
|
||||
|
||||
export function registerConfigureCommand(program: Command) {
|
||||
program
|
||||
const configure = program
|
||||
.command("configure")
|
||||
.description("Interactive prompt to set up credentials, devices, and agent defaults")
|
||||
.addHelpText(
|
||||
@ -48,4 +49,5 @@ export function registerConfigureCommand(program: Command) {
|
||||
await configureCommandWithSections(sections as never, defaultRuntime);
|
||||
});
|
||||
});
|
||||
markCommandRequiresPluginRegistry(configure);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Command } from "commander";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import type { ProgramContext } from "./context.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./command-metadata.js";
|
||||
import { createMessageCliHelpers } from "./message/helpers.js";
|
||||
import { registerMessageDiscordAdminCommands } from "./message/register.discord-admin.js";
|
||||
import {
|
||||
@ -39,6 +40,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/message", "docs.clawd.bot/cli/mes
|
||||
.action(() => {
|
||||
message.help({ error: true });
|
||||
});
|
||||
markCommandRequiresPluginRegistry(message);
|
||||
|
||||
const helpers = createMessageCliHelpers(message, ctx.messageChannelOptions);
|
||||
registerMessageSendCommand(message, helpers);
|
||||
|
||||
@ -12,6 +12,7 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./command-metadata.js";
|
||||
|
||||
function resolveInstallDaemonFlag(
|
||||
command: unknown,
|
||||
@ -32,7 +33,7 @@ function resolveInstallDaemonFlag(
|
||||
}
|
||||
|
||||
export function registerOnboardCommand(program: Command) {
|
||||
program
|
||||
const onboard = program
|
||||
.command("onboard")
|
||||
.description("Interactive wizard to set up the gateway, workspace, and skills")
|
||||
.addHelpText(
|
||||
@ -150,4 +151,5 @@ export function registerOnboardCommand(program: Command) {
|
||||
);
|
||||
});
|
||||
});
|
||||
markCommandRequiresPluginRegistry(onboard);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { runCommandWithRuntime } from "../cli-utils.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./command-metadata.js";
|
||||
import { parsePositiveIntOrUndefined } from "./helpers.js";
|
||||
|
||||
function resolveVerbose(opts: { verbose?: boolean; debug?: boolean }): boolean {
|
||||
@ -24,7 +25,7 @@ function parseTimeoutMs(timeout: unknown): number | null | undefined {
|
||||
}
|
||||
|
||||
export function registerStatusHealthSessionsCommands(program: Command) {
|
||||
program
|
||||
const status = program
|
||||
.command("status")
|
||||
.description("Show channel health and recent session recipients")
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
@ -72,8 +73,9 @@ Examples:
|
||||
);
|
||||
});
|
||||
});
|
||||
markCommandRequiresPluginRegistry(status);
|
||||
|
||||
program
|
||||
const health = program
|
||||
.command("health")
|
||||
.description("Fetch health from the running gateway")
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
@ -103,6 +105,7 @@ Examples:
|
||||
);
|
||||
});
|
||||
});
|
||||
markCommandRequiresPluginRegistry(health);
|
||||
|
||||
program
|
||||
.command("sessions")
|
||||
|
||||
@ -5,6 +5,7 @@ import { sandboxExplainCommand } from "../commands/sandbox-explain.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./program/command-metadata.js";
|
||||
|
||||
// --- Types ---
|
||||
|
||||
@ -142,7 +143,7 @@ export function registerSandboxCli(program: Command) {
|
||||
|
||||
// --- Explain Command ---
|
||||
|
||||
sandbox
|
||||
const explain = sandbox
|
||||
.command("explain")
|
||||
.description("Explain effective sandbox/tool policy for a session/agent")
|
||||
.option("--session <key>", "Session key to inspect (defaults to agent main)")
|
||||
@ -161,4 +162,5 @@ export function registerSandboxCli(program: Command) {
|
||||
),
|
||||
),
|
||||
);
|
||||
markCommandRequiresPluginRegistry(explain);
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { fixSecurityFootguns } from "../security/fix.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { markCommandRequiresPluginRegistry } from "./program/command-metadata.js";
|
||||
|
||||
type SecurityAuditOptions = {
|
||||
json?: boolean;
|
||||
@ -36,6 +37,7 @@ export function registerSecurityCli(program: Command) {
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/security", "docs.clawd.bot/cli/security")}\n`,
|
||||
);
|
||||
markCommandRequiresPluginRegistry(security);
|
||||
|
||||
security
|
||||
.command("audit")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user