File size: 8,717 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// Single agent-turn command registration; delegates execution to the Gateway-backed agent command.
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { Command } from "commander";
import { formatDocsLink } from "../../../packages/terminal-core/src/links.js";
import { theme } from "../../../packages/terminal-core/src/theme.js";
import { THINKING_LEVELS_HELP } from "../../auto-reply/thinking.shared.js";
import { inheritOptionFromParent } from "../command-options.js";
import { measureCliCommandStartup } from "../command-startup-timing.js";
import { formatHelpExamples } from "../help-format.js";

function collectFallback(value: string, previous: string[]): string[] {
  return [...previous, value];
}

/** Register `openclaw agent` for one Gateway-backed agent turn. */
export function registerAgentTurnCommand(
  program: Command,
  args: { agentChannelOptions: string },
): void {
  const agent = program
    .command("agent")
    .description("Run an agent turn via the Gateway (use --local for embedded)")
    .option("-m, --message <text>", "Message body for the agent")
    .option("--message-file <path>", "Read the agent message body from a UTF-8 file (max 4 MiB)")
    .option("-t, --to <number>", "Recipient number in E.164 used to derive the session key")
    .option("--session-key <key>", "Explicit session key (agent:<id>:<key>, or scoped to --agent)")
    .option("--session-id <id>", "Use an explicit session id")
    .option("--agent <id>", "Agent id (overrides routing bindings)")
    .option("--model <id>", "Model override for this run (provider/model or model id)")
    .option(
      "--thinking <level>",
      `Thinking level: ${THINKING_LEVELS_HELP.replaceAll("|", " | ")} where supported`,
    )
    .option("--verbose <on|off>", "Persist agent verbose level for the session")
    .option(
      "--channel <channel>",
      `Delivery channel: ${args.agentChannelOptions} (omit to use the main session channel)`,
    )
    .option("--reply-to <target>", "Delivery target override (separate from session routing)")
    .option("--reply-channel <channel>", "Delivery channel override (separate from routing)")
    .option("--reply-account <id>", "Delivery account id override")
    .option(
      "--local",
      "Run the embedded agent locally using configured provider credentials or local CLI logins",
      false,
    )
    .option("--deliver", "Send the agent's reply back to the selected channel", false)
    .option("--json", "Output result as JSON", false)
    .option(
      "--timeout <seconds>",
      "Override agent command timeout (seconds, default 600 or config value)",
    )
    .addHelpText(
      "after",
      () =>
        `
${theme.heading("Examples:")}
${formatHelpExamples([
  ['openclaw agent --to +15555550123 --message "status update"', "Start a new session."],
  ['openclaw agent --agent ops --message "Summarize logs"', "Use a specific agent."],
  ["openclaw agent --agent ops --message-file ./task.md", "Read a multiline message file."],
  [
    'openclaw agent --session-key agent:ops:incident-42 --message "Summarize status"',
    "Target an exact session key.",
  ],
  [
    'openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium',
    "Target a session with explicit thinking level.",
  ],
  [
    'openclaw agent --to +15555550123 --message "Trace logs" --verbose on --json',
    "Enable verbose logging and JSON output.",
  ],
  ['openclaw agent --to +15555550123 --message "Summon reply" --deliver', "Deliver reply."],
  [
    'openclaw agent --agent ops --message "Generate report" --deliver --reply-channel slack --reply-to "#reports"',
    "Send reply to a different channel/target.",
  ],
])}

${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/agent")}`,
    )
    .action(async (opts): Promise<void> => {
      const verboseLevel =
        typeof opts.verbose === "string" ? normalizeLowercaseStringOrEmpty(opts.verbose) : "";
      const [
        { defaultRuntime },
        { runCommandWithRuntime },
        { setVerbose },
        { agentCliCommand },
        { requestExitAfterOneShotOutput },
      ] = await measureCliCommandStartup("agent-action-imports", () =>
        Promise.all([
          import("../../runtime.js"),
          import("../cli-utils.js"),
          import("../../global-state.js"),
          import("../../commands/agent-via-gateway.js"),
          import("../one-shot-exit.js"),
        ]),
      );
      await runCommandWithRuntime(defaultRuntime, async () => {
        setVerbose(verboseLevel === "on");
        await agentCliCommand(opts, defaultRuntime);
        requestExitAfterOneShotOutput(defaultRuntime);
      });
    });

  agent
    .command("exec [message]")
    .description("Run one isolated headless embedded agent turn")
    .option("--message-file <path>", "Read the UTF-8 prompt from a file; use - for stdin")
    .option("--cwd <dir>", "Set both the agent workspace and tool working directory")
    .option("--state-dir <dir>", "Use an existing state directory without deleting it")
    .option(
      "--config <path>",
      "Run against this config file instead of the ambient config (pins a reproducible run)",
    )
    .option("--isolated", "Ignore the ambient config and run against exec defaults only", false)
    .option("--model <provider/model>", "Use an explicit primary model for this run")
    .option("--code-mode <mode>", "Tool mode: direct | auto | code")
    .option("--local-model-lean", "Use the reduced local-model tool surface")
    .option(
      "--thinking <level>",
      `Thinking level: ${THINKING_LEVELS_HELP.replaceAll("|", " | ")} where supported`,
    )
    .option(
      "--fallback <provider/model>",
      "Add an ordered fallback model (repeatable; requires --model)",
      collectFallback,
      [],
    )
    .option("--auth-env-only", "Use provider credentials from environment variables only", false)
    .option("--no-auth-env-only", "Allow stored and external CLI credential discovery")
    .option("--timeout <seconds>", "Agent deadline in seconds", "600")
    .option("--json", "Emit the stable agent-exec JSON envelope", false)
    .addHelpText(
      "after",
      () =>
        `\n${theme.heading("Examples:")}\n${formatHelpExamples([
          ['openclaw agent exec "Fix the failing test"', "Run in the current directory."],
          [
            "openclaw agent exec --message-file task.md --cwd ./repo",
            "Read a prompt file and set the workspace.",
          ],
          [
            'openclaw agent exec "Summarize this repo" --model openai/gpt-6-astra --fallback anthropic/claude-sonnet-4-6 --json',
            "Use an explicit fallback chain and JSON output.",
          ],
          [
            'openclaw agent exec "Inspect this repo" --model ollama/qwen3.5:9b --code-mode code --local-model-lean --json',
            "Force Code Mode with the lean local-model tool surface.",
          ],
        ])}\n\n${theme.muted("Docs:")} ${formatDocsLink("/cli/agent#agent-exec", "docs.openclaw.ai/cli/agent#agent-exec")}`,
    )
    .action(async (message: string | undefined, opts, command): Promise<void> => {
      const parentOpts = command.parent?.opts() as
        | {
            messageFile?: string;
            model?: string;
            thinking?: string;
            timeout?: string;
            json?: boolean;
          }
        | undefined;
      const execOpts = {
        ...opts,
        messageFile: opts.messageFile ?? parentOpts?.messageFile,
        model: opts.model ?? parentOpts?.model,
        thinking: opts.thinking ?? parentOpts?.thinking,
        // Exec --timeout defaults to "600"; inherit a parent flag only when the
        // leaf source is default. An explicit nested --timeout must win.
        timeout: inheritOptionFromParent<string>(command, "timeout") ?? opts.timeout,
        json: opts.json === true || parentOpts?.json === true,
      };
      const [
        { defaultRuntime },
        { runCommandWithRuntime },
        { agentExecCommand },
        { requestExitAfterOneShotOutput },
      ] = await Promise.all([
        import("../../runtime.js"),
        import("../cli-utils.js"),
        import("../../commands/agent-exec.js"),
        import("../one-shot-exit.js"),
      ]);
      await runCommandWithRuntime(defaultRuntime, async () => {
        const result = await agentExecCommand(message, execOpts, defaultRuntime);
        if (result.exitCode !== 0) {
          defaultRuntime.exit(result.exitCode, { resetStream: process.stderr });
          return;
        }
        requestExitAfterOneShotOutput(defaultRuntime, result.exitCode);
      });
    });
}